rust_fuzzylogic/
mamdani.rs

1//! Mamdani inference: rules, activation, and implication (clipping).
2//!
3//! This module defines the core building blocks of a Mamdani-style fuzzy
4//! inference system:
5//! - `Rule`: pairs an `Antecedent` (IF) with one or more `Consequent`s (THEN).
6//! - `Rule::activation`: evaluates the antecedent using the default Min–Max
7//!   operator family (AND=min, OR=max, NOT=1-x) to produce an activation
8//!   degree `alpha ∈ [0, 1]` for the current crisp inputs.
9//! - `Rule::implicate`: applies implication using clipping (`min(alpha, μ_B(x))`)
10//!   to produce discretized output membership samples for each consequent.
11//!
12//! The resulting per-variable membership samples can be aggregated across rules
13//! (see `crate::aggregate`) and then defuzzified to crisp outputs (see
14//! `crate::defuzz`).
15//!
16//! Example
17//! ```rust
18//! use std::collections::HashMap;
19//! use rust_fuzzylogic::prelude::*;
20//! use rust_fuzzylogic::membership::triangular::Triangular;
21//! use rust_fuzzylogic::term::Term;
22//! use rust_fuzzylogic::variable::Variable;
23//! use rust_fuzzylogic::antecedent::Antecedent;
24//! use rust_fuzzylogic::mamdani::{Rule, Consequent};
25//!
26//! // Variable and terms
27//! let mut temp = Variable::new(-10.0, 10.0).unwrap();
28//! temp.insert_term("hot", Term::new("hot", Triangular::new(0.0, 5.0, 10.0).unwrap())).unwrap();
29//! let mut fan = Variable::new(0.0, 10.0).unwrap();
30//! fan.insert_term("high", Term::new("high", Triangular::new(5.0, 7.5, 10.0).unwrap())).unwrap();
31//!
32//! // IF temp IS hot THEN fan IS high
33//! let rule = Rule{
34//!     antecedent: Antecedent::Atom{ var: "temp".into(), term: "hot".into() },
35//!     consequent: vec![Consequent{ var: "fan".into(), term: "high".into() }],
36//! };
37//!
38//! // Activation for a crisp input
39//! let mut inputs: HashMap<&str, Float> = HashMap::new();
40//! inputs.insert("temp", 7.5);
41//! let mut vars: HashMap<&str, Variable> = HashMap::new();
42//! vars.insert("temp", temp);
43//! vars.insert("fan", fan);
44//!
45//! let alpha = rule.activation(&inputs, &vars).unwrap();
46//! assert!(alpha > 0.0 && alpha <= 1.0);
47//!
48//! // Implication discretizes μ_B(x) clipped by alpha across the domain
49//! let sampler = UniformSampler::default();
50//! let implied = rule.implicate(alpha, &vars, &sampler).unwrap();
51//! assert_eq!(implied["fan"].len(), sampler.n);
52//! ```
53
54use std::{borrow::Borrow, collections::HashMap, hash::Hash};
55
56//#[cfg(feature = "inference-mamdani")]
57use crate::{
58    antecedent::{eval_antecedent, Antecedent},
59    error::{FuzzyError, MissingSpace},
60    prelude::*,
61    sampler::UniformSampler,
62    variable::Variable,
63};
64
65/// Output clause (THEN-part) referencing a linguistic variable and term.
66///
67/// A `Consequent` ties a linguistic variable (e.g., `"fan"`) to one of its
68/// labeled membership functions/terms (e.g., `"high"`). During implication,
69/// the membership function for `term` is sampled over the variable domain and
70/// clipped by the rule activation `alpha`.
71#[derive(Clone)]
72pub struct Consequent {
73    /// Target output variable name this consequent refers to.
74    pub var: String,
75    /// Term label within the target variable to be used during implication.
76    pub term: String,
77    //pub weight: Float,
78    //pub imp: Implication,
79}
80
81impl Consequent {
82    /// Validate that `term` matches this consequent's term label.
83    ///
84    /// Returns `Ok(())` if the provided `term` equals `self.term`, otherwise
85    /// returns `FuzzyError::NotFound { space: Term, key: term }`.
86    pub fn get_term(&self, term: String) -> Result<()> {
87        // Simple equality check; does not perform variable lookup.
88        if term == self.term {
89            return Ok(());
90        } else {
91            return Err(FuzzyError::NotFound {
92                space: MissingSpace::Term,
93                key: term,
94            });
95        }
96    }
97
98    /// Validate that `vars` matches this consequent's variable name.
99    ///
100    /// Returns `Ok(())` if the provided `vars` equals `self.var`, otherwise
101    /// returns `FuzzyError::NotFound { space: Var, key: vars }`.
102    pub fn get_vars(&self, vars: String) -> Result<()> {
103        if vars == self.var {
104            return Ok(());
105        } else {
106            return Err(FuzzyError::NotFound {
107                space: MissingSpace::Var,
108                key: vars,
109            });
110        }
111    }
112}
113
114/// Full fuzzy rule pairing an antecedent with one or more consequents.
115///
116/// - `antecedent` encodes the IF-part as an expression tree of atomic
117///   predicates combined with AND/OR/NOT using the default Min–Max family.
118/// - `consequent` lists one or more THEN-part outputs; each is evaluated over
119///   its variable domain during implication.
120#[derive(Clone)]
121pub struct Rule {
122    /// IF-part expressed as an `Antecedent` AST over input variables/terms.
123    pub antecedent: Antecedent,
124    /// THEN-parts listing output variable/term pairs to implicate.
125    pub consequent: Vec<Consequent>,
126}
127
128//Mamdani Inference Engine
129//#[cfg(feature = "inference-mamdani")]
130impl Rule {
131    /// Evaluate the antecedent against crisp inputs to obtain activation.
132    ///
133    /// Type parameters
134    /// - `KI`: key type for `input`, must borrow as `str` (e.g., `&str`).
135    /// - `KV`: key type for `vars`, must borrow as `str`.
136    ///
137    /// Returns the activation degree `alpha ∈ [0, 1]` for this rule.
138    ///
139    /// Errors
140    /// - `FuzzyError::NotFound` if an input or variable is missing.
141    /// - `FuzzyError::TypeMismatch` if the antecedent references an unknown term.
142    /// - `FuzzyError::OutOfBounds` if an input value lies outside a variable's domain.
143    pub fn activation<KI, KV>(
144        &self,
145        input: &HashMap<KI, Float>,
146        vars: &HashMap<KV, Variable>,
147    ) -> Result<Float>
148    where
149        KI: Eq + Hash + Borrow<str>,
150        KV: Eq + Hash + Borrow<str>,
151    {
152        eval_antecedent(&self.antecedent, input, vars)
153    }
154
155    /// Apply implication to produce discretized membership outputs.
156    ///
157    /// For each `Consequent`, this function:
158    /// 1) retrieves the target variable's domain, 2) builds an evenly spaced
159    ///    grid of `sampler.n` points, 3) evaluates the consequent term's
160    ///    membership `μ_B(x)` on that grid, and 4) applies clipping via
161    ///    `min(alpha, μ_B(x))`.
162    ///
163    /// The result is a map from variable name to the vector of sampled values.
164    ///
165    /// Errors
166    /// - `FuzzyError::NotFound` if a variable or term cannot be found.
167    /// - `FuzzyError::OutOfBounds` if evaluation occurs outside the domain.
168    ///
169    /// Note
170    /// - The x-grid spacing is `(max - min) / (sampler.n - 1)` so the vector
171    ///   always includes both domain endpoints.
172    pub fn implicate<KV>(
173        &self,
174        alpha: Float,
175        vars: &HashMap<KV, Variable>,
176        sampler: &UniformSampler,
177    ) -> Result<HashMap<String, Vec<Float>>>
178    where
179        KV: Eq + Hash + Borrow<str>,
180    {
181        // Accumulate per-output-variable membership samples produced by this rule.
182        let mut result_map: HashMap<String, Vec<Float>> = HashMap::new();
183
184        for i in 0..self.consequent.len() {
185            let cons = &self.consequent[i];
186            let var_name = cons.var.as_str();
187            let var_ref = vars.get(var_name).ok_or(FuzzyError::NotFound {
188                space: MissingSpace::Var,
189                key: cons.var.clone(),
190            })?;
191
192            let (dom_min, dom_max) = var_ref.domain();
193            // Prepare a buffer of N samples for μ_B(x) clipped by alpha.
194            let mut result_vec = vec![0.0; sampler.n];
195            let step = (dom_max - dom_min) / (sampler.n - 1) as Float;
196
197            for k in 0..sampler.n {
198                // Uniform grid including both endpoints.
199                let x = dom_min + (k as Float * step);
200                // Evaluate μ_B(x) and apply Mamdani clipping: min(alpha, μ_B(x)).
201                result_vec[k] = var_ref.eval(cons.term.as_str(), x)?.min(alpha);
202            }
203
204            // Store samples under the output variable name.
205            result_map.insert(cons.var.clone(), result_vec);
206        }
207        return Ok(result_map);
208        //TODO: Return type should be hashmap<string, Vec<Float>> where string signifies the variable(eg "fanspeed")
209    }
210
211    /// Placeholder: check if a given variable exists within a rule.
212    pub fn get_vars(self) {
213        unimplemented!()
214    }
215
216    /// Validate that this rule references only existing variables and terms.
217    ///
218    /// Ensures all antecedent atoms and consequents point to variables present
219    /// in `vars` and that the referenced term labels exist within those
220    /// variables' term maps. Returns `Ok(())` on success, or `NotFound` errors
221    /// identifying the missing `Var` or `Term`.
222    pub fn validate<KV>(&self, vars: &HashMap<KV, Variable>) -> Result<()>
223    where
224        KV: Eq + Hash + Borrow<str>,
225    {
226        // Validate antecedent atoms reference existing var/term in `vars`.
227        fn validate_antecedent<KV>(ant: &Antecedent, vars: &HashMap<KV, Variable>) -> Result<()>
228        where
229            KV: Eq + Hash + Borrow<str>,
230        {
231            match ant {
232                Antecedent::Atom { var, term } => {
233                    // Variable must exist.
234                    let v = vars.get(var.as_str()).ok_or(FuzzyError::NotFound {
235                        space: MissingSpace::Var,
236                        key: var.clone(),
237                    })?;
238                    // Term must exist on the variable.
239                    if !v.terms.contains_key(term.as_str()) {
240                        return Err(FuzzyError::NotFound {
241                            space: MissingSpace::Term,
242                            key: term.clone(),
243                        });
244                    }
245                    Ok(())
246                }
247                Antecedent::And(a, b) | Antecedent::Or(a, b) => {
248                    // Validate both sides.
249                    validate_antecedent(a, vars)?;
250                    validate_antecedent(b, vars)
251                }
252                Antecedent::Not(a) => validate_antecedent(a, vars),
253            }
254        }
255
256        validate_antecedent(&self.antecedent, vars)?;
257
258        // Validate consequents reference existing var/term.
259        for cons in &self.consequent {
260            let v = vars.get(cons.var.as_str()).ok_or(FuzzyError::NotFound {
261                space: MissingSpace::Var,
262                key: cons.var.clone(),
263            })?;
264            // Consequent term must exist on the target variable.
265            if !v.terms.contains_key(cons.term.as_str()) {
266                return Err(FuzzyError::NotFound {
267                    space: MissingSpace::Term,
268                    key: cons.term.clone(),
269                });
270            }
271        }
272
273        Ok(())
274    }
275}