|
| 1 | +// Operators for fuzzy logic antecedents and inference. |
| 2 | +// Provides a trait (`FuzzyOps`) and concrete families (`Ops`) implementing AND/OR/NOT. |
| 3 | +use crate::Float; |
1 | 4 |
|
| 5 | +/// Common interface for fuzzy logic operators (T-norm, S-norm, complement). |
| 6 | +pub trait FuzzyOps { |
| 7 | + /// T-norm (logical AND) combining two degrees in [0, 1]. |
| 8 | + fn t(&self, a: Float, b: Float) -> Float; |
| 9 | + |
| 10 | + /// S-norm (logical OR) combining two degrees in [0, 1]. |
| 11 | + fn s(&self, a: Float, b: Float) -> Float; |
| 12 | + |
| 13 | + /// Complement (logical NOT) of a degree in [0, 1]. |
| 14 | + fn c(&self, a: Float) -> Float; |
| 15 | +} |
| 16 | + |
| 17 | +#[cfg(feature = "ops-minmax")] |
| 18 | +pub struct MinMax; |
| 19 | +#[cfg(feature = "ops-minmax")] |
| 20 | +impl FuzzyOps for MinMax { |
| 21 | + fn t(&self, a: Float, b: Float) -> Float { |
| 22 | + a.min(b) |
| 23 | + } |
| 24 | + |
| 25 | + fn s(&self, a: Float, b: Float) -> Float { |
| 26 | + a.max(b) |
| 27 | + } |
| 28 | + |
| 29 | + fn c(&self, a: Float) -> Float { |
| 30 | + 1.0 - a |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +#[cfg(feature = "ops-product")] |
| 35 | +pub struct MinMax; |
| 36 | +#[cfg(feature = "ops-product")] |
| 37 | +impl FuzzyOps for MinMax { |
| 38 | + fn t(&self, a: Float, b: Float) -> Float { |
| 39 | + a * b |
| 40 | + } |
| 41 | + |
| 42 | + fn s(&self, a: Float, b: Float) -> Float { |
| 43 | + a + b - a * b |
| 44 | + } |
| 45 | + |
| 46 | + fn c(&self, a: Float) -> Float { |
| 47 | + 1.0 - a |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +#[cfg(feature = "ops-lukasiewicz")] |
| 52 | +pub struct MinMax; |
| 53 | +#[cfg(feature = "ops-lukasiewicz")] |
| 54 | +impl FuzzyOps for MinMax { |
| 55 | + fn t(&self, a: Float, b: Float) -> Float { |
| 56 | + (a + b - 1.0).max(0.0) |
| 57 | + } |
| 58 | + |
| 59 | + fn s(&self, a: Float, b: Float) -> Float { |
| 60 | + (a + b).min(1.0) |
| 61 | + } |
| 62 | + |
| 63 | + fn c(&self, a: Float) -> Float { |
| 64 | + 1.0 - a |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +#[cfg(feature = "ops-dyn")] |
| 69 | +#[derive(Clone, Copy, Debug)] |
| 70 | +/// Built-in operator families providing AND/OR/NOT over degrees. |
| 71 | +pub enum Ops { |
| 72 | + /// Min–Max family |
| 73 | + /// - T: `min(a, b)` |
| 74 | + /// - S: `max(a, b)` |
| 75 | + /// - C: `1 - a` |
| 76 | + MinMax, |
| 77 | + /// Product family |
| 78 | + /// - T: `a * b` |
| 79 | + /// - S: `a + b - a * b` (not algebraic sum; may exceed 1.0) |
| 80 | + /// - C: `1 - a` |
| 81 | + Product, |
| 82 | + /// Łukasiewicz family |
| 83 | + /// - T: `max(0, a + b - 1)` |
| 84 | + /// - S: `min(1, a + b)` |
| 85 | + /// - C: `1 - a` |
| 86 | + Lukasiewicz, |
| 87 | +} |
| 88 | +#[cfg(feature = "ops-dyn")] |
| 89 | +/// Implements `FuzzyOps` for each `Ops` variant using the formulas above. |
| 90 | +impl FuzzyOps for Ops { |
| 91 | + /// T-norm (AND) per family. |
| 92 | + fn t(&self, a: Float, b: Float) -> Float { |
| 93 | + match self { |
| 94 | + Ops::MinMax => a.min(b), |
| 95 | + Ops::Product => a * b, |
| 96 | + Ops::Lukasiewicz => (a + b - 1.0).max(0.0), |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + /// S-norm (OR) per family. |
| 101 | + fn s(&self, a: Float, b: Float) -> Float { |
| 102 | + match self { |
| 103 | + Ops::MinMax => a.max(b), |
| 104 | + Ops::Product => a + b - a * b, |
| 105 | + Ops::Lukasiewicz => (a + b).min(1.0), |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + /// Complement (NOT) shared by all families: `1 - a`. |
| 110 | + fn c(&self, a: Float) -> Float { |
| 111 | + 1.0 - a |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +#[cfg(feature = "ops-dyn")] |
| 116 | +#[cfg(test)] |
| 117 | +mod tests_dyn_ops { |
| 118 | + use crate::ops::*; |
| 119 | + |
| 120 | + // RED: expected default operator behavior (min/max/1-x). |
| 121 | + // This test references the intended API and should fail right now |
| 122 | + // because the operators are not implemented yet. |
| 123 | + #[test] |
| 124 | + fn red_minmax_defaults_and_or_not() { |
| 125 | + let v = crate::ops::Ops::MinMax; |
| 126 | + |
| 127 | + // Mixed values |
| 128 | + assert_eq!(v.t(0.2, 0.8), 0.2); |
| 129 | + assert_eq!(v.s(0.2, 0.8), 0.8); |
| 130 | + assert_eq!(v.c(0.2), 0.8); |
| 131 | + |
| 132 | + // Boundaries |
| 133 | + assert_eq!(v.t(0.0, 1.0), 0.0); |
| 134 | + assert_eq!(v.s(0.0, 1.0), 1.0); |
| 135 | + assert_eq!(v.c(0.0), 1.0); |
| 136 | + assert_eq!(v.c(1.0), 0.0); |
| 137 | + } |
| 138 | + |
| 139 | + #[test] |
| 140 | + fn product_ops_and_or_not_matches_code() { |
| 141 | + let v = Ops::Product; |
| 142 | + let eps = crate::Float::EPSILON; |
| 143 | + // t = a*b |
| 144 | + assert!((v.t(0.2, 0.8) - 0.16).abs() < eps); |
| 145 | + // s = a + b (per current code) |
| 146 | + assert!((v.s(0.1, 0.2) - 0.28).abs() < eps); |
| 147 | + // c = 1 - a |
| 148 | + assert!((v.c(0.2) - 0.8).abs() < eps); |
| 149 | + } |
| 150 | + |
| 151 | + #[test] |
| 152 | + fn lukasiewicz_ops_and_or_not_matches_code() { |
| 153 | + let v = Ops::Lukasiewicz; |
| 154 | + let eps = crate::Float::EPSILON; |
| 155 | + // t = max(0, a + b - 1) |
| 156 | + assert!((v.t(0.2, 0.8) - 0.0).abs() < eps); |
| 157 | + assert!((v.t(0.8, 0.3) - 0.1).abs() < eps); |
| 158 | + // s = min(1, a + b) |
| 159 | + assert!((v.s(0.2, 0.8) - 1.0).abs() < eps); |
| 160 | + assert!((v.s(0.4, 0.4) - 0.8).abs() < eps); |
| 161 | + // c = 1 - a |
| 162 | + assert!((v.c(0.2) - 0.8).abs() < eps); |
| 163 | + } |
| 164 | +} |
0 commit comments