rust_fuzzylogic/
aggregate.rs

1//! Aggregation: combine rule outputs into per-variable membership samples.
2//!
3//! This module implements the aggregation stage for Mamdani-style fuzzy
4//! inference systems. Given a set of rules and crisp inputs, each rule is
5//! evaluated to produce its implied output membership functions on a
6//! discretized grid. Aggregation then combines those contributions across all
7//! rules by taking the pointwise supremum (max) for each output variable.
8//!
9//! Key characteristics:
10//! - Sampling: output membership functions are discretized using a
11//!   `UniformSampler` (evenly spaced samples across the variable domain).
12//! - Implication: individual rules produce per-variable sample vectors via
13//!   `Rule::implicate` using the chosen operator family.
14//! - Aggregation: contributions for the same output variable are merged with
15//!   a pointwise max (`elements_max`).
16//!
17//! Errors from `aggregation` originate from rule activation, implication,
18//! variable lookup, or input lookup and propagate as `FuzzyError`.
19//!
20//! Example
21//! ```rust
22//! use std::collections::HashMap;
23//! use rust_fuzzylogic::prelude::*;
24//! use rust_fuzzylogic::membership::triangular::Triangular;
25//! use rust_fuzzylogic::term::Term;
26//! use rust_fuzzylogic::variable::Variable;
27//! use rust_fuzzylogic::antecedent::Antecedent;
28//! use rust_fuzzylogic::mamdani::{Rule, Consequent};
29//! use rust_fuzzylogic::aggregate::aggregation;
30//!
31//! // Build a minimal system
32//! let mut temp = Variable::new(-10.0, 10.0).unwrap();
33//! temp.insert_term("hot", Term::new("hot", Triangular::new(0.0, 5.0, 10.0).unwrap())).unwrap();
34//! let mut fan = Variable::new(0.0, 10.0).unwrap();
35//! fan.insert_term("high", Term::new("high", Triangular::new(5.0, 7.5, 10.0).unwrap())).unwrap();
36//!
37//! let mut vars: HashMap<&str, Variable> = HashMap::new();
38//! vars.insert("temp", temp);
39//! vars.insert("fan", fan);
40//!
41//! // IF temp IS hot THEN fan IS high
42//! let rule = Rule{
43//!     antecedent: Antecedent::Atom{ var: "temp".into(), term: "hot".into() },
44//!     consequent: vec![Consequent{ var: "fan".into(), term: "high".into() }],
45//! };
46//! let rules = vec![rule];
47//!
48//! let mut input: HashMap<&str, Float> = HashMap::new();
49//! input.insert("temp", 7.5);
50//! let sampler = UniformSampler::default();
51//!
52//! // Aggregate implied outputs for each consequent variable
53//! let agg = aggregation(&rules, &input, &vars, &sampler).unwrap();
54//! assert!(agg.contains_key("fan"));
55//! assert_eq!(agg["fan"].len(), sampler.n);
56//! ```
57
58use crate::{mamdani::Rule, prelude::*, variable::Variable};
59use std::{borrow::Borrow, collections::HashMap, hash::Hash};
60
61/// Combine two membership sample vectors by taking the pointwise maximum.
62///
63/// Merges `src` into `data` in-place using `max` for every index. Both vectors
64/// must have identical length; this function assumes consistent sampling
65/// resolution produced by the same `UniformSampler`.
66///
67/// Example
68/// ```rust
69/// use rust_fuzzylogic::prelude::*;
70/// use rust_fuzzylogic::aggregate::elements_max;
71/// let mut a: Vec<Float> = vec![0.1, 0.3, 0.2];
72/// let b: Vec<Float> = vec![0.0, 0.5, 0.4];
73/// elements_max(&mut a, &b);
74/// assert_eq!(a, vec![0.1, 0.5, 0.4]);
75/// ```
76pub fn elements_max(data: &mut Vec<Float>, src: &Vec<Float>) {
77    for (d, s) in data.iter_mut().zip(src) {
78        *d = d.max(*s)
79    }
80}
81
82/// Aggregate the contributions of all rules into output membership functions.
83///
84/// For each `Rule`, this function computes its activation (`Rule::activation`)
85/// from `input` and `vars`, applies implication (`Rule::implicate`) to obtain
86/// output membership sample vectors, then merges those vectors into a single
87/// map keyed by output variable name using pointwise maxima.
88///
89/// Type parameters and bounds:
90/// - `KI`: key type for the input map (must borrow as `str`) — e.g., `&str`,
91///   `String`, or `Arc<str>`.
92/// - `KV`: key type for the variables map (must borrow as `str`).
93///
94/// Returns a map from output variable name to its aggregated membership samples
95/// (length `sampler.n`).
96///
97/// Errors
98/// - Propagates `FuzzyError::NotFound` if an input or variable is missing.
99/// - Propagates `FuzzyError::TypeMismatch` if a term name is unknown.
100/// - Propagates `FuzzyError::OutOfBounds` if inputs are outside variable domains.
101///
102/// Performance
103/// - Time: O(R * C * N) where `R` = rules, `C` = average number of consequents
104///   per rule, and `N` = sampler resolution per variable.
105/// - Memory: O(V * N) for the aggregated output across `V` output variables.
106pub fn aggregation<KI, KV>(
107    rules: &[Rule],
108    input: &HashMap<KI, Float>,
109    vars: &HashMap<KV, Variable>,
110    sampler: &UniformSampler,
111) -> Result<HashMap<String, Vec<Float>>>
112where
113    KI: Eq + Hash + Borrow<str>,
114    KV: Eq + Hash + Borrow<str>,
115{
116    let mut implicated_map: HashMap<String, Vec<Float>> = HashMap::new();
117    for i in 0..rules.len() {
118        let alpha = rules[i].activation(&input, &vars)?;
119        let implicated = rules[i].implicate(alpha, vars, &sampler)?;
120
121        for (k, v) in implicated {
122            implicated_map
123                .entry(k)
124                .and_modify(|cur| elements_max(cur, &v))
125                .or_insert(v);
126        }
127    }
128
129    return Ok(implicated_map);
130}