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
29/// Evaluate a fuzzy antecedent to a membership degree in [0, 1].
30///
31/// Uses the default Min–Max operator family (AND=min, OR=max, NOT=1−x).
32///
33/// Parameters:
34/// - `ant`: antecedent AST to evaluate.
35/// - `input`: crisp inputs keyed by variable name; key type `KI` must borrow as `str`.
36/// - `vars`: variables keyed by name; key type `KV` must borrow as `str`.
37///
38/// Type bounds:
39/// - `KI: Eq + Hash + Borrow<str>`
40/// - `KV: Eq + Hash + Borrow<str>`
41///
42/// Returns `Ok(y)` with `y ∈ [0, 1]` on success, or an error if a variable or
43/// input is missing, a term is unknown, or the input is outside the variable domain.
44///
45/// Complexity is linear in the AST size; recursion depth equals AST height.
46pub fn eval_antecedent<KI, KV>(
47    ant: &Antecedent,
48    input: &HashMap<KI, Float>,
49    vars: &HashMap<KV, Variable>,
50) -> Result<Float>
51where
52    KI: Eq + Hash + Borrow<str>,
53    KV: Eq + Hash + Borrow<str>,
54{
55    // Recursive evaluation according to the default Min–Max family.
56    match ant {
57        Antecedent::Atom { var, term } => {
58            let v = vars.get(var.as_str()).ok_or(FuzzyError::NotFound {
59                space: crate::error::MissingSpace::Var,
60                key: var.clone(),
61            })?;
62            let x = *input.get(var.as_str()).ok_or(FuzzyError::NotFound {
63                space: crate::error::MissingSpace::Input,
64                key: term.clone(),
65            })?;
66            v.eval(term.as_str(), x)
67        }
68        Antecedent::And(a, b) => {
69            let a = eval_antecedent(a, input, vars)?;
70            let b = eval_antecedent(b, input, vars)?;
71            Ok(a.min(b))
72        }
73        Antecedent::Or(a, b) => {
74            let a = eval_antecedent(a, input, vars)?;
75            let b = eval_antecedent(b, input, vars)?;
76            Ok(a.max(b))
77        }
78        Antecedent::Not(a) => {
79            let a = eval_antecedent(a, input, vars)?;
80            Ok(1.0 - a)
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use std::collections::HashMap;
88
89    use crate::membership::triangular::Triangular;
90    use crate::prelude::*;
91    use crate::term::Term;
92    use crate::variable::Variable;
93
94    #[test]
95    fn red_antecedent_and_not_behavior() {
96        let eps = crate::Float::EPSILON;
97
98        // Build a variable with two terms: cold and hot.
99        let mut temp = Variable::new(-10.0, 10.0).unwrap();
100        temp.insert_term(
101            "cold",
102            Term::new("cold", Triangular::new(-10.0, -5.0, 0.0).unwrap()),
103        )
104        .unwrap();
105        temp.insert_term(
106            "hot",
107            Term::new("hot", Triangular::new(0.0, 5.0, 10.0).unwrap()),
108        )
109        .unwrap();
110
111        let mut vars: HashMap<&str, Variable> = HashMap::new();
112        vars.insert("temp", temp);
113
114        // Crisp input for temp
115        let mut inputs: HashMap<&str, crate::Float> = HashMap::new();
116        inputs.insert("temp", 7.5);
117
118        // AST: (temp is hot) AND NOT (temp is cold)
119        let ast = crate::antecedent::Antecedent::And(
120            Box::new(crate::antecedent::Antecedent::Atom {
121                var: "temp".into(),
122                term: "hot".into(),
123            }),
124            Box::new(crate::antecedent::Antecedent::Not(Box::new(
125                crate::antecedent::Antecedent::Atom {
126                    var: "temp".into(),
127                    term: "cold".into(),
128                },
129            ))),
130        );
131
132        // Expected with defaults: min(hot(7.5), 1 - cold(7.5))
133        let hot = Triangular::new(0.0, 5.0, 10.0).unwrap().eval(7.5);
134        let cold = Triangular::new(-10.0, -5.0, 0.0).unwrap().eval(7.5);
135        let expected = hot.min(1.0 - cold);
136
137        let y = crate::antecedent::eval_antecedent(&ast, &inputs, &vars).unwrap();
138        assert!((y - expected).abs() < eps);
139    }
140
141    // RED: OR behavior using the same variable at a different crisp value.
142    #[test]
143    fn antecedent_or_behavior() {
144        // Variable setup
145        let mut temp = Variable::new(-10.0, 10.0).unwrap();
146        temp.insert_term(
147            "cold",
148            Term::new("cold", Triangular::new(-10.0, -5.0, 0.0).unwrap()),
149        )
150        .unwrap();
151        temp.insert_term(
152            "hot",
153            Term::new("hot", Triangular::new(0.0, 5.0, 10.0).unwrap()),
154        )
155        .unwrap();
156
157        let mut vars: HashMap<&str, Variable> = HashMap::new();
158        vars.insert("temp", temp);
159
160        let mut inputs: HashMap<&str, crate::Float> = HashMap::new();
161        inputs.insert("temp", -5.0);
162
163        // AST: (temp is cold) OR (temp is hot)
164        let ast = crate::antecedent::Antecedent::Or(
165            Box::new(crate::antecedent::Antecedent::Atom {
166                var: "temp".into(),
167                term: "cold".into(),
168            }),
169            Box::new(crate::antecedent::Antecedent::Atom {
170                var: "temp".into(),
171                term: "hot".into(),
172            }),
173        );
174
175        // Expected with defaults: max(cold(-5), hot(-5)) = 1.0
176        let cold = Triangular::new(-10.0, -5.0, 0.0).unwrap().eval(-5.0);
177        let hot = Triangular::new(0.0, 5.0, 10.0).unwrap().eval(-5.0);
178        let expected = cold.max(hot);
179
180        let y = crate::antecedent::eval_antecedent(&ast, &inputs, &vars).unwrap();
181        assert!((y - expected).abs() < crate::Float::EPSILON);
182    }
183}