rust_fuzzylogic/defuzz.rs
1//! Defuzzification: reduce aggregated membership samples to crisp outputs.
2//!
3//! This module implements centroid-of-area (center of gravity) defuzzification
4//! over discretized membership samples produced by aggregation. For each output
5//! variable, the aggregated membership vector `μ[i]` is paired with the grid of
6//! x-coordinates derived from the variable's domain and the vector length, and
7//! the crisp output is computed as:
8//!
9//! `x* = (Σ x[i] · μ[i]) / (Σ μ[i])`, where `i = 0..N-1` and `N ≥ 2`.
10//!
11//! Notes
12//! - The x-grid spacing is `step = (max - min) / (N - 1)` so both domain
13//! endpoints are always sampled.
14//! - If the sum Σ μ[i] is numerically zero, the result will follow IEEE-754
15//! semantics (e.g., `NaN` or `inf`) since no special handling is applied.
16//! Ensure that your aggregated membership carries non-zero mass.
17//!
18//! Example
19//! ```rust
20//! use std::collections::HashMap;
21//! use rust_fuzzylogic::prelude::*;
22//! use rust_fuzzylogic::membership::triangular::Triangular;
23//! use rust_fuzzylogic::term::Term;
24//! use rust_fuzzylogic::variable::Variable;
25//! use rust_fuzzylogic::defuzz::defuzzification;
26//!
27//! // Suppose aggregation produced a single output variable "fan" with N samples
28//! let sampler_n = 5usize;
29//! let mut agg: HashMap<String, Vec<Float>> = HashMap::new();
30//! agg.insert("fan".into(), vec![0.0, 0.2, 0.6, 0.2, 0.0]);
31//!
32//! // The corresponding variable domain is needed to build x[i]
33//! let mut fan = Variable::new(0.0, 10.0).unwrap();
34//! fan.insert_term("high", Term::new("high", Triangular::new(5.0, 7.5, 10.0).unwrap())).unwrap();
35//! let mut vars: HashMap<&str, Variable> = HashMap::new();
36//! vars.insert("fan", fan);
37//!
38//! // Compute centroid for each aggregated output variable
39//! let crisp = defuzzification(&agg, &vars).unwrap();
40//! assert!(crisp["fan"] >= 0.0 && crisp["fan"] <= 10.0);
41//! ```
42// Defuzzification utilities for collapsing aggregated membership values.
43use crate::{error::MissingSpace, prelude::*, variable::Variable};
44use std::{borrow::Borrow, collections::HashMap, hash::Hash};
45
46/// Defuzzify aggregated membership samples using the centroid-of-area method.
47///
48/// For each entry in `agg_memberships`, the function reconstructs a uniform
49/// x-grid from the variable's domain and the vector length, then computes the
50/// centroid `Σ x[i]·μ[i] / Σ μ[i]`.
51///
52/// Type parameters and bounds:
53/// - `KV`: key type for the variables map; must borrow as `str`.
54///
55/// Returns a map from output variable name to its crisp centroid value.
56///
57/// Errors
58/// - `FuzzyError::BadArity` if a membership vector has fewer than 2 samples.
59/// - `FuzzyError::NotFound` if a required output variable is missing from `vars`.
60pub fn defuzzification<KV>(
61 agg_memberships: &HashMap<String, Vec<Float>>,
62 vars: &HashMap<KV, Variable>,
63) -> Result<HashMap<String, Float>>
64where
65 KV: Eq + Hash + Borrow<str>,
66{
67 let mut result_map: HashMap<String, Float> = HashMap::new();
68 for (i, j) in agg_memberships {
69 let num = j.len();
70 if num < 2 {
71 return Err(FuzzyError::BadArity);
72 }
73
74 let (var_min, var_max) = vars
75 .get(&i)
76 .ok_or(FuzzyError::NotFound {
77 space: MissingSpace::Var,
78 key: i.to_string(),
79 })?
80 .domain();
81 let step = (var_max - var_min) / (num as Float - 1.0);
82
83 let (mut sum_agg_memberships_x, mut sum_agg_memberships): (Float, Float) = (0.0, 0.0);
84 let mut l: usize = 0;
85 for k in j {
86 let x = var_min + step * l as Float;
87 sum_agg_memberships_x = sum_agg_memberships_x + (x * k);
88 sum_agg_memberships = sum_agg_memberships + k;
89 l += 1;
90 }
91 result_map.insert(i.to_string(), sum_agg_memberships_x / sum_agg_memberships);
92 }
93
94 return Ok(result_map);
95}