qualia_core_db/modalities/fuzzy_rdf_schema.rs
1//! Fuzzy RDF schema — graded entailment (Ma, Li & Ma ch 3.3). RDFS reasoning with
2//! a degree: `subClassOf`/`type` hold to a degree in `[0,1]`, and degrees compose
3//! along the class hierarchy by a t-norm.
4//!
5//! Mission fit: "this guardianship relation is 0.6 a `MedicalProxy`" is a *degree of
6//! role-holding*; propagating it through the hierarchy with a t-norm reasons about
7//! **partial agency** without faking a crisp claim. Reuses the existing fuzzy
8//! operators ([`crate::modalities::fuzzy`]); kernel-class `Reduction`.
9
10use crate::modalities::fuzzy::{t_conorm_godel, t_norm_product};
11
12/// Graded transitive closure of `subClassOf`. Input edges `(sub, super, degree)`
13/// over `n` classes. The closure degree that `a ⊑ c` is the **best (t-conorm/max)
14/// over all paths** of the **t-norm (product) along each path**. Diagonal is `1.0`
15/// (every class is a subclass of itself with full degree). Returns the `n×n`
16/// row-major degree matrix.
17pub fn subclass_closure(n: usize, edges: &[(usize, usize, f64)]) -> Vec<f64> {
18 let mut m = vec![0.0f32; n * n];
19 for i in 0..n {
20 m[i * n + i] = 1.0;
21 }
22 for &(a, b, d) in edges {
23 if a < n && b < n {
24 let dd = d.clamp(0.0, 1.0) as f32;
25 // Keep the strongest direct assertion.
26 if dd > m[a * n + b] {
27 m[a * n + b] = dd;
28 }
29 }
30 }
31 // Fuzzy transitive closure (Floyd-Warshall with product t-norm + max t-conorm).
32 for k in 0..n {
33 for i in 0..n {
34 let ik = m[i * n + k];
35 if ik == 0.0 {
36 continue;
37 }
38 for j in 0..n {
39 let via = t_norm_product(ik, m[k * n + j]);
40 let cur = m[i * n + j];
41 m[i * n + j] = t_conorm_godel(cur, via);
42 }
43 }
44 }
45 m.iter().map(|&v| v as f64).collect()
46}
47
48/// Degree to which an instance is of class `c`, given it is of class `a` with degree
49/// `type_degree` and `a ⊑ c` with `subclass_degree`: `t-norm(type, subclass)`.
50pub fn type_entailment(type_degree: f64, subclass_degree: f64) -> f64 {
51 t_norm_product(
52 type_degree.clamp(0.0, 1.0) as f32,
53 subclass_degree.clamp(0.0, 1.0) as f32,
54 ) as f64
55}
56
57/// Convenience: given the closure matrix and an instance's direct `type` degrees per
58/// class (`type_of[c]`), the entailed degree of membership in class `c` =
59/// `max_a t-norm(type_of[a], closure[a][c])`. Returns the entailed type degrees.
60pub fn entailed_types(n: usize, closure: &[f64], type_of: &[f64]) -> Vec<f64> {
61 let mut out = vec![0.0; n];
62 for c in 0..n {
63 let mut best = 0.0f64;
64 for a in 0..n {
65 if type_of[a] > 0.0 {
66 let d = type_entailment(type_of[a], closure[a * n + c]);
67 if d > best {
68 best = d;
69 }
70 }
71 }
72 out[c] = best;
73 }
74 out
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 const EPS: f64 = 1e-6;
81
82 #[test]
83 fn transitive_degree_composes_by_product() {
84 // A ⊑ B (0.8), B ⊑ C (0.7) → A ⊑ C = 0.8·0.7 = 0.56.
85 let m = subclass_closure(3, &[(0, 1, 0.8), (1, 2, 0.7)]);
86 assert!((m[0 * 3 + 1] - 0.8).abs() < EPS);
87 assert!((m[1 * 3 + 2] - 0.7).abs() < EPS);
88 assert!((m[0 * 3 + 2] - 0.56).abs() < EPS, "A⊑C {}", m[0 * 3 + 2]);
89 // Reflexive.
90 assert!((m[0 * 3 + 0] - 1.0).abs() < EPS);
91 }
92
93 #[test]
94 fn best_path_wins() {
95 // Two paths A→C: A→B→C (0.9·0.5=0.45) and A→D→C (0.6·0.9=0.54). Max = 0.54.
96 let edges = [(0, 1, 0.9), (1, 2, 0.5), (0, 3, 0.6), (3, 2, 0.9)];
97 let m = subclass_closure(4, &edges);
98 assert!(
99 (m[0 * 4 + 2] - 0.54).abs() < EPS,
100 "best path {}",
101 m[0 * 4 + 2]
102 );
103 }
104
105 #[test]
106 fn type_entailment_propagates() {
107 // type(x, A)=0.6, A ⊑ MedicalProxy(=class 1) with degree 0.8 → 0.48.
108 let m = subclass_closure(2, &[(0, 1, 0.8)]);
109 let types = entailed_types(2, &m, &[0.6, 0.0]);
110 assert!((types[1] - 0.48).abs() < EPS, "entailed {}", types[1]);
111 // x is still fully of its own class A.
112 assert!((types[0] - 0.6).abs() < EPS);
113 }
114
115 #[test]
116 fn type_entailment_pairwise() {
117 assert!((type_entailment(0.5, 0.4) - 0.2).abs() < EPS);
118 }
119}