Skip to main content

tensorlogic_ir/expr/
defuzzification.rs

1//! Defuzzification methods for converting fuzzy sets to crisp values.
2//!
3//! This module implements various defuzzification strategies used to convert
4//! fuzzy membership functions into concrete numerical values, essential for:
5//! - Fuzzy control systems
6//! - Decision-making under uncertainty
7//! - Fuzzy inference system outputs
8//!
9//! # Defuzzification Methods
10//!
11//! - **Centroid (COA)**: Center of Area/Gravity - most common method
12//! - **Bisector**: Vertical line dividing the area into two equal parts
13//! - **Mean of Maximum (MOM)**: Average of maximum membership values
14//! - **Smallest of Maximum (SOM)**: Leftmost point of maximum membership
15//! - **Largest of Maximum (LOM)**: Rightmost point of maximum membership
16//! - **Weighted Average**: For discrete/singleton fuzzy sets
17
18use std::collections::HashMap;
19
20/// Defuzzification method selection.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum DefuzzificationMethod {
23    /// Center of Area (Centroid) - most widely used
24    Centroid,
25    /// Bisector of Area
26    Bisector,
27    /// Mean of Maximum
28    MeanOfMaximum,
29    /// Smallest (leftmost) of Maximum
30    SmallestOfMaximum,
31    /// Largest (rightmost) of Maximum
32    LargestOfMaximum,
33    /// Weighted Average (for singleton fuzzy sets)
34    WeightedAverage,
35}
36
37/// Represents a fuzzy membership function over a continuous domain.
38///
39/// Discretized as samples over [min, max] range.
40#[derive(Debug, Clone)]
41pub struct FuzzySet {
42    /// Domain minimum value
43    pub min: f64,
44    /// Domain maximum value
45    pub max: f64,
46    /// Membership values at sample points (uniformly spaced)
47    pub memberships: Vec<f64>,
48}
49
50impl FuzzySet {
51    /// Create a new fuzzy set with given range and sample size.
52    pub fn new(min: f64, max: f64, samples: usize) -> Self {
53        Self {
54            min,
55            max,
56            memberships: vec![0.0; samples],
57        }
58    }
59
60    /// Create a fuzzy set from explicit membership values.
61    pub fn from_memberships(min: f64, max: f64, memberships: Vec<f64>) -> Self {
62        Self {
63            min,
64            max,
65            memberships,
66        }
67    }
68
69    /// Get the domain value at a given sample index.
70    fn value_at_index(&self, index: usize) -> f64 {
71        let range = self.max - self.min;
72        let step = range / (self.memberships.len() - 1).max(1) as f64;
73        self.min + index as f64 * step
74    }
75
76    /// Number of samples.
77    pub fn len(&self) -> usize {
78        self.memberships.len()
79    }
80
81    /// Check if empty.
82    pub fn is_empty(&self) -> bool {
83        self.memberships.is_empty()
84    }
85
86    /// Find maximum membership value.
87    fn max_membership(&self) -> f64 {
88        self.memberships.iter().fold(0.0f64, |a, &b| a.max(b))
89    }
90
91    /// Find indices of maximum membership.
92    fn max_membership_indices(&self) -> Vec<usize> {
93        let max_val = self.max_membership();
94        if max_val == 0.0 {
95            return vec![];
96        }
97
98        self.memberships
99            .iter()
100            .enumerate()
101            .filter(|(_, &val)| (val - max_val).abs() < 1e-10)
102            .map(|(i, _)| i)
103            .collect()
104    }
105
106    /// Compute area under the membership function (using trapezoidal rule).
107    fn area(&self) -> f64 {
108        if self.memberships.is_empty() {
109            return 0.0;
110        }
111
112        let step = (self.max - self.min) / (self.memberships.len() - 1).max(1) as f64;
113        let mut area = 0.0;
114
115        for i in 0..self.memberships.len() - 1 {
116            // Trapezoidal rule: (h/2) * (f(x_i) + f(x_{i+1}))
117            area += step * (self.memberships[i] + self.memberships[i + 1]) / 2.0;
118        }
119
120        area
121    }
122
123    /// Compute centroid of the fuzzy set.
124    fn centroid_numerator(&self) -> f64 {
125        if self.memberships.is_empty() {
126            return 0.0;
127        }
128
129        let step = (self.max - self.min) / (self.memberships.len() - 1).max(1) as f64;
130        let mut numerator = 0.0;
131
132        for i in 0..self.memberships.len() - 1 {
133            let x1 = self.value_at_index(i);
134            let x2 = self.value_at_index(i + 1);
135            let y1 = self.memberships[i];
136            let y2 = self.memberships[i + 1];
137
138            // Centroid of trapezoid: x_c = (x1 + x2)/2
139            let x_mid = (x1 + x2) / 2.0;
140            let trap_area = step * (y1 + y2) / 2.0;
141
142            numerator += x_mid * trap_area;
143        }
144
145        numerator
146    }
147}
148
149/// Defuzzify a fuzzy set using the specified method.
150///
151/// Returns the crisp output value, or None if the fuzzy set is empty
152/// or the method cannot be applied.
153pub fn defuzzify(fuzzy_set: &FuzzySet, method: DefuzzificationMethod) -> Option<f64> {
154    match method {
155        DefuzzificationMethod::Centroid => centroid(fuzzy_set),
156        DefuzzificationMethod::Bisector => bisector(fuzzy_set),
157        DefuzzificationMethod::MeanOfMaximum => mean_of_maximum(fuzzy_set),
158        DefuzzificationMethod::SmallestOfMaximum => smallest_of_maximum(fuzzy_set),
159        DefuzzificationMethod::LargestOfMaximum => largest_of_maximum(fuzzy_set),
160        DefuzzificationMethod::WeightedAverage => weighted_average(fuzzy_set),
161    }
162}
163
164/// Centroid (Center of Area) defuzzification.
165///
166/// Computes: ∫x·μ(x)dx / ∫μ(x)dx
167pub fn centroid(fuzzy_set: &FuzzySet) -> Option<f64> {
168    let area = fuzzy_set.area();
169    if area == 0.0 {
170        return None;
171    }
172
173    let numerator = fuzzy_set.centroid_numerator();
174    Some(numerator / area)
175}
176
177/// Bisector of Area defuzzification.
178///
179/// Finds the x value that divides the area under the membership
180/// function into two equal parts.
181pub fn bisector(fuzzy_set: &FuzzySet) -> Option<f64> {
182    let total_area = fuzzy_set.area();
183    if total_area == 0.0 {
184        return None;
185    }
186
187    let target_area = total_area / 2.0;
188    let step = (fuzzy_set.max - fuzzy_set.min) / (fuzzy_set.memberships.len() - 1).max(1) as f64;
189
190    let mut cumulative_area = 0.0;
191
192    for i in 0..fuzzy_set.memberships.len() - 1 {
193        let trap_area = step * (fuzzy_set.memberships[i] + fuzzy_set.memberships[i + 1]) / 2.0;
194        if cumulative_area + trap_area >= target_area {
195            // Linear interpolation within this segment
196            let remaining = target_area - cumulative_area;
197            let fraction = remaining / trap_area;
198            let x = fuzzy_set.value_at_index(i)
199                + fraction * (fuzzy_set.value_at_index(i + 1) - fuzzy_set.value_at_index(i));
200            return Some(x);
201        }
202        cumulative_area += trap_area;
203    }
204
205    // Shouldn't reach here, but return midpoint as fallback
206    Some((fuzzy_set.min + fuzzy_set.max) / 2.0)
207}
208
209/// Mean of Maximum defuzzification.
210///
211/// Returns the average of all x values where membership is maximum.
212pub fn mean_of_maximum(fuzzy_set: &FuzzySet) -> Option<f64> {
213    let max_indices = fuzzy_set.max_membership_indices();
214    if max_indices.is_empty() {
215        return None;
216    }
217
218    let sum: f64 = max_indices
219        .iter()
220        .map(|&i| fuzzy_set.value_at_index(i))
221        .sum();
222
223    Some(sum / max_indices.len() as f64)
224}
225
226/// Smallest of Maximum defuzzification.
227///
228/// Returns the leftmost x value where membership is maximum.
229pub fn smallest_of_maximum(fuzzy_set: &FuzzySet) -> Option<f64> {
230    let max_indices = fuzzy_set.max_membership_indices();
231    max_indices.first().map(|&i| fuzzy_set.value_at_index(i))
232}
233
234/// Largest of Maximum defuzzification.
235///
236/// Returns the rightmost x value where membership is maximum.
237pub fn largest_of_maximum(fuzzy_set: &FuzzySet) -> Option<f64> {
238    let max_indices = fuzzy_set.max_membership_indices();
239    max_indices.last().map(|&i| fuzzy_set.value_at_index(i))
240}
241
242/// Weighted Average defuzzification for singleton fuzzy sets.
243///
244/// Computes: Σ(x_i * μ(x_i)) / Σμ(x_i)
245pub fn weighted_average(fuzzy_set: &FuzzySet) -> Option<f64> {
246    let mut numerator = 0.0;
247    let mut denominator = 0.0;
248
249    for (i, &membership) in fuzzy_set.memberships.iter().enumerate() {
250        let x = fuzzy_set.value_at_index(i);
251        numerator += x * membership;
252        denominator += membership;
253    }
254
255    if denominator == 0.0 {
256        None
257    } else {
258        Some(numerator / denominator)
259    }
260}
261
262/// Singleton fuzzy set for discrete inputs (common in fuzzy control).
263///
264/// Represents a collection of singleton (crisp input, fuzzy membership) pairs.
265#[derive(Debug, Clone)]
266pub struct SingletonFuzzySet {
267    /// Map from crisp value to membership degree
268    pub singletons: HashMap<String, f64>,
269}
270
271impl SingletonFuzzySet {
272    /// Create a new empty singleton fuzzy set.
273    pub fn new() -> Self {
274        Self {
275            singletons: HashMap::new(),
276        }
277    }
278
279    /// Add a singleton (value, membership) pair.
280    pub fn add(&mut self, value: String, membership: f64) {
281        self.singletons.insert(value, membership.clamp(0.0, 1.0));
282    }
283
284    /// Defuzzify using weighted average of singletons.
285    ///
286    /// Assumes values can be parsed as f64.
287    pub fn defuzzify(&self) -> Option<f64> {
288        let mut numerator = 0.0;
289        let mut denominator = 0.0;
290
291        for (value_str, &membership) in &self.singletons {
292            if let Ok(value) = value_str.parse::<f64>() {
293                numerator += value * membership;
294                denominator += membership;
295            }
296        }
297
298        if denominator == 0.0 {
299            None
300        } else {
301            Some(numerator / denominator)
302        }
303    }
304
305    /// Get the singleton with maximum membership (winner-takes-all).
306    pub fn winner_takes_all(&self) -> Option<(String, f64)> {
307        self.singletons
308            .iter()
309            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
310            .map(|(k, &v)| (k.clone(), v))
311    }
312}
313
314impl Default for SingletonFuzzySet {
315    fn default() -> Self {
316        Self::new()
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    fn create_test_fuzzy_set() -> FuzzySet {
325        // Triangular membership function peaked at 0.5
326        FuzzySet::from_memberships(
327            0.0,
328            1.0,
329            vec![0.0, 0.25, 0.5, 0.75, 1.0, 0.75, 0.5, 0.25, 0.0],
330        )
331    }
332
333    #[test]
334    fn test_fuzzy_set_creation() {
335        let fs = FuzzySet::new(0.0, 10.0, 11);
336        assert_eq!(fs.len(), 11);
337        assert!((fs.min - 0.0).abs() < 1e-10);
338        assert!((fs.max - 10.0).abs() < 1e-10);
339    }
340
341    #[test]
342    fn test_value_at_index() {
343        let fs = FuzzySet::new(0.0, 10.0, 11);
344        assert!((fs.value_at_index(0) - 0.0).abs() < 1e-10);
345        assert!((fs.value_at_index(5) - 5.0).abs() < 1e-10);
346        assert!((fs.value_at_index(10) - 10.0).abs() < 1e-10);
347    }
348
349    #[test]
350    fn test_max_membership() {
351        let fs = create_test_fuzzy_set();
352        assert!((fs.max_membership() - 1.0).abs() < 1e-10);
353    }
354
355    #[test]
356    fn test_max_membership_indices() {
357        let fs = create_test_fuzzy_set();
358        let indices = fs.max_membership_indices();
359        assert_eq!(indices.len(), 1);
360        assert_eq!(indices[0], 4);
361    }
362
363    #[test]
364    fn test_centroid() {
365        let fs = create_test_fuzzy_set();
366        let result = centroid(&fs).unwrap();
367        // For symmetric triangular, centroid should be near 0.5
368        assert!((result - 0.5).abs() < 0.1);
369    }
370
371    #[test]
372    fn test_bisector() {
373        let fs = create_test_fuzzy_set();
374        let result = bisector(&fs).unwrap();
375        // Bisector for symmetric shape should be near center
376        assert!((result - 0.5).abs() < 0.1);
377    }
378
379    #[test]
380    fn test_mean_of_maximum() {
381        let fs = create_test_fuzzy_set();
382        let result = mean_of_maximum(&fs).unwrap();
383        // MOM for single peak at 0.5
384        assert!((result - 0.5).abs() < 1e-10);
385    }
386
387    #[test]
388    fn test_smallest_of_maximum() {
389        let fs = FuzzySet::from_memberships(0.0, 1.0, vec![0.0, 0.5, 1.0, 1.0, 1.0, 0.5, 0.0]);
390        let result = smallest_of_maximum(&fs).unwrap();
391        // Leftmost max at index 2 → 2/6 ≈ 0.33
392        assert!((result - 0.333).abs() < 0.05);
393    }
394
395    #[test]
396    fn test_largest_of_maximum() {
397        let fs = FuzzySet::from_memberships(0.0, 1.0, vec![0.0, 0.5, 1.0, 1.0, 1.0, 0.5, 0.0]);
398        let result = largest_of_maximum(&fs).unwrap();
399        // Rightmost max at index 4 → 4/6 ≈ 0.67
400        assert!((result - 0.667).abs() < 0.05);
401    }
402
403    #[test]
404    fn test_weighted_average() {
405        let fs = FuzzySet::from_memberships(0.0, 10.0, vec![0.2, 0.5, 0.8, 0.5, 0.2]);
406        let result = weighted_average(&fs).unwrap();
407        // Should be weighted toward middle (index 2)
408        assert!(result > 4.0 && result < 6.0);
409    }
410
411    #[test]
412    fn test_empty_fuzzy_set() {
413        let fs = FuzzySet::from_memberships(0.0, 1.0, vec![0.0, 0.0, 0.0]);
414        assert!(centroid(&fs).is_none());
415        assert!(bisector(&fs).is_none());
416        assert!(mean_of_maximum(&fs).is_none());
417    }
418
419    #[test]
420    fn test_singleton_fuzzy_set() {
421        let mut sfs = SingletonFuzzySet::new();
422        sfs.add("0.0".to_string(), 0.2);
423        sfs.add("5.0".to_string(), 0.8);
424        sfs.add("10.0".to_string(), 0.3);
425
426        let result = sfs.defuzzify().unwrap();
427        // Weighted average: (0*0.2 + 5*0.8 + 10*0.3) / (0.2 + 0.8 + 0.3)
428        // = (0 + 4 + 3) / 1.3 ≈ 5.38
429        assert!((result - 5.38).abs() < 0.1);
430    }
431
432    #[test]
433    fn test_singleton_winner_takes_all() {
434        let mut sfs = SingletonFuzzySet::new();
435        sfs.add("low".to_string(), 0.3);
436        sfs.add("medium".to_string(), 0.8);
437        sfs.add("high".to_string(), 0.5);
438
439        let (winner, membership) = sfs.winner_takes_all().unwrap();
440        assert_eq!(winner, "medium");
441        assert!((membership - 0.8).abs() < 1e-10);
442    }
443
444    #[test]
445    fn test_defuzzify_dispatch() {
446        let fs = create_test_fuzzy_set();
447
448        assert!(defuzzify(&fs, DefuzzificationMethod::Centroid).is_some());
449        assert!(defuzzify(&fs, DefuzzificationMethod::Bisector).is_some());
450        assert!(defuzzify(&fs, DefuzzificationMethod::MeanOfMaximum).is_some());
451        assert!(defuzzify(&fs, DefuzzificationMethod::SmallestOfMaximum).is_some());
452        assert!(defuzzify(&fs, DefuzzificationMethod::LargestOfMaximum).is_some());
453        assert!(defuzzify(&fs, DefuzzificationMethod::WeightedAverage).is_some());
454    }
455}