fuzzy_control/
lib.rs

1//! A type-safe fuzzy control library for Rust.
2//!
3//! This library provides a complete implementation of fuzzy logic control systems,
4//! with strong type safety guarantees and generic support for different numeric types.
5//!
6//! # Overview
7//!
8//! A fuzzy control system operates in three main stages:
9//! 1. **Fuzzification**: Convert crisp input values into fuzzy membership degrees
10//! 2. **Inference**: Apply fuzzy rules to determine fuzzy outputs
11//! 3. **Defuzzification**: Convert fuzzy outputs back into crisp control values
12
13use num_traits::{Float as NumFloat, Zero, One};
14use std::collections::HashMap;
15use std::fmt;
16
17pub mod membership_functions;
18pub mod operators;
19pub mod defuzzification;
20pub mod visualization;
21
22/// Trait alias for numeric types that can be used in fuzzy computations.
23pub trait Float: NumFloat + Zero + One + PartialOrd + Copy + fmt::Debug {}
24impl<T: NumFloat + Zero + One + PartialOrd + Copy + fmt::Debug> Float for T {}
25
26/// Represents a fuzzy membership degree constrained to the range [0, 1].
27#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
28pub struct MembershipDegree<M: Float>(M);
29
30impl<M: Float> MembershipDegree<M> {
31    /// Creates a new membership degree, returning an error if the value is outside [0, 1].
32    pub fn new(value: M) -> Result<Self, MembershipError> {
33        if value >= M::zero() && value <= M::one() {
34            Ok(MembershipDegree(value))
35        } else {
36            Err(MembershipError::OutOfRange {
37                value: format!("{:?}", value),
38            })
39        }
40    }
41    
42    /// Creates a new membership degree, clamping the value to [0, 1] if necessary.
43    pub fn new_clamped(value: M) -> Self {
44        let clamped = if value < M::zero() {
45            M::zero()
46        } else if value > M::one() {
47            M::one()
48        } else {
49            value
50        };
51        MembershipDegree(clamped)
52    }
53    
54    /// Returns the underlying numeric value of this membership degree.
55    pub fn get(&self) -> M {
56        self.0
57    }
58    
59    /// Creates a membership degree representing complete non-membership (0).
60    pub fn zero() -> Self {
61        MembershipDegree(M::zero())
62    }
63    
64    /// Creates a membership degree representing complete membership (1).
65    pub fn one() -> Self {
66        MembershipDegree(M::one())
67    }
68}
69
70/// Errors that can occur during fuzzy logic operations.
71#[derive(Debug, Clone)]
72pub enum MembershipError {
73    OutOfRange { value: String },
74    InvalidConfiguration { message: String },
75}
76
77impl fmt::Display for MembershipError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            MembershipError::OutOfRange { value } => {
81                write!(f, "Membership degree out of range [0, 1]: {}", value)
82            }
83            MembershipError::InvalidConfiguration { message } => {
84                write!(f, "Invalid configuration: {}", message)
85            }
86        }
87    }
88}
89
90impl std::error::Error for MembershipError {}
91
92/// A membership function maps domain values to fuzzy membership degrees.
93pub trait MembershipFunction<D, M>
94where
95    D: Float,
96    M: Float,
97{
98    /// Evaluates the membership function at a given domain value.
99    fn evaluate(&self, value: D) -> MembershipDegree<M>;
100}
101
102/// A fuzzy set associates a linguistic term with a membership function.
103pub struct FuzzySet<D: Float, M: Float> {
104    name: String,
105    membership_fn: Box<dyn MembershipFunction<D, M>>,
106}
107
108impl<D: Float, M: Float> FuzzySet<D, M> {
109    /// Creates a new fuzzy set with a given name and membership function.
110    pub fn new(name: impl Into<String>, membership_fn: Box<dyn MembershipFunction<D, M>>) -> Self {
111        FuzzySet {
112            name: name.into(),
113            membership_fn,
114        }
115    }
116    
117    /// Returns the name of this fuzzy set.
118    pub fn name(&self) -> &str {
119        &self.name
120    }
121    
122    /// Evaluates the membership degree for a given domain value.
123    pub fn evaluate(&self, value: D) -> MembershipDegree<M> {
124        self.membership_fn.evaluate(value)
125    }
126}
127
128/// A linguistic variable represents a domain concept with associated fuzzy sets.
129pub struct LinguisticVariable<D: Float, M: Float> {
130    name: String,
131    range: (D, D),
132    pub(crate) sets: Vec<FuzzySet<D, M>>,
133}
134
135impl<D: Float, M: Float> LinguisticVariable<D, M> {
136    /// Creates a new linguistic variable with a name and domain range.
137    pub fn new(name: impl Into<String>, range: (D, D)) -> Self {
138        LinguisticVariable {
139            name: name.into(),
140            range,
141            sets: Vec::new(),
142        }
143    }
144    
145    /// Adds a fuzzy set to this linguistic variable.
146    pub fn add_set(&mut self, set: FuzzySet<D, M>) {
147        self.sets.push(set);
148    }
149    
150    /// Returns the name of this linguistic variable.
151    pub fn name(&self) -> &str {
152        &self.name
153    }
154    
155    /// Returns the domain range of this linguistic variable.
156    pub fn range(&self) -> (D, D) {
157        self.range
158    }
159    
160    /// Fuzzifies a crisp domain value into membership degrees for all fuzzy sets.
161    pub fn fuzzify(&self, value: D) -> HashMap<String, MembershipDegree<M>> {
162        self.sets
163            .iter()
164            .map(|set| (set.name().to_string(), set.evaluate(value)))
165            .collect()
166    }
167}
168
169/// T-norm operators implement fuzzy AND operations.
170pub trait TNorm<M: Float> {
171    /// Applies the T-norm to combine two membership degrees.
172    fn apply(&self, a: MembershipDegree<M>, b: MembershipDegree<M>) -> MembershipDegree<M>;
173}
174
175/// S-norm operators implement fuzzy OR operations.
176pub trait SNorm<M: Float> {
177    /// Applies the S-norm to combine two membership degrees.
178    fn apply(&self, a: MembershipDegree<M>, b: MembershipDegree<M>) -> MembershipDegree<M>;
179}
180
181/// Represents a condition in a fuzzy rule antecedent.
182pub struct Condition<D: Float, M: Float> {
183    pub(crate) variable_name: String,
184    pub(crate) set_name: String,
185    _phantom_d: std::marker::PhantomData<D>,
186    _phantom_m: std::marker::PhantomData<M>,
187}
188
189impl<D: Float, M: Float> Condition<D, M> {
190    /// Creates a new condition.
191    pub fn new(variable_name: impl Into<String>, set_name: impl Into<String>) -> Self {
192        Condition {
193            variable_name: variable_name.into(),
194            set_name: set_name.into(),
195            _phantom_d: std::marker::PhantomData,
196            _phantom_m: std::marker::PhantomData,
197        }
198    }
199}
200
201/// Represents a consequent in a fuzzy rule.
202pub struct Consequent<D: Float, M: Float> {
203    pub(crate) variable_name: String,
204    pub(crate) set_name: String,
205    _phantom_d: std::marker::PhantomData<D>,
206    _phantom_m: std::marker::PhantomData<M>,
207}
208
209impl<D: Float, M: Float> Consequent<D, M> {
210    /// Creates a new consequent.
211    pub fn new(variable_name: impl Into<String>, set_name: impl Into<String>) -> Self {
212        Consequent {
213            variable_name: variable_name.into(),
214            set_name: set_name.into(),
215            _phantom_d: std::marker::PhantomData,
216            _phantom_m: std::marker::PhantomData,
217        }
218    }
219}
220
221/// A fuzzy rule expressing IF-THEN logic.
222pub struct FuzzyRule<D: Float, M: Float> {
223    pub(crate) antecedents: Vec<Condition<D, M>>,
224    pub(crate) operator: RuleOperator,
225    pub(crate) consequents: Vec<Consequent<D, M>>,
226    pub(crate) weight: MembershipDegree<M>,
227}
228
229/// Operators for combining rule antecedents.
230#[derive(Debug, Clone, Copy)]
231pub enum RuleOperator {
232    And,
233    Or,
234}
235
236impl<D: Float, M: Float> FuzzyRule<D, M> {
237    /// Creates a new fuzzy rule with default weight of 1.0.
238    pub fn new(
239        antecedents: Vec<Condition<D, M>>,
240        operator: RuleOperator,
241        consequents: Vec<Consequent<D, M>>,
242    ) -> Self {
243        FuzzyRule {
244            antecedents,
245            operator,
246            consequents,
247            weight: MembershipDegree::one(),
248        }
249    }
250    
251    /// Sets the weight (importance) of this rule.
252    pub fn with_weight(mut self, weight: MembershipDegree<M>) -> Self {
253        self.weight = weight;
254        self
255    }
256}
257
258/// The main fuzzy control system.
259pub struct FuzzyController<D: Float = f64, M: Float = f64> {
260    pub(crate) inputs: HashMap<String, LinguisticVariable<D, M>>,
261    pub(crate) outputs: HashMap<String, LinguisticVariable<D, M>>,
262    pub(crate) rules: Vec<FuzzyRule<D, M>>,
263    pub(crate) t_norm: Box<dyn TNorm<M>>,
264    pub(crate) s_norm: Box<dyn SNorm<M>>,
265}
266
267impl<D: Float, M: Float> FuzzyController<D, M> {
268    /// Creates a new builder for constructing a fuzzy controller.
269    pub fn builder() -> FuzzyControllerBuilder<D, M> {
270        FuzzyControllerBuilder::new()
271    }
272    
273    /// Evaluates the fuzzy controller with given crisp inputs using Mamdani inference.
274    pub fn evaluate(
275        &self,
276        inputs: &HashMap<String, D>,
277        defuzzifier: &dyn defuzzification::Defuzzifier<D, M>,
278    ) -> Result<HashMap<String, D>, MembershipError> {
279        // Stage 1: Fuzzification
280        let mut fuzzified_inputs: HashMap<String, HashMap<String, MembershipDegree<M>>> = 
281            HashMap::new();
282        
283        for (var_name, var) in &self.inputs {
284            let crisp_value = inputs.get(var_name).ok_or_else(|| {
285                MembershipError::InvalidConfiguration {
286                    message: format!("Missing input variable: {}", var_name),
287                }
288            })?;
289            
290            fuzzified_inputs.insert(var_name.clone(), var.fuzzify(*crisp_value));
291        }
292        
293        // Stage 2: Rule Evaluation and Aggregation
294        let mut output_activations: HashMap<String, HashMap<String, MembershipDegree<M>>> = 
295            HashMap::new();
296        
297        for (out_name, out_var) in &self.outputs {
298            let mut set_activations = HashMap::new();
299            for set in &out_var.sets {
300                set_activations.insert(set.name().to_string(), MembershipDegree::zero());
301            }
302            output_activations.insert(out_name.clone(), set_activations);
303        }
304        
305        for rule in &self.rules {
306            let firing_strength = self.evaluate_rule_antecedents(rule, &fuzzified_inputs)?;
307            let weighted_strength = MembershipDegree::new_clamped(
308                firing_strength.get() * rule.weight.get()
309            );
310            
311            for consequent in &rule.consequents {
312                let activations = output_activations
313                    .get_mut(&consequent.variable_name)
314                    .ok_or_else(|| {
315                        MembershipError::InvalidConfiguration {
316                            message: format!(
317                                "Unknown output variable in rule: {}",
318                                consequent.variable_name
319                            ),
320                        }
321                    })?;
322                
323                let current_activation = activations
324                    .get(&consequent.set_name)
325                    .copied()
326                    .unwrap_or(MembershipDegree::zero());
327                
328                let new_activation = MembershipDegree::new_clamped(
329                    current_activation.get().max(weighted_strength.get())
330                );
331                
332                activations.insert(consequent.set_name.clone(), new_activation);
333            }
334        }
335        
336        // Stage 3: Defuzzification
337        let mut crisp_outputs = HashMap::new();
338        
339        for (out_name, out_var) in &self.outputs {
340            let activations = &output_activations[out_name];
341            let crisp_value = defuzzifier.defuzzify(out_var, activations)?;
342            crisp_outputs.insert(out_name.clone(), crisp_value);
343        }
344        
345        Ok(crisp_outputs)
346    }
347    
348    /// Evaluates the antecedents of a rule to determine its firing strength.
349    pub(crate) fn evaluate_rule_antecedents(
350        &self,
351        rule: &FuzzyRule<D, M>,
352        fuzzified_inputs: &HashMap<String, HashMap<String, MembershipDegree<M>>>,
353    ) -> Result<MembershipDegree<M>, MembershipError> {
354        if rule.antecedents.is_empty() {
355            return Ok(MembershipDegree::one());
356        }
357        
358        let mut condition_degrees = Vec::new();
359        
360        for condition in &rule.antecedents {
361            let variable_memberships = fuzzified_inputs
362                .get(&condition.variable_name)
363                .ok_or_else(|| {
364                    MembershipError::InvalidConfiguration {
365                        message: format!(
366                            "Unknown variable in rule condition: {}",
367                            condition.variable_name
368                        ),
369                    }
370                })?;
371            
372            let degree = variable_memberships
373                .get(&condition.set_name)
374                .copied()
375                .ok_or_else(|| {
376                    MembershipError::InvalidConfiguration {
377                        message: format!(
378                            "Unknown fuzzy set in rule condition: {}.{}",
379                            condition.variable_name, condition.set_name
380                        ),
381                    }
382                })?;
383            
384            condition_degrees.push(degree);
385        }
386        
387        let result = match rule.operator {
388            RuleOperator::And => {
389                condition_degrees
390                    .into_iter()
391                    .reduce(|acc, degree| self.t_norm.apply(acc, degree))
392                    .unwrap()
393            }
394            RuleOperator::Or => {
395                condition_degrees
396                    .into_iter()
397                    .reduce(|acc, degree| self.s_norm.apply(acc, degree))
398                    .unwrap()
399            }
400        };
401        
402        Ok(result)
403    }
404    
405    /// Convenience method to evaluate with default centroid defuzzification.
406    pub fn evaluate_centroid(
407        &self,
408        inputs: &HashMap<String, D>,
409    ) -> Result<HashMap<String, D>, MembershipError> {
410        let defuzzifier = defuzzification::Centroid::new(200)?;
411        self.evaluate(inputs, &defuzzifier)
412    }
413}
414
415/// Builder for fuzzy controller
416pub struct FuzzyControllerBuilder<D: Float, M: Float> {
417    inputs: HashMap<String, LinguisticVariable<D, M>>,
418    outputs: HashMap<String, LinguisticVariable<D, M>>,
419    rules: Vec<FuzzyRule<D, M>>,
420    t_norm: Option<Box<dyn TNorm<M>>>,
421    s_norm: Option<Box<dyn SNorm<M>>>,
422}
423
424impl<D: Float, M: Float> FuzzyControllerBuilder<D, M> {
425    /// Creates a new fuzzy controller builder.
426    pub fn new() -> Self {
427        FuzzyControllerBuilder {
428            inputs: HashMap::new(),
429            outputs: HashMap::new(),
430            rules: Vec::new(),
431            t_norm: None,
432            s_norm: None,
433        }
434    }
435    
436    /// Adds an input linguistic variable to the controller.
437    pub fn add_input(mut self, variable: LinguisticVariable<D, M>) -> Self {
438        self.inputs.insert(variable.name().to_string(), variable);
439        self
440    }
441    
442    /// Adds an output linguistic variable to the controller.
443    pub fn add_output(mut self, variable: LinguisticVariable<D, M>) -> Self {
444        self.outputs.insert(variable.name().to_string(), variable);
445        self
446    }
447    
448    /// Adds a fuzzy rule to the controller.
449    pub fn add_rule(mut self, rule: FuzzyRule<D, M>) -> Self {
450        self.rules.push(rule);
451        self
452    }
453    
454    /// Sets the T-norm operator for combining rule antecedents with AND.
455    pub fn with_t_norm(mut self, t_norm: Box<dyn TNorm<M>>) -> Self {
456        self.t_norm = Some(t_norm);
457        self
458    }
459    
460    /// Sets the S-norm operator for combining rule antecedents with OR.
461    pub fn with_s_norm(mut self, s_norm: Box<dyn SNorm<M>>) -> Self {
462        self.s_norm = Some(s_norm);
463        self
464    }
465    
466    /// Adds multiple input variables at once.
467    pub fn add_inputs(mut self, variables: Vec<LinguisticVariable<D, M>>) -> Self {
468        for variable in variables {
469            self.inputs.insert(variable.name().to_string(), variable);
470        }
471        self
472    }
473    
474    /// Adds multiple output variables at once.
475    pub fn add_outputs(mut self, variables: Vec<LinguisticVariable<D, M>>) -> Self {
476        for variable in variables {
477            self.outputs.insert(variable.name().to_string(), variable);
478        }
479        self
480    }
481    
482    /// Adds multiple rules at once.
483    pub fn add_rules(mut self, rules: Vec<FuzzyRule<D, M>>) -> Self {
484        self.rules.extend(rules);
485        self
486    }
487    
488    /// Builds the fuzzy controller with the configured settings.
489    pub fn build(self) -> FuzzyController<D, M> {
490        use operators::{MinTNorm, MaxSNorm};
491        
492        FuzzyController {
493            inputs: self.inputs,
494            outputs: self.outputs,
495            rules: self.rules,
496            t_norm: self.t_norm.unwrap_or_else(|| Box::new(MinTNorm)),
497            s_norm: self.s_norm.unwrap_or_else(|| Box::new(MaxSNorm)),
498        }
499    }
500}
501
502impl<D: Float, M: Float> Default for FuzzyControllerBuilder<D, M> {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use super::*;
511    use crate::membership_functions::*;
512    use crate::operators::*;
513    use crate::defuzzification::*;
514    
515    fn approx_eq<T: Float>(a: T, b: T, epsilon: T) -> bool {
516        (a - b).abs() < epsilon
517    }
518    
519    #[test]
520    fn test_membership_degree_new_valid() {
521        let degree = MembershipDegree::new(0.5);
522        assert!(degree.is_ok());
523        assert_eq!(degree.unwrap().get(), 0.5);
524    }
525    
526    #[test]
527    fn test_membership_degree_new_invalid() {
528        assert!(MembershipDegree::new(1.5).is_err());
529        assert!(MembershipDegree::new(-0.5).is_err());
530    }
531    
532    #[test]
533    fn test_membership_degree_clamped() {
534        assert_eq!(MembershipDegree::new_clamped(1.5).get(), 1.0);
535        assert_eq!(MembershipDegree::new_clamped(-0.3).get(), 0.0);
536    }
537    
538    #[test]
539    fn test_triangular_membership() {
540        let tri = Triangular::<f64, f64>::new(0.0, 50.0, 100.0).unwrap();
541        assert_eq!(tri.evaluate(0.0).get(), 0.0);
542        assert_eq!(tri.evaluate(50.0).get(), 1.0);
543        assert_eq!(tri.evaluate(100.0).get(), 0.0);
544        assert!(approx_eq(tri.evaluate(25.0).get(), 0.5, 0.01));
545    }
546    
547    #[test]
548    fn test_simple_controller() -> Result<(), MembershipError> {
549        let mut temp = LinguisticVariable::new("temperature", (0.0, 100.0));
550        temp.add_set(FuzzySet::new(
551            "cold",
552            Box::new(Triangular::new(0.0, 0.0, 50.0)?)
553        ));
554        temp.add_set(FuzzySet::new(
555            "hot",
556            Box::new(Triangular::new(50.0, 100.0, 100.0)?)
557        ));
558        
559        let mut fan = LinguisticVariable::new("fan_speed", (0.0, 100.0));
560        fan.add_set(FuzzySet::new(
561            "low",
562            Box::new(Triangular::new(0.0, 0.0, 50.0)?)
563        ));
564        fan.add_set(FuzzySet::new(
565            "high",
566            Box::new(Triangular::new(50.0, 100.0, 100.0)?)
567        ));
568        
569        let rules = vec![
570            FuzzyRule::new(
571                vec![Condition::new("temperature", "cold")],
572                RuleOperator::And,
573                vec![Consequent::new("fan_speed", "low")],
574            ),
575            FuzzyRule::new(
576                vec![Condition::new("temperature", "hot")],
577                RuleOperator::And,
578                vec![Consequent::new("fan_speed", "high")],
579            ),
580        ];
581        
582        let controller = FuzzyController::builder()
583            .add_input(temp)
584            .add_output(fan)
585            .add_rules(rules)
586            .build();
587        
588        let inputs = HashMap::from([("temperature".to_string(), 15.0)]);
589        let outputs = controller.evaluate_centroid(&inputs)?;
590        assert!(outputs["fan_speed"] < 50.0);
591        
592        let inputs = HashMap::from([("temperature".to_string(), 85.0)]);
593        let outputs = controller.evaluate_centroid(&inputs)?;
594        assert!(outputs["fan_speed"] > 50.0);
595        
596        Ok(())
597    }
598}