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}