1use std::collections::HashMap;
7use thiserror::Error;
8
9#[derive(Debug, Clone, Error)]
15pub enum FuzzyError {
16 #[error("fuzzy variable not found: {0}")]
18 VariableNotFound(String),
19
20 #[error("fuzzy set '{set}' not found in variable '{variable}'")]
22 SetNotFound { variable: String, set: String },
23
24 #[error("duplicate fuzzy variable: {0}")]
26 DuplicateVariable(String),
27
28 #[error("no fuzzy rules target output variable '{0}'")]
30 NoRulesForOutput(String),
31
32 #[error("numerical error in fuzzy inference: {0}")]
35 NumericalError(String),
36}
37
38#[derive(Debug, Clone, PartialEq)]
44pub enum MembershipFunction {
45 Triangular { a: f64, b: f64, c: f64 },
49
50 Trapezoidal { a: f64, b: f64, c: f64, d: f64 },
54
55 Gaussian { mean: f64, sigma: f64 },
59
60 Singleton { value: f64 },
62
63 Universe,
65}
66
67impl MembershipFunction {
68 #[must_use]
70 pub fn evaluate(&self, x: f64) -> f64 {
71 match self {
72 MembershipFunction::Triangular { a, b, c } => {
73 let left = if (b - a).abs() < f64::EPSILON {
74 if (x - a).abs() < f64::EPSILON {
75 1.0
76 } else {
77 0.0
78 }
79 } else {
80 (x - a) / (b - a)
81 };
82 let right = if (c - b).abs() < f64::EPSILON {
83 if (x - b).abs() < f64::EPSILON {
84 1.0
85 } else {
86 0.0
87 }
88 } else {
89 (c - x) / (c - b)
90 };
91 left.min(right).max(0.0)
92 }
93 MembershipFunction::Trapezoidal { a, b, c, d } => {
94 let left = if (b - a).abs() < f64::EPSILON {
95 if x >= *a {
96 1.0
97 } else {
98 0.0
99 }
100 } else {
101 (x - a) / (b - a)
102 };
103 let right = if (d - c).abs() < f64::EPSILON {
104 if x <= *d {
105 1.0
106 } else {
107 0.0
108 }
109 } else {
110 (d - x) / (d - c)
111 };
112 left.min(1.0_f64).min(right).max(0.0)
113 }
114 MembershipFunction::Gaussian { mean, sigma } => {
115 if sigma.abs() < f64::EPSILON {
116 if (x - mean).abs() < f64::EPSILON {
117 1.0
118 } else {
119 0.0
120 }
121 } else {
122 let z = (x - mean) / sigma;
123 (-0.5 * z * z).exp()
124 }
125 }
126 MembershipFunction::Singleton { value } => {
127 if (x - value).abs() < f64::EPSILON {
128 1.0
129 } else {
130 0.0
131 }
132 }
133 MembershipFunction::Universe => 1.0,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
144pub struct FuzzySet {
145 pub name: String,
147 pub mf: MembershipFunction,
149}
150
151impl FuzzySet {
152 #[must_use]
154 pub fn new(name: impl Into<String>, mf: MembershipFunction) -> Self {
155 Self {
156 name: name.into(),
157 mf,
158 }
159 }
160
161 #[must_use]
163 pub fn degree(&self, x: f64) -> f64 {
164 self.mf.evaluate(x)
165 }
166}
167
168#[derive(Debug, Clone)]
174pub struct FuzzyVariable {
175 pub name: String,
177 pub sets: Vec<FuzzySet>,
179 pub min_val: f64,
181 pub max_val: f64,
183}
184
185impl FuzzyVariable {
186 #[must_use]
188 pub fn new(name: impl Into<String>, min_val: f64, max_val: f64) -> Self {
189 Self {
190 name: name.into(),
191 sets: Vec::new(),
192 min_val,
193 max_val,
194 }
195 }
196
197 pub fn add_set(&mut self, set: FuzzySet) {
199 self.sets.push(set);
200 }
201
202 #[must_use]
204 pub fn get_set(&self, name: &str) -> Option<&FuzzySet> {
205 self.sets.iter().find(|s| s.name == name)
206 }
207}
208
209#[derive(Debug, Clone)]
215pub struct FuzzyProposition {
216 pub variable: String,
218 pub set: String,
220}
221
222impl FuzzyProposition {
223 #[must_use]
225 pub fn new(variable: impl Into<String>, set: impl Into<String>) -> Self {
226 Self {
227 variable: variable.into(),
228 set: set.into(),
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
243pub struct FuzzyRule {
244 pub antecedents: Vec<FuzzyProposition>,
246 pub consequent: FuzzyProposition,
248 pub weight: f64,
250}
251
252impl FuzzyRule {
253 #[must_use]
255 pub fn new(
256 antecedents: Vec<FuzzyProposition>,
257 consequent: FuzzyProposition,
258 weight: f64,
259 ) -> Self {
260 Self {
261 antecedents,
262 consequent,
263 weight,
264 }
265 }
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
274pub enum InferenceMethod {
275 Mamdani,
277 Sugeno,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
283pub enum DefuzzMethod {
284 Centroid,
286 MeanOfMax,
288 LargestOfMax,
290}
291
292#[derive(Debug, Clone)]
298pub struct FuzzyStats {
299 pub variable_count: usize,
301 pub rule_count: usize,
303 pub inference: String,
305 pub defuzz: String,
307}
308
309const CENTROID_STEPS: usize = 100;
315
316pub struct FuzzyLogicEngine {
326 variables: HashMap<String, FuzzyVariable>,
327 rules: Vec<FuzzyRule>,
328 inference: InferenceMethod,
329 defuzz: DefuzzMethod,
330}
331
332impl FuzzyLogicEngine {
333 #[must_use]
339 pub fn new(inference: InferenceMethod, defuzz: DefuzzMethod) -> Self {
340 Self {
341 variables: HashMap::new(),
342 rules: Vec::new(),
343 inference,
344 defuzz,
345 }
346 }
347
348 pub fn add_variable(&mut self, var: FuzzyVariable) -> Result<(), FuzzyError> {
358 if self.variables.contains_key(&var.name) {
359 return Err(FuzzyError::DuplicateVariable(var.name.clone()));
360 }
361 self.variables.insert(var.name.clone(), var);
362 Ok(())
363 }
364
365 pub fn add_rule(&mut self, rule: FuzzyRule) -> Result<(), FuzzyError> {
373 for prop in &rule.antecedents {
375 self.validate_proposition(prop)?;
376 }
377 self.validate_proposition(&rule.consequent)?;
379 self.rules.push(rule);
380 Ok(())
381 }
382
383 #[must_use]
389 pub fn rule_count(&self) -> usize {
390 self.rules.len()
391 }
392
393 #[must_use]
395 pub fn variable_names(&self) -> Vec<&str> {
396 self.variables.keys().map(String::as_str).collect()
397 }
398
399 pub fn membership_at(&self, variable: &str, set: &str, x: f64) -> Result<f64, FuzzyError> {
404 let var = self.get_variable(variable)?;
405 let fuzzy_set = var.get_set(set).ok_or_else(|| FuzzyError::SetNotFound {
406 variable: variable.to_string(),
407 set: set.to_string(),
408 })?;
409 Ok(fuzzy_set.degree(x))
410 }
411
412 pub fn fuzzify(&self, variable: &str, crisp: f64) -> Result<Vec<(String, f64)>, FuzzyError> {
420 let var = self.get_variable(variable)?;
421 Ok(var
422 .sets
423 .iter()
424 .map(|s| (s.name.clone(), s.degree(crisp)))
425 .collect())
426 }
427
428 pub fn fire_rule(
435 &self,
436 rule: &FuzzyRule,
437 inputs: &HashMap<String, f64>,
438 ) -> Result<f64, FuzzyError> {
439 let mut activation: f64 = 1.0;
440 for prop in &rule.antecedents {
441 let crisp = inputs.get(&prop.variable).copied().unwrap_or(0.0);
442 let degree = self.membership_at(&prop.variable, &prop.set, crisp)?;
443 activation = activation.min(degree);
444 }
445 Ok(activation * rule.weight)
446 }
447
448 pub fn infer(
457 &self,
458 inputs: &HashMap<String, f64>,
459 output_var: &str,
460 ) -> Result<f64, FuzzyError> {
461 let relevant: Vec<(&FuzzyRule, f64)> = self
463 .rules
464 .iter()
465 .filter(|r| r.consequent.variable == output_var)
466 .map(|r| {
467 let alpha = self.fire_rule(r, inputs)?;
468 Ok((r, alpha))
469 })
470 .collect::<Result<Vec<_>, FuzzyError>>()?;
471
472 if relevant.is_empty() {
473 return Err(FuzzyError::NoRulesForOutput(output_var.to_string()));
474 }
475
476 match self.inference {
477 InferenceMethod::Mamdani => self.infer_mamdani(output_var, &relevant),
478 InferenceMethod::Sugeno => self.infer_sugeno(output_var, &relevant),
479 }
480 }
481
482 pub fn evaluate(
488 &mut self,
489 inputs: &HashMap<String, f64>,
490 output_var: &str,
491 ) -> Result<f64, FuzzyError> {
492 self.infer(inputs, output_var)
493 }
494
495 #[must_use]
497 pub fn stats(&self) -> FuzzyStats {
498 FuzzyStats {
499 variable_count: self.variables.len(),
500 rule_count: self.rules.len(),
501 inference: format!("{:?}", self.inference),
502 defuzz: format!("{:?}", self.defuzz),
503 }
504 }
505
506 fn get_variable(&self, name: &str) -> Result<&FuzzyVariable, FuzzyError> {
511 self.variables
512 .get(name)
513 .ok_or_else(|| FuzzyError::VariableNotFound(name.to_string()))
514 }
515
516 fn validate_proposition(&self, prop: &FuzzyProposition) -> Result<(), FuzzyError> {
517 let var = self.get_variable(&prop.variable)?;
518 if var.get_set(&prop.set).is_none() {
519 return Err(FuzzyError::SetNotFound {
520 variable: prop.variable.clone(),
521 set: prop.set.clone(),
522 });
523 }
524 Ok(())
525 }
526
527 fn infer_mamdani(
530 &self,
531 output_var: &str,
532 relevant: &[(&FuzzyRule, f64)],
533 ) -> Result<f64, FuzzyError> {
534 let out_var = self.get_variable(output_var)?;
535 let steps = CENTROID_STEPS;
536 let range = out_var.max_val - out_var.min_val;
537 let step_size = range / steps as f64;
538
539 let x_values: Vec<f64> = (0..=steps)
542 .map(|i| out_var.min_val + i as f64 * step_size)
543 .collect();
544
545 let mut aggregate: Vec<f64> = vec![0.0; x_values.len()];
546 for (rule, alpha) in relevant {
547 let set =
548 out_var
549 .get_set(&rule.consequent.set)
550 .ok_or_else(|| FuzzyError::SetNotFound {
551 variable: output_var.to_string(),
552 set: rule.consequent.set.clone(),
553 })?;
554 for (i, &x) in x_values.iter().enumerate() {
555 let mu = set.degree(x).min(*alpha);
556 if mu > aggregate[i] {
557 aggregate[i] = mu;
558 }
559 }
560 }
561
562 self.defuzzify(&x_values, &aggregate)
563 }
564
565 fn infer_sugeno(
567 &self,
568 output_var: &str,
569 relevant: &[(&FuzzyRule, f64)],
570 ) -> Result<f64, FuzzyError> {
571 let out_var = self.get_variable(output_var)?;
572 let steps = CENTROID_STEPS;
573 let range = out_var.max_val - out_var.min_val;
574 let step_size = range / steps as f64;
575
576 let x_values: Vec<f64> = (0..=steps)
577 .map(|i| out_var.min_val + i as f64 * step_size)
578 .collect();
579
580 let mut weighted_sum: f64 = 0.0;
581 let mut weight_total: f64 = 0.0;
582
583 for (rule, alpha) in relevant {
584 if *alpha <= 0.0 {
585 continue;
586 }
587 let set =
588 out_var
589 .get_set(&rule.consequent.set)
590 .ok_or_else(|| FuzzyError::SetNotFound {
591 variable: output_var.to_string(),
592 set: rule.consequent.set.clone(),
593 })?;
594 let centroid = centroid_of_set(set, &x_values);
596 weighted_sum += alpha * centroid;
597 weight_total += alpha;
598 }
599
600 if weight_total < f64::EPSILON {
601 return Err(FuzzyError::NumericalError(
602 "Sugeno total activation weight is zero".to_string(),
603 ));
604 }
605 Ok(weighted_sum / weight_total)
606 }
607
608 fn defuzzify(&self, x: &[f64], mu: &[f64]) -> Result<f64, FuzzyError> {
610 match self.defuzz {
611 DefuzzMethod::Centroid => defuzz_centroid(x, mu),
612 DefuzzMethod::MeanOfMax => defuzz_mean_of_max(x, mu),
613 DefuzzMethod::LargestOfMax => defuzz_largest_of_max(x, mu),
614 }
615 }
616}
617
618fn defuzz_centroid(x: &[f64], mu: &[f64]) -> Result<f64, FuzzyError> {
624 let sum_xmu: f64 = x.iter().zip(mu.iter()).map(|(xi, mi)| xi * mi).sum();
625 let sum_mu: f64 = mu.iter().sum();
626 if sum_mu < f64::EPSILON {
627 return Err(FuzzyError::NumericalError(
628 "centroid defuzzification: aggregate membership is all-zero".to_string(),
629 ));
630 }
631 Ok(sum_xmu / sum_mu)
632}
633
634fn defuzz_mean_of_max(x: &[f64], mu: &[f64]) -> Result<f64, FuzzyError> {
636 let max_mu = mu.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
637 if max_mu < f64::EPSILON {
638 return Err(FuzzyError::NumericalError(
639 "mean-of-max defuzzification: aggregate membership is all-zero".to_string(),
640 ));
641 }
642 let max_x: Vec<f64> = x
643 .iter()
644 .zip(mu.iter())
645 .filter(|(_, &m)| (m - max_mu).abs() < 1e-10)
646 .map(|(&xi, _)| xi)
647 .collect();
648 if max_x.is_empty() {
649 return Err(FuzzyError::NumericalError(
650 "mean-of-max defuzzification: no maximum found".to_string(),
651 ));
652 }
653 Ok(max_x.iter().sum::<f64>() / max_x.len() as f64)
654}
655
656fn defuzz_largest_of_max(x: &[f64], mu: &[f64]) -> Result<f64, FuzzyError> {
658 let max_mu = mu.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
659 if max_mu < f64::EPSILON {
660 return Err(FuzzyError::NumericalError(
661 "largest-of-max defuzzification: aggregate membership is all-zero".to_string(),
662 ));
663 }
664 let largest_x = x
665 .iter()
666 .zip(mu.iter())
667 .filter(|(_, &m)| (m - max_mu).abs() < 1e-10)
668 .map(|(&xi, _)| xi)
669 .fold(f64::NEG_INFINITY, f64::max);
670 if largest_x == f64::NEG_INFINITY {
671 return Err(FuzzyError::NumericalError(
672 "largest-of-max defuzzification: no maximum found".to_string(),
673 ));
674 }
675 Ok(largest_x)
676}
677
678fn centroid_of_set(set: &FuzzySet, x_values: &[f64]) -> f64 {
680 let sum_xmu: f64 = x_values.iter().map(|&x| x * set.degree(x)).sum();
681 let sum_mu: f64 = x_values.iter().map(|&x| set.degree(x)).sum();
682 if sum_mu < f64::EPSILON {
683 let first = x_values.first().copied().unwrap_or(0.0);
685 let last = x_values.last().copied().unwrap_or(0.0);
686 (first + last) / 2.0
687 } else {
688 sum_xmu / sum_mu
689 }
690}
691
692#[cfg(test)]
697mod tests {
698 use crate::fuzzy_logic::{
699 DefuzzMethod, FuzzyError, FuzzyLogicEngine, FuzzyProposition, FuzzyRule, FuzzySet,
700 FuzzyVariable, InferenceMethod, MembershipFunction,
701 };
702 use std::collections::HashMap;
703
704 #[test]
709 fn triangular_mf_peak() {
710 let mf = MembershipFunction::Triangular {
711 a: 0.0,
712 b: 5.0,
713 c: 10.0,
714 };
715 assert!((mf.evaluate(5.0) - 1.0).abs() < 1e-10);
716 }
717
718 #[test]
719 fn triangular_mf_left_edge() {
720 let mf = MembershipFunction::Triangular {
721 a: 0.0,
722 b: 5.0,
723 c: 10.0,
724 };
725 assert!((mf.evaluate(0.0)).abs() < 1e-10);
726 }
727
728 #[test]
729 fn triangular_mf_right_edge() {
730 let mf = MembershipFunction::Triangular {
731 a: 0.0,
732 b: 5.0,
733 c: 10.0,
734 };
735 assert!((mf.evaluate(10.0)).abs() < 1e-10);
736 }
737
738 #[test]
739 fn triangular_mf_outside_range() {
740 let mf = MembershipFunction::Triangular {
741 a: 0.0,
742 b: 5.0,
743 c: 10.0,
744 };
745 assert_eq!(mf.evaluate(-1.0), 0.0);
746 assert_eq!(mf.evaluate(11.0), 0.0);
747 }
748
749 #[test]
750 fn triangular_mf_midpoint_left() {
751 let mf = MembershipFunction::Triangular {
752 a: 0.0,
753 b: 4.0,
754 c: 8.0,
755 };
756 assert!((mf.evaluate(2.0) - 0.5).abs() < 1e-10);
758 }
759
760 #[test]
761 fn trapezoidal_mf_flat_top() {
762 let mf = MembershipFunction::Trapezoidal {
763 a: 0.0,
764 b: 2.0,
765 c: 8.0,
766 d: 10.0,
767 };
768 assert!((mf.evaluate(5.0) - 1.0).abs() < 1e-10);
770 }
771
772 #[test]
773 fn trapezoidal_mf_rising_slope() {
774 let mf = MembershipFunction::Trapezoidal {
775 a: 0.0,
776 b: 4.0,
777 c: 8.0,
778 d: 10.0,
779 };
780 assert!((mf.evaluate(2.0) - 0.5).abs() < 1e-10);
782 }
783
784 #[test]
785 fn trapezoidal_mf_falling_slope() {
786 let mf = MembershipFunction::Trapezoidal {
787 a: 0.0,
788 b: 2.0,
789 c: 8.0,
790 d: 10.0,
791 };
792 assert!((mf.evaluate(9.0) - 0.5).abs() < 1e-10);
794 }
795
796 #[test]
797 fn trapezoidal_mf_outside() {
798 let mf = MembershipFunction::Trapezoidal {
799 a: 2.0,
800 b: 4.0,
801 c: 6.0,
802 d: 8.0,
803 };
804 assert_eq!(mf.evaluate(1.0), 0.0);
805 assert_eq!(mf.evaluate(9.0), 0.0);
806 }
807
808 #[test]
809 fn gaussian_mf_at_mean() {
810 let mf = MembershipFunction::Gaussian {
811 mean: 5.0,
812 sigma: 1.0,
813 };
814 assert!((mf.evaluate(5.0) - 1.0).abs() < 1e-10);
815 }
816
817 #[test]
818 fn gaussian_mf_sigma_symmetry() {
819 let mf = MembershipFunction::Gaussian {
820 mean: 0.0,
821 sigma: 2.0,
822 };
823 let left = mf.evaluate(-1.0);
825 let right = mf.evaluate(1.0);
826 assert!((left - right).abs() < 1e-10);
827 }
828
829 #[test]
830 fn gaussian_mf_decay() {
831 let mf = MembershipFunction::Gaussian {
832 mean: 0.0,
833 sigma: 1.0,
834 };
835 assert!((mf.evaluate(1.0) - std::f64::consts::E.powf(-0.5)).abs() < 1e-10);
837 }
838
839 #[test]
840 fn singleton_mf_exact() {
841 let mf = MembershipFunction::Singleton { value: 3.0 };
842 assert!((mf.evaluate(3.0) - 1.0).abs() < 1e-10);
843 assert_eq!(mf.evaluate(3.1), 0.0);
844 }
845
846 #[test]
847 fn universe_mf_always_one() {
848 let mf = MembershipFunction::Universe;
849 assert!((mf.evaluate(-100.0) - 1.0).abs() < 1e-10);
850 assert!((mf.evaluate(0.0) - 1.0).abs() < 1e-10);
851 assert!((mf.evaluate(100.0) - 1.0).abs() < 1e-10);
852 }
853
854 #[test]
859 fn fuzzy_set_degree_delegates_to_mf() {
860 let set = FuzzySet::new(
861 "hot",
862 MembershipFunction::Triangular {
863 a: 5.0,
864 b: 10.0,
865 c: 15.0,
866 },
867 );
868 assert!((set.degree(10.0) - 1.0).abs() < 1e-10);
869 assert_eq!(set.degree(5.0), 0.0);
870 }
871
872 #[test]
877 fn fuzzy_variable_add_and_get_set() {
878 let mut var = FuzzyVariable::new("temp", 0.0, 100.0);
879 var.add_set(FuzzySet::new(
880 "low",
881 MembershipFunction::Triangular {
882 a: 0.0,
883 b: 0.0,
884 c: 50.0,
885 },
886 ));
887 var.add_set(FuzzySet::new(
888 "high",
889 MembershipFunction::Triangular {
890 a: 50.0,
891 b: 100.0,
892 c: 100.0,
893 },
894 ));
895 assert!(var.get_set("low").is_some());
896 assert!(var.get_set("medium").is_none());
897 }
898
899 fn build_temperature_engine() -> FuzzyLogicEngine {
904 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
905
906 let mut temp = FuzzyVariable::new("temperature", 0.0, 100.0);
909 temp.add_set(FuzzySet::new(
910 "cold",
911 MembershipFunction::Trapezoidal {
912 a: 0.0,
913 b: 0.0,
914 c: 20.0,
915 d: 40.0,
916 },
917 ));
918 temp.add_set(FuzzySet::new(
919 "warm",
920 MembershipFunction::Triangular {
921 a: 30.0,
922 b: 50.0,
923 c: 70.0,
924 },
925 ));
926 temp.add_set(FuzzySet::new(
927 "hot",
928 MembershipFunction::Trapezoidal {
929 a: 60.0,
930 b: 80.0,
931 c: 100.0,
932 d: 100.0,
933 },
934 ));
935 engine.add_variable(temp).expect("add temperature");
936
937 let mut fan = FuzzyVariable::new("fan_speed", 0.0, 100.0);
939 fan.add_set(FuzzySet::new(
940 "slow",
941 MembershipFunction::Trapezoidal {
942 a: 0.0,
943 b: 0.0,
944 c: 20.0,
945 d: 40.0,
946 },
947 ));
948 fan.add_set(FuzzySet::new(
949 "medium",
950 MembershipFunction::Triangular {
951 a: 30.0,
952 b: 50.0,
953 c: 70.0,
954 },
955 ));
956 fan.add_set(FuzzySet::new(
957 "fast",
958 MembershipFunction::Trapezoidal {
959 a: 60.0,
960 b: 80.0,
961 c: 100.0,
962 d: 100.0,
963 },
964 ));
965 engine.add_variable(fan).expect("add fan_speed");
966
967 engine
969 .add_rule(FuzzyRule::new(
970 vec![FuzzyProposition::new("temperature", "cold")],
971 FuzzyProposition::new("fan_speed", "slow"),
972 1.0,
973 ))
974 .expect("add rule cold->slow");
975 engine
976 .add_rule(FuzzyRule::new(
977 vec![FuzzyProposition::new("temperature", "warm")],
978 FuzzyProposition::new("fan_speed", "medium"),
979 1.0,
980 ))
981 .expect("add rule warm->medium");
982 engine
983 .add_rule(FuzzyRule::new(
984 vec![FuzzyProposition::new("temperature", "hot")],
985 FuzzyProposition::new("fan_speed", "fast"),
986 1.0,
987 ))
988 .expect("add rule hot->fast");
989
990 engine
991 }
992
993 #[test]
994 fn engine_duplicate_variable_error() {
995 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
996 let var = FuzzyVariable::new("x", 0.0, 1.0);
997 engine.add_variable(var.clone()).expect("first add");
998 let err = engine.add_variable(var);
999 assert!(matches!(err, Err(FuzzyError::DuplicateVariable(_))));
1000 }
1001
1002 #[test]
1003 fn engine_add_rule_missing_variable_error() {
1004 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
1005 let rule = FuzzyRule::new(
1006 vec![FuzzyProposition::new("nonexistent", "set")],
1007 FuzzyProposition::new("out", "s"),
1008 1.0,
1009 );
1010 let err = engine.add_rule(rule);
1011 assert!(matches!(err, Err(FuzzyError::VariableNotFound(_))));
1012 }
1013
1014 #[test]
1015 fn engine_add_rule_missing_set_error() {
1016 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
1017 let mut var = FuzzyVariable::new("x", 0.0, 10.0);
1018 var.add_set(FuzzySet::new("low", MembershipFunction::Universe));
1019 engine.add_variable(var).expect("add var");
1020 let rule = FuzzyRule::new(
1022 vec![FuzzyProposition::new("x", "nonexistent_set")],
1023 FuzzyProposition::new("x", "low"),
1024 1.0,
1025 );
1026 let err = engine.add_rule(rule);
1027 assert!(matches!(err, Err(FuzzyError::SetNotFound { .. })));
1028 }
1029
1030 #[test]
1031 fn engine_rule_count() {
1032 let engine = build_temperature_engine();
1033 assert_eq!(engine.rule_count(), 3);
1034 }
1035
1036 #[test]
1037 fn engine_variable_names_count() {
1038 let engine = build_temperature_engine();
1039 assert_eq!(engine.variable_names().len(), 2);
1040 }
1041
1042 #[test]
1047 fn fuzzify_returns_all_sets() {
1048 let engine = build_temperature_engine();
1049 let result = engine.fuzzify("temperature", 50.0).expect("fuzzify");
1050 assert_eq!(result.len(), 3);
1051 }
1052
1053 #[test]
1054 fn fuzzify_unknown_variable() {
1055 let engine = build_temperature_engine();
1056 let err = engine.fuzzify("humidity", 50.0);
1057 assert!(matches!(err, Err(FuzzyError::VariableNotFound(_))));
1058 }
1059
1060 #[test]
1065 fn fire_rule_full_activation() {
1066 let engine = build_temperature_engine();
1067 let mut inputs = HashMap::new();
1068 inputs.insert("temperature".to_string(), 0.0_f64); let rule = &engine.rules[0]; let alpha = engine.fire_rule(rule, &inputs).expect("fire rule");
1071 assert!((alpha - 1.0).abs() < 1e-10);
1072 }
1073
1074 #[test]
1075 fn fire_rule_zero_activation() {
1076 let engine = build_temperature_engine();
1077 let mut inputs = HashMap::new();
1078 inputs.insert("temperature".to_string(), 100.0_f64); let rule = &engine.rules[0]; let alpha = engine.fire_rule(rule, &inputs).expect("fire rule");
1081 assert!(alpha < 1e-10);
1082 }
1083
1084 #[test]
1085 fn fire_rule_weight_applied() {
1086 let engine = build_temperature_engine();
1087 let mut inputs = HashMap::new();
1089 inputs.insert("temperature".to_string(), 0.0_f64);
1090 let rule = FuzzyRule::new(
1091 vec![FuzzyProposition::new("temperature", "cold")],
1092 FuzzyProposition::new("fan_speed", "slow"),
1093 0.5,
1094 );
1095 let alpha = engine.fire_rule(&rule, &inputs).expect("fire rule");
1096 assert!((alpha - 0.5).abs() < 1e-10);
1097 }
1098
1099 #[test]
1104 fn mamdani_cold_input_gives_low_speed() {
1105 let engine = build_temperature_engine();
1106 let mut inputs = HashMap::new();
1107 inputs.insert("temperature".to_string(), 5.0_f64);
1108 let speed = engine.infer(&inputs, "fan_speed").expect("infer");
1109 assert!(speed < 40.0, "expected slow speed, got {speed}");
1111 }
1112
1113 #[test]
1114 fn mamdani_hot_input_gives_high_speed() {
1115 let engine = build_temperature_engine();
1116 let mut inputs = HashMap::new();
1117 inputs.insert("temperature".to_string(), 95.0_f64);
1118 let speed = engine.infer(&inputs, "fan_speed").expect("infer");
1119 assert!(speed > 60.0, "expected fast speed, got {speed}");
1121 }
1122
1123 #[test]
1124 fn mamdani_no_rules_for_output_error() {
1125 let engine = build_temperature_engine();
1126 let inputs = HashMap::new();
1127 let err = engine.infer(&inputs, "temperature");
1128 assert!(matches!(err, Err(FuzzyError::NoRulesForOutput(_))));
1129 }
1130
1131 #[test]
1136 fn sugeno_cold_input_gives_low_speed() {
1137 let mut engine = build_temperature_engine();
1138 engine.inference = InferenceMethod::Sugeno;
1140 let mut inputs = HashMap::new();
1141 inputs.insert("temperature".to_string(), 5.0_f64);
1142 let speed = engine.infer(&inputs, "fan_speed").expect("infer sugeno");
1143 assert!(speed < 40.0, "expected slow speed, got {speed}");
1144 }
1145
1146 #[test]
1147 fn sugeno_hot_input_gives_high_speed() {
1148 let mut engine = build_temperature_engine();
1149 engine.inference = InferenceMethod::Sugeno;
1150 let mut inputs = HashMap::new();
1151 inputs.insert("temperature".to_string(), 95.0_f64);
1152 let speed = engine.infer(&inputs, "fan_speed").expect("infer sugeno");
1153 assert!(speed > 60.0, "expected fast speed, got {speed}");
1154 }
1155
1156 #[test]
1161 fn mean_of_max_defuzz() {
1162 let mut engine = build_temperature_engine();
1163 engine.defuzz = DefuzzMethod::MeanOfMax;
1164 let mut inputs = HashMap::new();
1165 inputs.insert("temperature".to_string(), 50.0_f64);
1166 let speed = engine.infer(&inputs, "fan_speed").expect("infer mom");
1167 assert!((0.0..=100.0).contains(&speed));
1168 }
1169
1170 #[test]
1171 fn largest_of_max_defuzz() {
1172 let mut engine = build_temperature_engine();
1173 engine.defuzz = DefuzzMethod::LargestOfMax;
1174 let mut inputs = HashMap::new();
1175 inputs.insert("temperature".to_string(), 50.0_f64);
1176 let speed = engine.infer(&inputs, "fan_speed").expect("infer lom");
1177 assert!((0.0..=100.0).contains(&speed));
1178 }
1179
1180 #[test]
1185 fn membership_at_correct_value() {
1186 let engine = build_temperature_engine();
1187 let mu = engine
1188 .membership_at("temperature", "warm", 50.0)
1189 .expect("membership_at");
1190 assert!((mu - 1.0).abs() < 1e-10);
1191 }
1192
1193 #[test]
1194 fn membership_at_unknown_set() {
1195 let engine = build_temperature_engine();
1196 let err = engine.membership_at("temperature", "boiling", 50.0);
1197 assert!(matches!(err, Err(FuzzyError::SetNotFound { .. })));
1198 }
1199
1200 #[test]
1201 fn membership_at_unknown_variable() {
1202 let engine = build_temperature_engine();
1203 let err = engine.membership_at("pressure", "low", 50.0);
1204 assert!(matches!(err, Err(FuzzyError::VariableNotFound(_))));
1205 }
1206
1207 #[test]
1212 fn evaluate_same_as_infer() {
1213 let mut engine = build_temperature_engine();
1214 let mut inputs = HashMap::new();
1215 inputs.insert("temperature".to_string(), 60.0_f64);
1216 let via_infer = engine.infer(&inputs, "fan_speed").expect("infer");
1217 let via_evaluate = engine
1218 .evaluate(&inputs.clone(), "fan_speed")
1219 .expect("evaluate");
1220 assert!((via_infer - via_evaluate).abs() < 1e-10);
1221 }
1222
1223 #[test]
1228 fn stats_reports_correct_counts() {
1229 let engine = build_temperature_engine();
1230 let stats = engine.stats();
1231 assert_eq!(stats.variable_count, 2);
1232 assert_eq!(stats.rule_count, 3);
1233 assert!(stats.inference.contains("Mamdani"));
1234 assert!(stats.defuzz.contains("Centroid"));
1235 }
1236
1237 #[test]
1242 fn multi_antecedent_and_rule() {
1243 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
1244 let mut temp = FuzzyVariable::new("temperature", 0.0, 100.0);
1246 temp.add_set(FuzzySet::new(
1247 "hot",
1248 MembershipFunction::Trapezoidal {
1249 a: 50.0,
1250 b: 75.0,
1251 c: 100.0,
1252 d: 100.0,
1253 },
1254 ));
1255 engine.add_variable(temp).expect("temp");
1256
1257 let mut hum = FuzzyVariable::new("humidity", 0.0, 100.0);
1258 hum.add_set(FuzzySet::new(
1259 "high",
1260 MembershipFunction::Trapezoidal {
1261 a: 50.0,
1262 b: 75.0,
1263 c: 100.0,
1264 d: 100.0,
1265 },
1266 ));
1267 engine.add_variable(hum).expect("hum");
1268
1269 let mut comfort = FuzzyVariable::new("discomfort", 0.0, 100.0);
1270 comfort.add_set(FuzzySet::new(
1271 "severe",
1272 MembershipFunction::Trapezoidal {
1273 a: 50.0,
1274 b: 75.0,
1275 c: 100.0,
1276 d: 100.0,
1277 },
1278 ));
1279 engine.add_variable(comfort).expect("comfort");
1280
1281 engine
1283 .add_rule(FuzzyRule::new(
1284 vec![
1285 FuzzyProposition::new("temperature", "hot"),
1286 FuzzyProposition::new("humidity", "high"),
1287 ],
1288 FuzzyProposition::new("discomfort", "severe"),
1289 1.0,
1290 ))
1291 .expect("add multi rule");
1292
1293 let mut inputs = HashMap::new();
1294 inputs.insert("temperature".to_string(), 90.0_f64);
1295 inputs.insert("humidity".to_string(), 90.0_f64);
1296 let result = engine.infer(&inputs, "discomfort").expect("multi infer");
1297 assert!(result > 50.0, "expected high discomfort, got {result}");
1299 }
1300
1301 #[test]
1302 fn multi_antecedent_and_takes_min() {
1303 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
1304
1305 let mut a = FuzzyVariable::new("a", 0.0, 1.0);
1306 a.add_set(FuzzySet::new("s", MembershipFunction::Universe));
1307 engine.add_variable(a).expect("a");
1308
1309 let mut b = FuzzyVariable::new("b", 0.0, 1.0);
1310 b.add_set(FuzzySet::new(
1311 "s",
1312 MembershipFunction::Triangular {
1313 a: 0.0,
1314 b: 0.0,
1315 c: 1.0,
1316 },
1317 ));
1318 engine.add_variable(b).expect("b");
1319
1320 let mut out = FuzzyVariable::new("out", 0.0, 1.0);
1321 out.add_set(FuzzySet::new("s", MembershipFunction::Universe));
1322 engine.add_variable(out).expect("out");
1323
1324 engine
1325 .add_rule(FuzzyRule::new(
1326 vec![
1327 FuzzyProposition::new("a", "s"), FuzzyProposition::new("b", "s"), ],
1330 FuzzyProposition::new("out", "s"),
1331 1.0,
1332 ))
1333 .expect("rule");
1334
1335 let mut inputs = HashMap::new();
1336 inputs.insert("a".to_string(), 0.5_f64);
1337 inputs.insert("b".to_string(), 0.0_f64); let result = engine.infer(&inputs, "out").expect("and min");
1342 assert!((result - 0.5).abs() < 0.1, "expected ~0.5, got {result}");
1343 }
1344
1345 #[test]
1350 fn infer_with_gaussian_mf() {
1351 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Mamdani, DefuzzMethod::Centroid);
1352
1353 let mut inp = FuzzyVariable::new("signal", 0.0, 10.0);
1354 inp.add_set(FuzzySet::new(
1355 "mid",
1356 MembershipFunction::Gaussian {
1357 mean: 5.0,
1358 sigma: 1.0,
1359 },
1360 ));
1361 engine.add_variable(inp).expect("inp");
1362
1363 let mut out = FuzzyVariable::new("output", 0.0, 10.0);
1364 out.add_set(FuzzySet::new(
1365 "mid",
1366 MembershipFunction::Gaussian {
1367 mean: 5.0,
1368 sigma: 1.0,
1369 },
1370 ));
1371 engine.add_variable(out).expect("out");
1372
1373 engine
1374 .add_rule(FuzzyRule::new(
1375 vec![FuzzyProposition::new("signal", "mid")],
1376 FuzzyProposition::new("output", "mid"),
1377 1.0,
1378 ))
1379 .expect("gaussian rule");
1380
1381 let mut inputs = HashMap::new();
1382 inputs.insert("signal".to_string(), 5.0_f64); let result = engine.infer(&inputs, "output").expect("gaussian infer");
1384 assert!((result - 5.0).abs() < 0.5, "expected ~5.0, got {result}");
1386 }
1387
1388 #[test]
1393 fn sugeno_zero_activation_numerical_error() {
1394 let mut engine = FuzzyLogicEngine::new(InferenceMethod::Sugeno, DefuzzMethod::Centroid);
1395
1396 let mut inp = FuzzyVariable::new("x", 0.0, 10.0);
1397 inp.add_set(FuzzySet::new(
1398 "low",
1399 MembershipFunction::Triangular {
1400 a: 0.0,
1401 b: 0.0,
1402 c: 5.0,
1403 },
1404 ));
1405 engine.add_variable(inp).expect("x");
1406
1407 let mut out = FuzzyVariable::new("y", 0.0, 10.0);
1408 out.add_set(FuzzySet::new("s", MembershipFunction::Universe));
1409 engine.add_variable(out).expect("y");
1410
1411 engine
1412 .add_rule(FuzzyRule::new(
1413 vec![FuzzyProposition::new("x", "low")],
1414 FuzzyProposition::new("y", "s"),
1415 1.0,
1416 ))
1417 .expect("rule");
1418
1419 let mut inputs = HashMap::new();
1421 inputs.insert("x".to_string(), 10.0_f64);
1422 let err = engine.infer(&inputs, "y");
1423 assert!(matches!(err, Err(FuzzyError::NumericalError(_))));
1424 }
1425}