Skip to content

Commit 5c8039b

Browse files
committed
Add fuzzy logic operator families and rule evaluation
Introduces the `FuzzyOps` trait and multiple operator families (MinMax, Product, Łukasiewicz) with feature flags for fuzzy logic operations. Adds the `Antecedent` AST and `eval_antecedent` function for rule evaluation using the default Min–Max operators. Updates error handling to support missing variable/input spaces, and re-exports `FuzzyOps` in the prelude.
1 parent 0e0eede commit 5c8039b

File tree

6 files changed

+374
-3
lines changed

6 files changed

+374
-3
lines changed

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ version = "0.1.0"
44
edition = "2024"
55

66
[features]
7-
default = ["f64"]
7+
default = ["f64", "ops-dyn"]
88
f32 = []
99
f64 = []
1010
serde = ["dep:serde"]
1111
parallel = ["dep:rayon"]
12+
ops-minmax = []
13+
ops-product = []
14+
ops-lukasiewicz = []
15+
ops-dyn = [] # enables runtime enum/trait-object API
16+
17+
[profile.release]
18+
debug = true
1219

1320
[dependencies]
1421
serde = { version = "1", features = ["derive"], optional = true }

src/error.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@ pub type Result<T> = std::result::Result<T, FuzzyError>;
77

88
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99
#[non_exhaustive]
10-
1110
///Basic errors that can occur in the rust-fuzzylogic library
1211
pub enum FuzzyError {
1312
BadArity,
1413
EmptyInput,
1514
TypeMismatch,
1615
OutOfBounds,
16+
NotFound { space: MissingSpace, key: String },
17+
}
18+
19+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20+
pub enum MissingSpace {
21+
Var,
22+
Input,
1723
}
1824

1925
impl fmt::Display for FuzzyError {
@@ -31,6 +37,16 @@ impl fmt::Display for FuzzyError {
3137
FuzzyError::OutOfBounds => {
3238
write!(f, "Out of bounds")
3339
}
40+
FuzzyError::NotFound { space, key } => {
41+
write!(
42+
f,
43+
"Keys not found. {key} cannot be found in {}",
44+
match space {
45+
MissingSpace::Input => "Inputs",
46+
MissingSpace::Var => "Vars",
47+
}
48+
)
49+
}
3450
}
3551
}
3652
}

src/ops.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,164 @@
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;
14

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+
}

src/prelude.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,8 @@ pub use crate::membership::MembershipFn;
1919
pub use crate::membership::trapezoidal::Trapezoidal;
2020
pub use crate::membership::{Gaussian, Triangular};
2121

22+
// Fuzzy Set Operands
23+
pub use crate::ops::FuzzyOps;
24+
2225
// Term wrapper around a boxed membership function
2326
pub use crate::term::Term;

0 commit comments

Comments
 (0)