qualia_core_db/modalities/fuzzy_quantifiers.rs
1//! Fuzzy linguistic quantifiers (Ma, Li & Ma ch 5.4; Zadeh) — evaluate statements
2//! like "*most* guardians concur" or "*few* objections" over graded evidence,
3//! keeping governance legible in human terms without collapsing to brittle counts.
4//!
5//! **Scope (a §12-allowed deferral):** this provides the *machinery* — Zadeh's
6//! relative quantifiers as monotone membership curves over the satisfied-proportion,
7//! plus the sigma-count proportion. The **named set** of governance quantifiers and
8//! their exact membership curves ("most", "almost all", and any sensitive ones) are
9//! **Timothy's to coin/ratify** — this module deliberately ships only generic
10//! constructors + the classic illustrative curves, not a governance vocabulary.
11//! Kernel-class `Reduction`.
12
13/// A relative fuzzy quantifier: a monotone non-decreasing map from a proportion in
14/// `[0,1]` to a truth degree in `[0,1]`, represented as a linear ramp `0` below
15/// `low`, `1` above `high`, linear between (`low ≤ high`).
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct RelativeQuantifier {
18 pub low: f64,
19 pub high: f64,
20}
21
22impl RelativeQuantifier {
23 /// "At least a `low`..`high` fraction" ramp — the building block for relative
24 /// quantifiers. `low == high` is a crisp threshold.
25 pub fn ramp(low: f64, high: f64) -> Self {
26 let l = low.clamp(0.0, 1.0);
27 let h = high.clamp(l, 1.0);
28 Self { low: l, high: h }
29 }
30
31 /// Zadeh's classic illustrative "most" (ramp 0.3 → 0.8). Provided as an *example*
32 /// only — governance quantifiers are coined by Timothy, not assumed here.
33 pub fn most_example() -> Self {
34 Self::ramp(0.3, 0.8)
35 }
36
37 /// Truth degree for the given satisfied-proportion.
38 pub fn apply(self, proportion: f64) -> f64 {
39 let p = proportion.clamp(0.0, 1.0);
40 // Check the upper bound first so the degenerate `low == high` (a crisp
41 // threshold) maps `p == high` to full truth rather than 0.
42 if p >= self.high {
43 1.0
44 } else if p <= self.low {
45 0.0
46 } else {
47 (p - self.low) / (self.high - self.low)
48 }
49 }
50}
51
52/// Sigma-count proportion: the fuzzy "fraction satisfied" = `Σ degrees / n`. `None`
53/// for an empty set.
54pub fn fuzzy_proportion(degrees: &[f64]) -> Option<f64> {
55 if degrees.is_empty() {
56 return None;
57 }
58 let s: f64 = degrees.iter().map(|d| d.clamp(0.0, 1.0)).sum();
59 Some(s / degrees.len() as f64)
60}
61
62/// Evaluate "Q elements satisfy P" — apply the quantifier to the sigma-count
63/// proportion of the per-element satisfaction `degrees`. `None` for an empty set.
64pub fn evaluate(degrees: &[f64], quantifier: RelativeQuantifier) -> Option<f64> {
65 Some(quantifier.apply(fuzzy_proportion(degrees)?))
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn ramp_endpoints_and_interpolation() {
74 let q = RelativeQuantifier::ramp(0.3, 0.8);
75 assert_eq!(q.apply(0.2), 0.0); // below low
76 assert_eq!(q.apply(0.9), 1.0); // above high
77 assert!((q.apply(0.55) - 0.5).abs() < 1e-9); // midpoint
78 }
79
80 #[test]
81 fn most_of_a_concurring_group_is_true() {
82 // 5 elements, mostly high satisfaction → "most" is largely true.
83 let degrees = [0.9, 0.8, 0.85, 0.7, 0.95];
84 let v = evaluate(°rees, RelativeQuantifier::most_example()).unwrap();
85 assert!(v > 0.8, "most should be ~true: {v}");
86 // A divided group → "most" is low.
87 let split = [0.9, 0.1, 0.8, 0.2, 0.1];
88 let v2 = evaluate(&split, RelativeQuantifier::most_example()).unwrap();
89 assert!(v2 < 0.5, "divided group: {v2}");
90 }
91
92 #[test]
93 fn proportion_is_sigma_count() {
94 assert!((fuzzy_proportion(&[1.0, 0.0, 0.5, 0.5]).unwrap() - 0.5).abs() < 1e-9);
95 assert!(fuzzy_proportion(&[]).is_none());
96 }
97
98 #[test]
99 fn crisp_threshold_quantifier() {
100 // "all" ≈ ramp(1,1): only proportion 1.0 yields full truth.
101 let all = RelativeQuantifier::ramp(1.0, 1.0);
102 assert_eq!(all.apply(0.99), 0.0);
103 assert_eq!(all.apply(1.0), 1.0);
104 }
105}