1use 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
22pub trait Float: NumFloat + Zero + One + PartialOrd + Copy + fmt::Debug {}
24impl<T: NumFloat + Zero + One + PartialOrd + Copy + fmt::Debug> Float for T {}
25
26#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
28pub struct MembershipDegree<M: Float>(M);
29
30impl<M: Float> MembershipDegree<M> {
31 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 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 pub fn get(&self) -> M {
56 self.0
57 }
58
59 pub fn zero() -> Self {
61 MembershipDegree(M::zero())
62 }
63
64 pub fn one() -> Self {
66 MembershipDegree(M::one())
67 }
68}
69
70#[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
92pub trait MembershipFunction<D, M>
94where
95 D: Float,
96 M: Float,
97{
98 fn evaluate(&self, value: D) -> MembershipDegree<M>;
100}
101
102pub 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 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 pub fn name(&self) -> &str {
119 &self.name
120 }
121
122 pub fn evaluate(&self, value: D) -> MembershipDegree<M> {
124 self.membership_fn.evaluate(value)
125 }
126}
127
128pub 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 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 pub fn add_set(&mut self, set: FuzzySet<D, M>) {
147 self.sets.push(set);
148 }
149
150 pub fn name(&self) -> &str {
152 &self.name
153 }
154
155 pub fn range(&self) -> (D, D) {
157 self.range
158 }
159
160 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
169pub trait TNorm<M: Float> {
171 fn apply(&self, a: MembershipDegree<M>, b: MembershipDegree<M>) -> MembershipDegree<M>;
173}
174
175pub trait SNorm<M: Float> {
177 fn apply(&self, a: MembershipDegree<M>, b: MembershipDegree<M>) -> MembershipDegree<M>;
179}
180
181pub 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 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
201pub 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 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
221pub 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#[derive(Debug, Clone, Copy)]
231pub enum RuleOperator {
232 And,
233 Or,
234}
235
236impl<D: Float, M: Float> FuzzyRule<D, M> {
237 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 pub fn with_weight(mut self, weight: MembershipDegree<M>) -> Self {
253 self.weight = weight;
254 self
255 }
256}
257
258pub 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 pub fn builder() -> FuzzyControllerBuilder<D, M> {
270 FuzzyControllerBuilder::new()
271 }
272
273 pub fn evaluate(
275 &self,
276 inputs: &HashMap<String, D>,
277 defuzzifier: &dyn defuzzification::Defuzzifier<D, M>,
278 ) -> Result<HashMap<String, D>, MembershipError> {
279 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 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 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 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 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
415pub 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 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 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 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 pub fn add_rule(mut self, rule: FuzzyRule<D, M>) -> Self {
450 self.rules.push(rule);
451 self
452 }
453
454 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 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 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 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 pub fn add_rules(mut self, rules: Vec<FuzzyRule<D, M>>) -> Self {
484 self.rules.extend(rules);
485 self
486 }
487
488 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}