Skip to main content

oxilean_std/fuzzy_logic/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4use super::functions::*;
5
6/// A unified fuzzy inference system supporting both Mamdani and Sugeno styles.
7pub struct FuzzyInferenceSystem {
8    /// System type.
9    pub fis_type: FISType,
10    /// Number of input variables.
11    pub n_inputs: usize,
12    /// Universe size for output (used in Mamdani).
13    pub output_size: usize,
14    /// Domain for Mamdani defuzzification.
15    pub output_domain: Vec<f64>,
16    /// Defuzzification method (for Mamdani).
17    pub defuzz_method: DefuzzMethod,
18    /// Mamdani rules (populated if Mamdani).
19    pub mamdani_rules: Vec<MamdaniRule>,
20    /// Sugeno rules (populated if Sugeno).
21    pub sugeno_rules: Vec<SugenoRule>,
22}
23impl FuzzyInferenceSystem {
24    /// Create a Mamdani FIS.
25    pub fn mamdani(output_size: usize, output_domain: Vec<f64>) -> Self {
26        FuzzyInferenceSystem {
27            fis_type: FISType::Mamdani,
28            n_inputs: 0,
29            output_size,
30            output_domain,
31            defuzz_method: DefuzzMethod::CentroidOfArea,
32            mamdani_rules: Vec::new(),
33            sugeno_rules: Vec::new(),
34        }
35    }
36    /// Create a Sugeno FIS.
37    pub fn sugeno(n_inputs: usize) -> Self {
38        FuzzyInferenceSystem {
39            fis_type: FISType::Sugeno,
40            n_inputs,
41            output_size: 0,
42            output_domain: Vec::new(),
43            defuzz_method: DefuzzMethod::CentroidOfArea,
44            mamdani_rules: Vec::new(),
45            sugeno_rules: Vec::new(),
46        }
47    }
48    /// Set the defuzzification method (Mamdani only).
49    pub fn with_defuzz(mut self, method: DefuzzMethod) -> Self {
50        self.defuzz_method = method;
51        self
52    }
53    /// Add a Mamdani rule.
54    pub fn add_mamdani_rule(&mut self, antecedent_mf: Vec<f64>, consequent: FuzzySet) {
55        self.mamdani_rules.push(MamdaniRule {
56            antecedent_mf,
57            consequent,
58        });
59    }
60    /// Add a Sugeno rule.
61    pub fn add_sugeno_rule(
62        &mut self,
63        antecedent_mf: Vec<f64>,
64        output_coeffs: Vec<f64>,
65        output_const: f64,
66    ) {
67        self.sugeno_rules.push(SugenoRule {
68            antecedent_mf,
69            output_coeffs,
70            output_const,
71        });
72    }
73    /// Run Mamdani inference and defuzzify.
74    ///
75    /// `input_degrees[i]` contains the firing degrees for input variable i.
76    pub fn infer_mamdani(&self, input_degrees: &[Vec<f64>]) -> f64 {
77        let sys = MamdaniSystem {
78            rules: self.mamdani_rules.clone(),
79            output_size: self.output_size,
80        };
81        let out_fuzzy = sys.infer(input_degrees);
82        defuzzify(&out_fuzzy, &self.output_domain, self.defuzz_method)
83    }
84    /// Run Sugeno inference and return crisp output.
85    pub fn infer_sugeno(&self, inputs: &[f64], input_degrees: &[Vec<f64>]) -> f64 {
86        let sys = SugenoSystem {
87            rules: self.sugeno_rules.clone(),
88        };
89        sys.infer(inputs, input_degrees)
90    }
91    /// Returns `true` if the FIS has at least one rule.
92    pub fn is_configured(&self) -> bool {
93        match self.fis_type {
94            FISType::Mamdani => !self.mamdani_rules.is_empty(),
95            FISType::Sugeno => !self.sugeno_rules.is_empty(),
96        }
97    }
98}
99/// Standard t-conorm variants (dual to t-norms via De Morgan).
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum TConorm {
102    Maximum,
103    ProbabilisticSum,
104    BoundedSum,
105    Drastic,
106}
107impl TConorm {
108    /// Evaluate the t-conorm at (a, b) ∈ [0,1]².
109    pub fn eval(self, a: f64, b: f64) -> f64 {
110        match self {
111            TConorm::Maximum => a.max(b),
112            TConorm::ProbabilisticSum => a + b - a * b,
113            TConorm::BoundedSum => (a + b).min(1.0),
114            TConorm::Drastic => {
115                if a < 1e-9 {
116                    b
117                } else if b < 1e-9 {
118                    a
119                } else {
120                    1.0
121                }
122            }
123        }
124    }
125}
126/// Fuzzy clustering (c-means membership matrix).
127#[allow(dead_code)]
128#[derive(Debug, Clone)]
129pub struct FuzzyClustering {
130    pub n_samples: usize,
131    pub n_clusters: usize,
132    pub fuzziness: f64,
133    pub membership: Vec<Vec<f64>>,
134}
135#[allow(dead_code)]
136impl FuzzyClustering {
137    pub fn new(n_samples: usize, n_clusters: usize, fuzziness: f64) -> Self {
138        let membership = vec![vec![1.0 / n_clusters as f64; n_samples]; n_clusters];
139        FuzzyClustering {
140            n_samples,
141            n_clusters,
142            fuzziness,
143            membership,
144        }
145    }
146    pub fn get_membership(&self, cluster: usize, sample: usize) -> f64 {
147        self.membership[cluster][sample]
148    }
149    pub fn hard_assignment(&self, sample: usize) -> usize {
150        (0..self.n_clusters)
151            .max_by(|&a, &b| {
152                self.membership[a][sample]
153                    .partial_cmp(&self.membership[b][sample])
154                    .unwrap_or(std::cmp::Ordering::Equal)
155            })
156            .unwrap_or(0)
157    }
158    /// Partition coefficient: measures crispness (1.0 = crisp, 1/c = max fuzz).
159    pub fn partition_coefficient(&self) -> f64 {
160        let mut sum = 0.0;
161        for c in 0..self.n_clusters {
162            for s in 0..self.n_samples {
163                sum += self.membership[c][s].powi(2);
164            }
165        }
166        sum / self.n_samples as f64
167    }
168    pub fn set_membership(&mut self, cluster: usize, sample: usize, val: f64) {
169        self.membership[cluster][sample] = val.clamp(0.0, 1.0);
170    }
171}
172/// Fuzzy C-Means (FCM) clustering algorithm (Bezdek 1981).
173///
174/// Minimises: J_m = Σ_i Σ_k (u_{ik})^m · d(x_i, c_k)^2
175/// subject to Σ_k u_{ik} = 1 for each data point i.
176pub struct FuzzyCMeans {
177    /// Number of clusters.
178    pub c: usize,
179    /// Fuzziness parameter m > 1 (m=2 is most common).
180    pub m: f64,
181    /// Maximum number of iterations.
182    pub max_iter: usize,
183    /// Convergence tolerance for cluster center movement.
184    pub tol: f64,
185}
186impl FuzzyCMeans {
187    /// Create with default parameters (m=2, max_iter=100, tol=1e-6).
188    pub fn new(c: usize) -> Self {
189        FuzzyCMeans {
190            c,
191            m: 2.0,
192            max_iter: 100,
193            tol: 1e-6,
194        }
195    }
196    /// Set fuzziness parameter.
197    pub fn with_m(mut self, m: f64) -> Self {
198        self.m = m;
199        self
200    }
201    /// Set maximum iterations.
202    pub fn with_max_iter(mut self, max_iter: usize) -> Self {
203        self.max_iter = max_iter;
204        self
205    }
206    /// Euclidean distance between two data points.
207    fn dist(a: &[f64], b: &[f64]) -> f64 {
208        a.iter()
209            .zip(b.iter())
210            .map(|(x, y)| (x - y).powi(2))
211            .sum::<f64>()
212            .sqrt()
213    }
214    /// Run FCM on `data` (each row is a data point).
215    ///
216    /// Returns `(membership, centers)`:
217    /// - `membership[i][k]` = degree of point i belonging to cluster k.
218    /// - `centers[k]` = center of cluster k.
219    pub fn fit(&self, data: &[Vec<f64>]) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
220        let n = data.len();
221        let dim = if n > 0 { data[0].len() } else { 1 };
222        if n == 0 || self.c == 0 {
223            return (Vec::new(), Vec::new());
224        }
225        let mut u: Vec<Vec<f64>> = (0..n)
226            .map(|i| {
227                let mut row: Vec<f64> = (0..self.c)
228                    .map(|k| {
229                        let base = 1.0 / self.c as f64;
230                        let delta = 0.1 * ((i + k) % 3) as f64 / 3.0 - 0.05;
231                        (base + delta).clamp(0.01, 0.99)
232                    })
233                    .collect();
234                let s: f64 = row.iter().sum();
235                for v in &mut row {
236                    *v /= s;
237                }
238                row
239            })
240            .collect();
241        let mut centers: Vec<Vec<f64>> = vec![vec![0.0; dim]; self.c];
242        for _iter in 0..self.max_iter {
243            let mut old_centers = centers.clone();
244            for k in 0..self.c {
245                let mut num = vec![0.0_f64; dim];
246                let mut denom = 0.0_f64;
247                for i in 0..n {
248                    let w = u[i][k].powf(self.m);
249                    denom += w;
250                    for d in 0..dim {
251                        num[d] += w * data[i][d];
252                    }
253                }
254                if denom.abs() > 1e-15 {
255                    for d in 0..dim {
256                        centers[k][d] = num[d] / denom;
257                    }
258                }
259            }
260            for i in 0..n {
261                let dists: Vec<f64> = (0..self.c)
262                    .map(|k| Self::dist(&data[i], &centers[k]).max(1e-15))
263                    .collect();
264                let zero_k: Vec<usize> = (0..self.c).filter(|&k| dists[k] < 1e-12).collect();
265                if !zero_k.is_empty() {
266                    for k in 0..self.c {
267                        u[i][k] = 0.0;
268                    }
269                    let share = 1.0 / zero_k.len() as f64;
270                    for &k in &zero_k {
271                        u[i][k] = share;
272                    }
273                } else {
274                    let exp = 2.0 / (self.m - 1.0);
275                    for k in 0..self.c {
276                        let sum: f64 = (0..self.c).map(|j| (dists[k] / dists[j]).powf(exp)).sum();
277                        u[i][k] = 1.0 / sum;
278                    }
279                }
280            }
281            let movement: f64 = old_centers
282                .iter_mut()
283                .zip(centers.iter())
284                .map(|(oc, nc)| Self::dist(oc, nc))
285                .sum();
286            if movement < self.tol {
287                break;
288            }
289        }
290        (u, centers)
291    }
292    /// Compute the partition coefficient V_PC = (1/N) Σ Σ u_{ik}^2 ∈ [1/c, 1].
293    ///
294    /// V_PC = 1 means hard partition, 1/c means maximum fuzziness.
295    pub fn partition_coefficient(membership: &[Vec<f64>]) -> f64 {
296        let n = membership.len();
297        if n == 0 {
298            return 0.0;
299        }
300        let sum: f64 = membership
301            .iter()
302            .flat_map(|row| row.iter())
303            .map(|&u| u * u)
304            .sum();
305        sum / n as f64
306    }
307    /// Compute the classification entropy V_CE = −(1/N) Σ Σ u_{ik} log(u_{ik}).
308    pub fn classification_entropy(membership: &[Vec<f64>]) -> f64 {
309        let n = membership.len();
310        if n == 0 {
311            return 0.0;
312        }
313        let sum: f64 = membership
314            .iter()
315            .flat_map(|row| row.iter())
316            .map(|&u| if u > 1e-15 { -u * u.ln() } else { 0.0 })
317            .sum();
318        sum / n as f64
319    }
320}
321/// Fuzzy inference engine (Mamdani-type).
322#[allow(dead_code)]
323#[derive(Debug, Clone)]
324pub struct MamdaniEngine {
325    pub input_names: Vec<String>,
326    pub output_name: String,
327    pub n_rules: usize,
328    pub defuzz_method: DefuzzMethod,
329}
330#[allow(dead_code)]
331impl MamdaniEngine {
332    pub fn new(inputs: Vec<&str>, output: &str, n_rules: usize) -> Self {
333        MamdaniEngine {
334            input_names: inputs.iter().map(|s| s.to_string()).collect(),
335            output_name: output.to_string(),
336            n_rules,
337            defuzz_method: DefuzzMethod::CentroidOfArea,
338        }
339    }
340    pub fn set_defuzz(&mut self, method: DefuzzMethod) {
341        self.defuzz_method = method;
342    }
343    pub fn n_inputs(&self) -> usize {
344        self.input_names.len()
345    }
346    /// Centroid defuzzification over uniformly-sampled output range.
347    pub fn centroid_defuzz(values: &[f64], memberships: &[f64]) -> f64 {
348        let num: f64 = values
349            .iter()
350            .zip(memberships.iter())
351            .map(|(v, m)| v * m)
352            .sum();
353        let den: f64 = memberships.iter().sum();
354        if den.abs() < 1e-12 {
355            0.0
356        } else {
357            num / den
358        }
359    }
360}
361/// A Sugeno rule: antecedent + linear output function.
362#[derive(Debug, Clone)]
363pub struct SugenoRule {
364    /// Input membership degrees for the antecedent.
365    pub antecedent_mf: Vec<f64>,
366    /// Coefficients for the linear output: z = c0 + c1*x1 + c2*x2 + ...
367    pub output_coeffs: Vec<f64>,
368    pub output_const: f64,
369}
370/// Many-valued logic variant.
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub enum ManyValuedLogic {
373    Lukasiewicz,
374    Godel,
375    Product,
376}
377impl ManyValuedLogic {
378    /// Conjunction (t-norm) for the logic.
379    pub fn conj(self, a: f64, b: f64) -> f64 {
380        match self {
381            ManyValuedLogic::Lukasiewicz => (a + b - 1.0).max(0.0),
382            ManyValuedLogic::Godel => a.min(b),
383            ManyValuedLogic::Product => a * b,
384        }
385    }
386    /// Disjunction (t-conorm) for the logic.
387    pub fn disj(self, a: f64, b: f64) -> f64 {
388        match self {
389            ManyValuedLogic::Lukasiewicz => (a + b).min(1.0),
390            ManyValuedLogic::Godel => a.max(b),
391            ManyValuedLogic::Product => a + b - a * b,
392        }
393    }
394    /// Residuum (implication a → b).
395    pub fn residuum(self, a: f64, b: f64) -> f64 {
396        match self {
397            ManyValuedLogic::Lukasiewicz => (1.0 - a + b).min(1.0),
398            ManyValuedLogic::Godel => {
399                if a <= b {
400                    1.0
401                } else {
402                    b
403                }
404            }
405            ManyValuedLogic::Product => {
406                if a <= b {
407                    1.0
408                } else {
409                    b / a
410                }
411            }
412        }
413    }
414    /// Negation: ¬a = a → 0.
415    pub fn neg(self, a: f64) -> f64 {
416        self.residuum(a, 0.0)
417    }
418    /// Biconditional: a ↔ b = (a → b) ∧ (b → a).
419    pub fn iff(self, a: f64, b: f64) -> f64 {
420        self.conj(self.residuum(a, b), self.residuum(b, a))
421    }
422}
423/// A finite MTL (Monoidal T-norm Logic) algebra over {0, ..., n-1}.
424/// The order is the integer order; 0 = bottom, n-1 = top.
425#[derive(Debug, Clone)]
426pub struct FiniteMTLAlgebra {
427    pub size: usize,
428    /// t-norm table: tnorm[i][j]
429    pub tnorm: Vec<Vec<usize>>,
430    /// residuum table: residuum[i][j] = max { k | tnorm[k][i] ≤ j }
431    pub residuum: Vec<Vec<usize>>,
432}
433impl FiniteMTLAlgebra {
434    /// Build an MTL algebra from a t-norm table (as usize indices).
435    pub fn from_tnorm(size: usize, tnorm: Vec<Vec<usize>>) -> Self {
436        let mut residuum = vec![vec![0usize; size]; size];
437        for a in 0..size {
438            for b in 0..size {
439                let mut best = 0usize;
440                for k in 0..size {
441                    if tnorm[k][a] <= b {
442                        best = best.max(k);
443                    }
444                }
445                residuum[a][b] = best;
446            }
447        }
448        FiniteMTLAlgebra {
449            size,
450            tnorm,
451            residuum,
452        }
453    }
454    /// Evaluate the t-norm.
455    pub fn t(&self, a: usize, b: usize) -> usize {
456        self.tnorm[a][b]
457    }
458    /// Evaluate the residuum (implication).
459    pub fn r(&self, a: usize, b: usize) -> usize {
460        self.residuum[a][b]
461    }
462    /// Negation: ¬a = a → 0.
463    pub fn neg(&self, a: usize) -> usize {
464        self.r(a, 0)
465    }
466    /// Check if the algebra satisfies prelinearity: (a → b) ∨ (b → a) = 1.
467    pub fn satisfies_prelinearity(&self) -> bool {
468        let top = self.size - 1;
469        for a in 0..self.size {
470            for b in 0..self.size {
471                let imp_ab = self.r(a, b);
472                let imp_ba = self.r(b, a);
473                let join = imp_ab.max(imp_ba);
474                if join != top {
475                    return false;
476                }
477            }
478        }
479        true
480    }
481    /// Check if it is a BL algebra: divisibility holds (a ∧ b = a * (a → b)).
482    pub fn satisfies_divisibility(&self) -> bool {
483        for a in 0..self.size {
484            for b in 0..self.size {
485                let meet = a.min(b);
486                let product = self.t(a, self.r(a, b));
487                if meet != product {
488                    return false;
489                }
490            }
491        }
492        true
493    }
494}
495/// A fuzzy rule for a Mamdani system: IF antecedent THEN consequent.
496#[derive(Debug, Clone)]
497pub struct MamdaniRule {
498    /// Antecedent membership degrees for each input variable's linguistic value.
499    pub antecedent_mf: Vec<f64>,
500    /// Consequent fuzzy set (output).
501    pub consequent: FuzzySet,
502}
503/// System type: Mamdani (fuzzy output) or Sugeno (crisp linear output).
504#[derive(Debug, Clone, Copy, PartialEq, Eq)]
505pub enum FISType {
506    Mamdani,
507    Sugeno,
508}
509/// Defuzzification strategy.
510#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum DefuzzMethod {
512    CentroidOfArea,
513    BisectorOfArea,
514    MeanOfMaxima,
515    SmallestOfMaxima,
516    LargestOfMaxima,
517}
518/// Standard t-norm variants.
519#[derive(Debug, Clone, Copy, PartialEq, Eq)]
520pub enum TNorm {
521    Minimum,
522    Product,
523    Lukasiewicz,
524    Drastic,
525}
526impl TNorm {
527    /// Evaluate the t-norm at (a, b) ∈ [0,1]².
528    pub fn eval(self, a: f64, b: f64) -> f64 {
529        match self {
530            TNorm::Minimum => a.min(b),
531            TNorm::Product => a * b,
532            TNorm::Lukasiewicz => (a + b - 1.0).max(0.0),
533            TNorm::Drastic => {
534                if (a - 1.0).abs() < 1e-9 {
535                    b
536                } else if (b - 1.0).abs() < 1e-9 {
537                    a
538                } else {
539                    0.0
540                }
541            }
542        }
543    }
544    /// Check the commutativity property at sample points.
545    pub fn is_commutative_sample(&self, a: f64, b: f64) -> bool {
546        (self.eval(a, b) - self.eval(b, a)).abs() < 1e-9
547    }
548    /// Check associativity at sample points.
549    pub fn is_associative_sample(&self, a: f64, b: f64, c: f64) -> bool {
550        (self.eval(self.eval(a, b), c) - self.eval(a, self.eval(b, c))).abs() < 1e-9
551    }
552}
553/// A fuzzy set over a universe represented as a list of (element, degree) pairs.
554/// Degrees are in [0, 1].
555#[derive(Debug, Clone)]
556pub struct FuzzySet {
557    pub universe_size: usize,
558    /// membership[i] ∈ [0.0, 1.0]
559    pub membership: Vec<f64>,
560}
561impl FuzzySet {
562    /// Create a crisp empty fuzzy set over a universe of the given size.
563    pub fn new(universe_size: usize) -> Self {
564        FuzzySet {
565            universe_size,
566            membership: vec![0.0; universe_size],
567        }
568    }
569    /// Set the membership degree for element i (clamped to [0,1]).
570    pub fn set(&mut self, i: usize, degree: f64) {
571        self.membership[i] = degree.clamp(0.0, 1.0);
572    }
573    /// Get the membership degree for element i.
574    pub fn get(&self, i: usize) -> f64 {
575        self.membership[i]
576    }
577    /// α-cut: returns a crisp set of elements with membership ≥ α.
578    pub fn alpha_cut(&self, alpha: f64) -> Vec<usize> {
579        self.membership
580            .iter()
581            .enumerate()
582            .filter(|(_, &d)| d >= alpha)
583            .map(|(i, _)| i)
584            .collect()
585    }
586    /// Strong α-cut: elements with membership > α.
587    pub fn strong_alpha_cut(&self, alpha: f64) -> Vec<usize> {
588        self.membership
589            .iter()
590            .enumerate()
591            .filter(|(_, &d)| d > alpha)
592            .map(|(i, _)| i)
593            .collect()
594    }
595    /// Fuzzy complement using standard negation: 1 − μ(x).
596    pub fn complement(&self) -> FuzzySet {
597        let membership = self.membership.iter().map(|&d| 1.0 - d).collect();
598        FuzzySet {
599            universe_size: self.universe_size,
600            membership,
601        }
602    }
603    /// Height of the fuzzy set: max membership degree.
604    pub fn height(&self) -> f64 {
605        self.membership.iter().cloned().fold(0.0_f64, f64::max)
606    }
607    /// Support: indices with membership > 0.
608    pub fn support(&self) -> Vec<usize> {
609        self.strong_alpha_cut(0.0)
610    }
611    /// Core: indices with membership = 1.
612    pub fn core(&self) -> Vec<usize> {
613        self.membership
614            .iter()
615            .enumerate()
616            .filter(|(_, &d)| (d - 1.0).abs() < 1e-9)
617            .map(|(i, _)| i)
618            .collect()
619    }
620    /// Is the fuzzy set normal (height = 1)?
621    pub fn is_normal(&self) -> bool {
622        (self.height() - 1.0).abs() < 1e-9
623    }
624}
625/// A fuzzy topology: a collection of fuzzy sets (open sets) over a universe.
626#[derive(Debug, Clone)]
627pub struct FuzzyTopology {
628    pub universe_size: usize,
629    /// Open fuzzy sets: each is a membership vector.
630    pub open_sets: Vec<Vec<f64>>,
631}
632impl FuzzyTopology {
633    pub fn new(universe_size: usize) -> Self {
634        let mut ft = FuzzyTopology {
635            universe_size,
636            open_sets: Vec::new(),
637        };
638        ft.open_sets.push(vec![0.0; universe_size]);
639        ft.open_sets.push(vec![1.0; universe_size]);
640        ft
641    }
642    pub fn add_open_set(&mut self, set: Vec<f64>) {
643        assert_eq!(set.len(), self.universe_size);
644        self.open_sets.push(set);
645    }
646    /// Check closure under finite intersection (minimum).
647    pub fn closed_under_intersection(&self) -> bool {
648        let n = self.open_sets.len();
649        for i in 0..n {
650            for j in i..n {
651                let inter: Vec<f64> = self.open_sets[i]
652                    .iter()
653                    .zip(self.open_sets[j].iter())
654                    .map(|(&a, &b)| a.min(b))
655                    .collect();
656                if !self.contains_set(&inter) {
657                    return false;
658                }
659            }
660        }
661        true
662    }
663    /// Check closure under arbitrary union (maximum over all subsets — here pairwise).
664    pub fn closed_under_union(&self) -> bool {
665        let n = self.open_sets.len();
666        for i in 0..n {
667            for j in i..n {
668                let union: Vec<f64> = self.open_sets[i]
669                    .iter()
670                    .zip(self.open_sets[j].iter())
671                    .map(|(&a, &b)| a.max(b))
672                    .collect();
673                if !self.contains_set(&union) {
674                    return false;
675                }
676            }
677        }
678        true
679    }
680    fn contains_set(&self, s: &[f64]) -> bool {
681        self.open_sets
682            .iter()
683            .any(|o| o.iter().zip(s.iter()).all(|(&a, &b)| (a - b).abs() < 1e-9))
684    }
685}
686/// Evaluates various t-norm families and verifies their algebraic properties.
687pub struct TNormComputer;
688impl TNormComputer {
689    /// Evaluate the Frank t-norm F_s(a,b) for parameter s ∈ (0,∞) \ {1}.
690    ///
691    /// - s→0: drastic t-norm, s→1: product, s→∞: minimum.
692    pub fn frank(s: f64, a: f64, b: f64) -> f64 {
693        if s <= 0.0 {
694            return a.min(b);
695        }
696        if (s - 1.0).abs() < 1e-9 {
697            return a * b;
698        }
699        if s > 1e9 {
700            return a.min(b);
701        }
702        let sa = s.powf(a) - 1.0;
703        let sb = s.powf(b) - 1.0;
704        let denom = s - 1.0;
705        if denom.abs() < 1e-15 {
706            return a.min(b);
707        }
708        let inner = 1.0 + sa * sb / denom;
709        if inner <= 0.0 {
710            return 0.0;
711        }
712        inner.log(s).clamp(0.0, 1.0)
713    }
714    /// Evaluate the Yager t-norm T_p(a,b) = max(0, 1 − ((1−a)^p + (1−b)^p)^{1/p}).
715    pub fn yager(p: f64, a: f64, b: f64) -> f64 {
716        if p <= 0.0 {
717            return a.min(b);
718        }
719        let sum = (1.0 - a).powf(p) + (1.0 - b).powf(p);
720        (1.0 - sum.powf(1.0 / p)).max(0.0).min(1.0)
721    }
722    /// Evaluate the Schweizer-Sklar t-norm T_p(a,b) = (a^p + b^p − 1)^{1/p}.
723    pub fn schweizer_sklar(p: f64, a: f64, b: f64) -> f64 {
724        if p == 0.0 {
725            return a * b;
726        }
727        if p < 0.0 {
728            let val = (a.powf(p) + b.powf(p) - 1.0).powf(1.0 / p);
729            return val.max(0.0).min(1.0);
730        }
731        let val = (a.powf(p) + b.powf(p) - 1.0).powf(1.0 / p);
732        val.max(0.0).min(1.0)
733    }
734    /// Check commutativity of a t-norm at sample points.
735    pub fn check_commutativity<F: Fn(f64, f64) -> f64>(t: &F, samples: &[(f64, f64)]) -> bool {
736        samples
737            .iter()
738            .all(|&(a, b)| (t(a, b) - t(b, a)).abs() < 1e-9)
739    }
740    /// Check associativity of a t-norm at sample triples.
741    pub fn check_associativity<F: Fn(f64, f64) -> f64>(t: &F, triples: &[(f64, f64, f64)]) -> bool {
742        triples
743            .iter()
744            .all(|&(a, b, c)| (t(t(a, b), c) - t(a, t(b, c))).abs() < 1e-9)
745    }
746    /// Check the boundary condition: T(a, 1) = a.
747    pub fn check_boundary<F: Fn(f64, f64) -> f64>(t: &F, samples: &[f64]) -> bool {
748        samples.iter().all(|&a| (t(a, 1.0) - a).abs() < 1e-9)
749    }
750    /// Check monotonicity: a ≤ b implies T(a,c) ≤ T(b,c).
751    pub fn check_monotonicity<F: Fn(f64, f64) -> f64>(t: &F, samples: &[(f64, f64, f64)]) -> bool {
752        samples
753            .iter()
754            .all(|&(a, b, c)| a > b || t(a, c) <= t(b, c) + 1e-9)
755    }
756}
757/// Gradual element: a fuzzy set representing a graded truth value.
758#[allow(dead_code)]
759#[derive(Debug, Clone)]
760pub struct GradualElement {
761    pub name: String,
762    pub degree: f64,
763}
764#[allow(dead_code)]
765impl GradualElement {
766    pub fn new(name: &str, degree: f64) -> Self {
767        GradualElement {
768            name: name.to_string(),
769            degree: degree.clamp(0.0, 1.0),
770        }
771    }
772    pub fn is_true(&self) -> bool {
773        self.degree > 0.5
774    }
775    pub fn complement(&self) -> Self {
776        GradualElement::new(&format!("not_{}", self.name), 1.0 - self.degree)
777    }
778    pub fn conjunction(&self, other: &GradualElement) -> GradualElement {
779        let deg = self.degree.min(other.degree);
780        GradualElement::new(&format!("({} AND {})", self.name, other.name), deg)
781    }
782    pub fn disjunction(&self, other: &GradualElement) -> GradualElement {
783        let deg = self.degree.max(other.degree);
784        GradualElement::new(&format!("({} OR {})", self.name, other.name), deg)
785    }
786}
787/// A fuzzy metric space (in the sense of George and Veeramani).
788/// M(x, y, t) ∈ [0,1] represents the "probability" that d(x,y) < t.
789#[derive(Debug, Clone)]
790pub struct FuzzyMetricSpace {
791    pub points: usize,
792    /// M[x][y][t_idx] — indexed over a finite grid of t values.
793    pub metric: Vec<Vec<Vec<f64>>>,
794    pub t_grid: Vec<f64>,
795}
796impl FuzzyMetricSpace {
797    pub fn new(points: usize, t_grid: Vec<f64>) -> Self {
798        let nt = t_grid.len();
799        FuzzyMetricSpace {
800            points,
801            metric: vec![vec![vec![0.0; nt]; points]; points],
802            t_grid,
803        }
804    }
805    pub fn set_metric(&mut self, x: usize, y: usize, t_idx: usize, value: f64) {
806        self.metric[x][y][t_idx] = value.clamp(0.0, 1.0);
807        self.metric[y][x][t_idx] = value.clamp(0.0, 1.0);
808    }
809    /// GV axiom: M(x, y, t) → 1 as t → ∞ (check last t entry is 1).
810    pub fn check_limit_axiom(&self) -> bool {
811        let last = self.t_grid.len() - 1;
812        for x in 0..self.points {
813            for y in 0..self.points {
814                if (self.metric[x][y][last] - 1.0).abs() > 1e-6 {
815                    return false;
816                }
817            }
818        }
819        true
820    }
821    /// GV axiom: M(x, x, t) = 1 for all t > 0.
822    pub fn check_diagonal_axiom(&self) -> bool {
823        for x in 0..self.points {
824            for t_idx in 0..self.t_grid.len() {
825                if (self.metric[x][x][t_idx] - 1.0).abs() > 1e-6 {
826                    return false;
827                }
828            }
829        }
830        true
831    }
832    /// GV non-separability check: M(x, y, t) = 1 for all t > 0 implies x = y.
833    pub fn check_non_separability(&self) -> bool {
834        for x in 0..self.points {
835            for y in 0..self.points {
836                if x == y {
837                    continue;
838                }
839                let all_one = self.metric[x][y].iter().all(|&v| (v - 1.0).abs() < 1e-6);
840                if all_one {
841                    return false;
842                }
843            }
844        }
845        true
846    }
847}
848/// Applies linguistic hedges (modifiers) to fuzzy membership degrees.
849///
850/// Hedges modify the membership function to represent qualifications
851/// like "very", "more or less", "somewhat", "extremely", etc.
852pub struct LinguisticHedgeApplier;
853impl LinguisticHedgeApplier {
854    /// "very A": μ_A(x)^2 (concentration).
855    pub fn very(degree: f64) -> f64 {
856        degree * degree
857    }
858    /// "more or less A": μ_A(x)^{0.5} (dilation).
859    pub fn more_or_less(degree: f64) -> f64 {
860        degree.sqrt()
861    }
862    /// "somewhat A": μ_A(x)^{0.333} (moderate dilation).
863    pub fn somewhat(degree: f64) -> f64 {
864        degree.powf(1.0 / 3.0)
865    }
866    /// "extremely A": μ_A(x)^3 (stronger concentration).
867    pub fn extremely(degree: f64) -> f64 {
868        degree.powi(3)
869    }
870    /// "not A": 1 − μ_A(x) (standard negation).
871    pub fn not(degree: f64) -> f64 {
872        1.0 - degree
873    }
874    /// "slightly A": intermediate concentration μ_A(x)^{1.7}.
875    pub fn slightly(degree: f64) -> f64 {
876        degree.powf(1.7)
877    }
878    /// "indeed A": normalization-based hedge (intensification).
879    ///
880    /// INT(μ) = 2μ^2 if μ ≤ 0.5, else 1 − 2(1−μ)^2.
881    pub fn indeed(degree: f64) -> f64 {
882        if degree <= 0.5 {
883            2.0 * degree * degree
884        } else {
885            1.0 - 2.0 * (1.0 - degree).powi(2)
886        }
887    }
888    /// "plus A": μ_A(x)^{1.25} (gentle concentration).
889    pub fn plus(degree: f64) -> f64 {
890        degree.powf(1.25)
891    }
892    /// Apply hedge by name. Returns degree unchanged if hedge is unknown.
893    pub fn apply(hedge: &str, degree: f64) -> f64 {
894        match hedge {
895            "very" => Self::very(degree),
896            "more_or_less" | "more-or-less" | "sort_of" => Self::more_or_less(degree),
897            "somewhat" => Self::somewhat(degree),
898            "extremely" => Self::extremely(degree),
899            "not" => Self::not(degree),
900            "slightly" => Self::slightly(degree),
901            "indeed" => Self::indeed(degree),
902            "plus" => Self::plus(degree),
903            _ => degree,
904        }
905    }
906    /// Apply a sequence of hedges in order (innermost first).
907    pub fn apply_chain(hedges: &[&str], degree: f64) -> f64 {
908        hedges.iter().fold(degree, |d, h| Self::apply(h, d))
909    }
910    /// Apply hedge to an entire fuzzy set.
911    pub fn apply_to_set(hedge: &str, set: &FuzzySet) -> FuzzySet {
912        let membership = set
913            .membership
914            .iter()
915            .map(|&d| Self::apply(hedge, d))
916            .collect();
917        FuzzySet {
918            universe_size: set.universe_size,
919            membership,
920        }
921    }
922    /// Returns the power exponent associated with a hedge (for analysis).
923    pub fn exponent(hedge: &str) -> Option<f64> {
924        match hedge {
925            "very" => Some(2.0),
926            "more_or_less" => Some(0.5),
927            "somewhat" => Some(1.0 / 3.0),
928            "extremely" => Some(3.0),
929            "slightly" => Some(1.7),
930            "plus" => Some(1.25),
931            _ => None,
932        }
933    }
934}
935/// Fuzzy rough set approximation over a fuzzy similarity relation.
936#[allow(dead_code)]
937#[derive(Debug, Clone)]
938pub struct FuzzyRoughApprox {
939    pub universe_size: usize,
940    pub similarity: Vec<Vec<f64>>,
941}
942#[allow(dead_code)]
943impl FuzzyRoughApprox {
944    pub fn new(n: usize) -> Self {
945        let mut sim = vec![vec![0.0; n]; n];
946        for i in 0..n {
947            sim[i][i] = 1.0;
948        }
949        FuzzyRoughApprox {
950            universe_size: n,
951            similarity: sim,
952        }
953    }
954    pub fn set_similarity(&mut self, x: usize, y: usize, val: f64) {
955        self.similarity[x][y] = val.clamp(0.0, 1.0);
956        self.similarity[y][x] = val.clamp(0.0, 1.0);
957    }
958    /// Lower approximation of fuzzy set A: (R_↓ A)(x) = inf_y T(R(x,y), A(y))
959    pub fn lower_approx(&self, a: &[f64]) -> Vec<f64> {
960        (0..self.universe_size)
961            .map(|x| {
962                (0..self.universe_size)
963                    .map(|y| {
964                        let r = self.similarity[x][y];
965                        let ay = a[y];
966                        (1.0 - r + ay).min(1.0)
967                    })
968                    .fold(f64::INFINITY, f64::min)
969            })
970            .collect()
971    }
972    /// Upper approximation of fuzzy set A: (R^↑ A)(x) = sup_y T(R(x,y), A(y))
973    pub fn upper_approx(&self, a: &[f64]) -> Vec<f64> {
974        (0..self.universe_size)
975            .map(|x| {
976                (0..self.universe_size)
977                    .map(|y| self.similarity[x][y].min(a[y]))
978                    .fold(0.0f64, f64::max)
979            })
980            .collect()
981    }
982}
983/// Mamdani fuzzy inference system.
984#[derive(Debug, Clone)]
985pub struct MamdaniSystem {
986    pub rules: Vec<MamdaniRule>,
987    pub output_size: usize,
988}
989impl MamdaniSystem {
990    pub fn new(output_size: usize) -> Self {
991        MamdaniSystem {
992            rules: Vec::new(),
993            output_size,
994        }
995    }
996    pub fn add_rule(&mut self, rule: MamdaniRule) {
997        self.rules.push(rule);
998    }
999    /// Aggregate all rule outputs using maximum, clip each by firing strength.
1000    pub fn infer(&self, input_degrees: &[Vec<f64>]) -> FuzzySet {
1001        let mut agg = FuzzySet::new(self.output_size);
1002        for rule in &self.rules {
1003            let strength = rule
1004                .antecedent_mf
1005                .iter()
1006                .zip(input_degrees.iter().flatten())
1007                .map(|(&a, &b)| a.min(b))
1008                .fold(1.0_f64, f64::min);
1009            for i in 0..self.output_size {
1010                let clipped = rule.consequent.get(i).min(strength);
1011                let current = agg.get(i);
1012                agg.set(i, current.max(clipped));
1013            }
1014        }
1015        agg
1016    }
1017}
1018/// Sugeno (Takagi-Sugeno) fuzzy inference system.
1019#[derive(Debug, Clone)]
1020pub struct SugenoSystem {
1021    pub rules: Vec<SugenoRule>,
1022}
1023impl SugenoSystem {
1024    pub fn new() -> Self {
1025        SugenoSystem { rules: Vec::new() }
1026    }
1027    pub fn add_rule(&mut self, rule: SugenoRule) {
1028        self.rules.push(rule);
1029    }
1030    /// Compute the crisp output using weighted average defuzzification.
1031    pub fn infer(&self, inputs: &[f64], input_degrees: &[Vec<f64>]) -> f64 {
1032        let mut weighted_sum = 0.0;
1033        let mut weight_total = 0.0;
1034        for rule in &self.rules {
1035            let strength: f64 = rule
1036                .antecedent_mf
1037                .iter()
1038                .zip(input_degrees.iter().flatten())
1039                .map(|(&a, &b)| a.min(b))
1040                .fold(1.0_f64, f64::min);
1041            let z = rule.output_const
1042                + rule
1043                    .output_coeffs
1044                    .iter()
1045                    .zip(inputs.iter())
1046                    .map(|(&c, &x)| c * x)
1047                    .sum::<f64>();
1048            weighted_sum += strength * z;
1049            weight_total += strength;
1050        }
1051        if weight_total.abs() < 1e-12 {
1052            0.0
1053        } else {
1054            weighted_sum / weight_total
1055        }
1056    }
1057}
1058/// Fuzzy number arithmetic (triangular fuzzy numbers).
1059#[allow(dead_code)]
1060#[derive(Debug, Clone)]
1061pub struct TriangularFuzzyNum {
1062    pub lower: f64,
1063    pub modal: f64,
1064    pub upper: f64,
1065}
1066#[allow(dead_code)]
1067impl TriangularFuzzyNum {
1068    pub fn new(lower: f64, modal: f64, upper: f64) -> Self {
1069        assert!(lower <= modal && modal <= upper);
1070        TriangularFuzzyNum {
1071            lower,
1072            modal,
1073            upper,
1074        }
1075    }
1076    pub fn membership(&self, x: f64) -> f64 {
1077        if x < self.lower || x > self.upper {
1078            0.0
1079        } else if x <= self.modal {
1080            (x - self.lower) / (self.modal - self.lower).max(1e-12)
1081        } else {
1082            (self.upper - x) / (self.upper - self.modal).max(1e-12)
1083        }
1084    }
1085    pub fn add(&self, other: &TriangularFuzzyNum) -> TriangularFuzzyNum {
1086        TriangularFuzzyNum::new(
1087            self.lower + other.lower,
1088            self.modal + other.modal,
1089            self.upper + other.upper,
1090        )
1091    }
1092    pub fn scale(&self, k: f64) -> TriangularFuzzyNum {
1093        if k >= 0.0 {
1094            TriangularFuzzyNum::new(k * self.lower, k * self.modal, k * self.upper)
1095        } else {
1096            TriangularFuzzyNum::new(k * self.upper, k * self.modal, k * self.lower)
1097        }
1098    }
1099    pub fn defuzzify_centroid(&self) -> f64 {
1100        (self.lower + self.modal + self.upper) / 3.0
1101    }
1102    pub fn alpha_cut(&self, alpha: f64) -> (f64, f64) {
1103        let lo = self.lower + alpha * (self.modal - self.lower);
1104        let hi = self.upper - alpha * (self.upper - self.modal);
1105        (lo, hi)
1106    }
1107}