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}