Skip to main content

qualia_core_db/solvers/fuzzy_query/
mod.rs

1//! **f-SPARQL** — degree-aware querying over the semantic graph.
2//!
3//! Classical SPARQL answers are crisp: a solution either matches or it does not.
4//! f-SPARQL carries a **truth degree** in `[0, 1]` on every solution, so a query can
5//! ask "guardians who are *roughly* the right age" or "relations that *strongly*
6//! match", rank by confidence, and threshold (an α-cut) instead of demanding an exact
7//! match. This is the query-layer face of the engine's fuzzy RDF
8//! ([`crate::modalities::fuzzy_rdf_schema`]) and the identity discipline: a graded
9//! answer is a *proposal with a confidence*, never an asserted fact.
10//!
11//! ## Not a fork of the SPARQL engine
12//!
13//! This is a thin, composable algebra **over the engine's own solution type**
14//! ([`crate::sparql_ast::BindingRow`]). The crisp executor still does the graph
15//! matching and produces rows one at a time; [`evaluate::annotate`] /
16//! [`evaluate::collect_from`] attach a degree to each row and the combinators here
17//! ([`solution`]) compose them. Nothing here re-implements the parser or the join
18//! engine.
19//!
20//! ## Degree algebra
21//!
22//! Conjunction (a basic graph pattern / `AND`) combines degrees with a **t-norm**;
23//! disjunction (`UNION`) with a **t-conorm**; `NOT` / negation with the fuzzy
24//! complement. These are exactly the operators in [`crate::modalities::fuzzy`] — the
25//! single source of truth — selected by [`DegreeNorm`]. Default semantics are Gödel
26//! (min/max), the usual f-SPARQL choice.
27//!
28//! Kernel-class `Reduction` over the solution sequence; CPU path always present (§13).
29
30pub mod evaluate;
31pub mod membership;
32pub mod solution;
33
34pub use evaluate::{annotate, collect_from, conjunctive_query};
35pub use solution::{FuzzyResultSet, FuzzySolution};
36
37use crate::modalities::fuzzy::{
38    fuzzy_not, t_conorm_godel, t_conorm_lukasiewicz, t_conorm_product, t_norm_godel,
39    t_norm_lukasiewicz, t_norm_product,
40};
41
42/// Which fuzzy-logic operator family combines solution degrees. Each variant dispatches
43/// to the corresponding operator in [`crate::modalities::fuzzy`] (reuse, not a
44/// re-implementation).
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum DegreeNorm {
47    /// Gödel: `and = min`, `or = max`. The standard f-SPARQL default.
48    #[default]
49    Godel,
50    /// Product: `and = a·b`, `or = a + b − a·b`.
51    Product,
52    /// Łukasiewicz: `and = max(0, a+b−1)`, `or = min(1, a+b)`.
53    Lukasiewicz,
54}
55
56impl DegreeNorm {
57    /// Conjunction of two degrees (t-norm).
58    pub fn and(self, a: f64, b: f64) -> f64 {
59        let (a, b) = (a as f32, b as f32);
60        let r = match self {
61            DegreeNorm::Godel => t_norm_godel(a, b),
62            DegreeNorm::Product => t_norm_product(a, b),
63            DegreeNorm::Lukasiewicz => t_norm_lukasiewicz(a, b),
64        };
65        r as f64
66    }
67
68    /// Disjunction of two degrees (t-conorm).
69    pub fn or(self, a: f64, b: f64) -> f64 {
70        let (a, b) = (a as f32, b as f32);
71        let r = match self {
72            DegreeNorm::Godel => t_conorm_godel(a, b),
73            DegreeNorm::Product => t_conorm_product(a, b),
74            DegreeNorm::Lukasiewicz => t_conorm_lukasiewicz(a, b),
75        };
76        r as f64
77    }
78
79    /// Fuzzy complement (`1 − a`), shared with the modality layer.
80    pub fn not(self, a: f64) -> f64 {
81        fuzzy_not(a as f32) as f64
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn norm_dispatch_matches_modality_operators() {
91        assert!((DegreeNorm::Godel.and(0.3, 0.7) - 0.3).abs() < 1e-6);
92        assert!((DegreeNorm::Godel.or(0.3, 0.7) - 0.7).abs() < 1e-6);
93        assert!((DegreeNorm::Product.and(0.5, 0.5) - 0.25).abs() < 1e-6);
94        assert!((DegreeNorm::Lukasiewicz.and(0.4, 0.4) - 0.0).abs() < 1e-6);
95        assert!((DegreeNorm::Lukasiewicz.or(0.6, 0.6) - 1.0).abs() < 1e-6);
96        assert!((DegreeNorm::Godel.not(0.2) - 0.8).abs() < 1e-6);
97    }
98}