rust_fuzzylogic/
antecedent.rs

1use std::{borrow::Borrow, collections::HashMap, hash::Hash};
2
3// Public APIs used by this module:
4// - `prelude::*`: common scalar, error types, and traits (e.g., `Float`, `Result`, `FuzzyError`).
5// - `Variable`: crisp variable with named fuzzy terms and domain validation.
6use crate::{prelude::*, variable::Variable};
7
8/// Antecedent abstract syntax tree (AST) for fuzzy rules.
9///
10/// This enum composes atomic predicates using the default Min–Max family:
11/// - AND: `min(a, b)`
12/// - OR:  `max(a, b)`
13/// - NOT: `1 - a`
14///
15/// Each atomic predicate refers to a variable name and a term name
16/// (e.g., `var = "temp"`, `term = "hot"`).
17#[derive(Debug, Clone, PartialEq)]
18pub enum Antecedent {
19    /// Atomic predicate: membership of `term` for variable `var`.
20    Atom { var: String, term: String },
21    /// Conjunction: `min(left, right)` with the default operator family.
22    And(Box<Self>, Box<Self>),
23    /// Disjunction: `max(left, right)` with the default operator family.
24    Or(Box<Self>, Box<Self>),
25    /// Negation: `1 - value` with the default operator family.
26    Not(Box<Self>),
27}
28
29impl Antecedent {
30    fn iter_atoms<'a>(&'a self) -> impl Iterator<Item = (&'a str, &'a str)> {
31        AtomsIter { stack: vec![self] }
32    }
33}
34
35struct AtomsIter<'a> {
36    stack: Vec<&'a Antecedent>,
37}
38
39impl<'a> Iterator for AtomsIter<'a> {
40    type Item = (&'a str, &'a str);
41    fn next(&mut self) -> Option<Self::Item> {
42        while let Some(node) = self.stack.pop() {
43            match node {
44                Antecedent::Atom { var, term } => return Some((var.as_str(), term.as_str())),
45                Antecedent::And(a, b) | Antecedent::Or(a, b) => {
46                    self.stack.push(b);
47                    self.stack.push(a);
48                }
49                Antecedent::Not(a) => self.stack.push(a),
50            }
51        }
52        None
53    }
54}
55
56/// Evaluate a fuzzy antecedent to a membership degree in [0, 1].
57///
58/// Uses the default Min–Max operator family (AND=min, OR=max, NOT=1−x).
59///
60/// Parameters:
61/// - `ant`: antecedent AST to evaluate.
62/// - `input`: crisp inputs keyed by variable name; key type `KI` must borrow as `str`.
63/// - `vars`: variables keyed by name; key type `KV` must borrow as `str`.
64///
65/// Type bounds:
66/// - `KI: Eq + Hash + Borrow<str>`
67/// - `KV: Eq + Hash + Borrow<str>`
68///
69/// Returns `Ok(y)` with `y ∈ [0, 1]` on success, or an error if a variable or
70/// input is missing, a term is unknown, or the input is outside the variable domain.
71///
72/// Complexity is linear in the AST size; recursion depth equals AST height.
73pub fn eval_antecedent<KI, KV>(
74    ant: &Antecedent,
75    input: &HashMap<KI, Float>,
76    vars: &HashMap<KV, Variable>,
77) -> Result<Float>
78where
79    KI: Eq + Hash + Borrow<str>,
80    KV: Eq + Hash + Borrow<str>,
81{
82    // Recursive evaluation according to the default Min–Max family.
83    match ant {
84        Antecedent::Atom { var, term } => {
85            let v = vars.get(var.as_str()).ok_or(FuzzyError::NotFound {
86                space: crate::error::MissingSpace::Var,
87                key: var.clone(),
88            })?;
89            let x = *input.get(var.as_str()).ok_or(FuzzyError::NotFound {
90                space: crate::error::MissingSpace::Input,
91                key: var.clone(),
92            })?;
93            v.eval(term.as_str(), x)
94        }
95        Antecedent::And(a, b) => {
96            let a = eval_antecedent(a, input, vars)?;
97            let b = eval_antecedent(b, input, vars)?;
98            Ok(a.min(b))
99        }
100        Antecedent::Or(a, b) => {
101            let a = eval_antecedent(a, input, vars)?;
102            let b = eval_antecedent(b, input, vars)?;
103            Ok(a.max(b))
104        }
105        Antecedent::Not(a) => {
106            let a = eval_antecedent(a, input, vars)?;
107            Ok(1.0 - a)
108        }
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use std::collections::HashMap;
115
116    use crate::membership::triangular::Triangular;
117    use crate::prelude::*;
118    use crate::term::Term;
119    use crate::variable::Variable;
120
121    #[test]
122    fn red_antecedent_and_not_behavior() {
123        let eps = crate::Float::EPSILON;
124
125        // Build a variable with two terms: cold and hot.
126        let mut temp = Variable::new(-10.0, 10.0).unwrap();
127        temp.insert_term(
128            "cold",
129            Term::new("cold", Triangular::new(-10.0, -5.0, 0.0).unwrap()),
130        )
131        .unwrap();
132        temp.insert_term(
133            "hot",
134            Term::new("hot", Triangular::new(0.0, 5.0, 10.0).unwrap()),
135        )
136        .unwrap();
137
138        let mut vars: HashMap<&str, Variable> = HashMap::new();
139        vars.insert("temp", temp);
140
141        // Crisp input for temp
142        let mut inputs: HashMap<&str, crate::Float> = HashMap::new();
143        inputs.insert("temp", 7.5);
144
145        // AST: (temp is hot) AND NOT (temp is cold)
146        let ast = crate::antecedent::Antecedent::And(
147            Box::new(crate::antecedent::Antecedent::Atom {
148                var: "temp".into(),
149                term: "hot".into(),
150            }),
151            Box::new(crate::antecedent::Antecedent::Not(Box::new(
152                crate::antecedent::Antecedent::Atom {
153                    var: "temp".into(),
154                    term: "cold".into(),
155                },
156            ))),
157        );
158
159        // Expected with defaults: min(hot(7.5), 1 - cold(7.5))
160        let hot = Triangular::new(0.0, 5.0, 10.0).unwrap().eval(7.5);
161        let cold = Triangular::new(-10.0, -5.0, 0.0).unwrap().eval(7.5);
162        let expected = hot.min(1.0 - cold);
163
164        let y = crate::antecedent::eval_antecedent(&ast, &inputs, &vars).unwrap();
165        assert!((y - expected).abs() < eps);
166    }
167
168    // RED: OR behavior using the same variable at a different crisp value.
169    #[test]
170    fn antecedent_or_behavior() {
171        // Variable setup
172        let mut temp = Variable::new(-10.0, 10.0).unwrap();
173        temp.insert_term(
174            "cold",
175            Term::new("cold", Triangular::new(-10.0, -5.0, 0.0).unwrap()),
176        )
177        .unwrap();
178        temp.insert_term(
179            "hot",
180            Term::new("hot", Triangular::new(0.0, 5.0, 10.0).unwrap()),
181        )
182        .unwrap();
183
184        let mut vars: HashMap<&str, Variable> = HashMap::new();
185        vars.insert("temp", temp);
186
187        let mut inputs: HashMap<&str, crate::Float> = HashMap::new();
188        inputs.insert("temp", -5.0);
189
190        // AST: (temp is cold) OR (temp is hot)
191        let ast = crate::antecedent::Antecedent::Or(
192            Box::new(crate::antecedent::Antecedent::Atom {
193                var: "temp".into(),
194                term: "cold".into(),
195            }),
196            Box::new(crate::antecedent::Antecedent::Atom {
197                var: "temp".into(),
198                term: "hot".into(),
199            }),
200        );
201
202        // Expected with defaults: max(cold(-5), hot(-5)) = 1.0
203        let cold = Triangular::new(-10.0, -5.0, 0.0).unwrap().eval(-5.0);
204        let hot = Triangular::new(0.0, 5.0, 10.0).unwrap().eval(-5.0);
205        let expected = cold.max(hot);
206
207        let y = crate::antecedent::eval_antecedent(&ast, &inputs, &vars).unwrap();
208        assert!((y - expected).abs() < crate::Float::EPSILON);
209    }
210}