Skip to main content

qualia_core_db/solvers/fuzzy_query/
membership.rs

1//! Membership functions for fuzzy `FILTER`s — map a crisp numeric value (e.g. an age,
2//! a distance, a similarity score bound to a query variable) to a degree in `[0, 1]`.
3//! These produce the per-solution degree that the [`super::solution`] algebra then
4//! composes. They are query-layer primitives (value → degree); the *logic* operators
5//! that combine degrees are reused from [`crate::modalities::fuzzy`].
6
7#[inline]
8fn clamp01(x: f64) -> f64 {
9    x.clamp(0.0, 1.0)
10}
11
12/// Rising ramp: `0` at/below `a`, `1` at/above `b`, linear between (`a < b`). Use for
13/// "at least about `b`" predicates. If `a >= b`, behaves as a crisp step at `a`.
14pub fn ramp_up(x: f64, a: f64, b: f64) -> f64 {
15    if b <= a {
16        return if x >= a { 1.0 } else { 0.0 };
17    }
18    clamp01((x - a) / (b - a))
19}
20
21/// Falling ramp: `1` at/below `a`, `0` at/above `b` (`a < b`). "At most about `a`".
22pub fn ramp_down(x: f64, a: f64, b: f64) -> f64 {
23    if b <= a {
24        return if x <= a { 1.0 } else { 0.0 };
25    }
26    clamp01((b - x) / (b - a))
27}
28
29/// Triangular membership: peak `1` at `m`, falling to `0` at `a` (left) and `b`
30/// (right), `0` outside `[a, b]`. Requires `a <= m <= b`.
31pub fn triangular(x: f64, a: f64, m: f64, b: f64) -> f64 {
32    if x <= a || x >= b {
33        0.0
34    } else if (x - m).abs() < f64::EPSILON {
35        1.0
36    } else if x < m {
37        clamp01((x - a) / (m - a))
38    } else {
39        clamp01((b - x) / (b - m))
40    }
41}
42
43/// Trapezoidal membership: `0` below `a`, rising on `[a, b]`, plateau `1` on `[b, c]`,
44/// falling on `[c, d]`, `0` above `d`. Requires `a <= b <= c <= d`.
45pub fn trapezoidal(x: f64, a: f64, b: f64, c: f64, d: f64) -> f64 {
46    if x <= a || x >= d {
47        0.0
48    } else if x < b {
49        clamp01((x - a) / (b - a))
50    } else if x <= c {
51        1.0
52    } else {
53        clamp01((d - x) / (d - c))
54    }
55}
56
57/// "≈ `target`" with tolerance `tol`: a symmetric triangle peaking at `target`,
58/// reaching `0` at `target ± tol`. `tol <= 0` is a crisp equality.
59pub fn approximately(x: f64, target: f64, tol: f64) -> f64 {
60    if tol <= 0.0 {
61        return if (x - target).abs() < f64::EPSILON {
62            1.0
63        } else {
64            0.0
65        };
66    }
67    triangular(x, target - tol, target, target + tol)
68}
69
70/// "Much greater than `reference`": rises from `0` at `reference` to `1` at
71/// `reference + spread`.
72pub fn much_greater_than(x: f64, reference: f64, spread: f64) -> f64 {
73    ramp_up(x, reference, reference + spread.max(0.0))
74}
75
76/// "Much less than `reference`": falls from `1` at `reference − spread` to `0` at
77/// `reference`.
78pub fn much_less_than(x: f64, reference: f64, spread: f64) -> f64 {
79    ramp_down(x, reference - spread.max(0.0), reference)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    const EPS: f64 = 1e-9;
86
87    #[test]
88    fn ramps_endpoints_and_interior() {
89        assert!((ramp_up(0.0, 1.0, 3.0)).abs() < EPS);
90        assert!((ramp_up(2.0, 1.0, 3.0) - 0.5).abs() < EPS);
91        assert!((ramp_up(9.0, 1.0, 3.0) - 1.0).abs() < EPS);
92        assert!((ramp_down(0.0, 1.0, 3.0) - 1.0).abs() < EPS);
93        assert!((ramp_down(2.0, 1.0, 3.0) - 0.5).abs() < EPS);
94    }
95
96    #[test]
97    fn triangle_peaks_at_m() {
98        assert!((triangular(30.0, 20.0, 30.0, 40.0) - 1.0).abs() < EPS);
99        assert!((triangular(25.0, 20.0, 30.0, 40.0) - 0.5).abs() < EPS);
100        assert!(triangular(45.0, 20.0, 30.0, 40.0).abs() < EPS);
101    }
102
103    #[test]
104    fn trapezoid_plateau() {
105        assert!((trapezoidal(5.0, 1.0, 4.0, 6.0, 9.0) - 1.0).abs() < EPS); // on plateau
106        assert!((trapezoidal(2.5, 1.0, 4.0, 6.0, 9.0) - 0.5).abs() < EPS); // rising
107        assert!((trapezoidal(7.5, 1.0, 4.0, 6.0, 9.0) - 0.5).abs() < EPS); // falling
108    }
109
110    #[test]
111    fn approximately_is_symmetric() {
112        assert!((approximately(30.0, 30.0, 5.0) - 1.0).abs() < EPS);
113        assert!((approximately(32.5, 30.0, 5.0) - 0.5).abs() < EPS);
114        assert!((approximately(27.5, 30.0, 5.0) - 0.5).abs() < EPS);
115        assert!(approximately(40.0, 30.0, 5.0).abs() < EPS);
116    }
117
118    #[test]
119    fn comparators() {
120        assert!((much_greater_than(100.0, 50.0, 50.0) - 1.0).abs() < EPS);
121        assert!((much_greater_than(50.0, 50.0, 50.0)).abs() < EPS);
122        assert!((much_less_than(0.0, 50.0, 50.0) - 1.0).abs() < EPS);
123    }
124}