Skip to main content

synapse_core/
reasoner.rs

1use anyhow::Result;
2use oxigraph::model::*;
3use oxigraph::store::Store;
4
5/// Reasoning strategy for knowledge graph inference
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ReasoningStrategy {
8    None,
9    RDFS,
10    OWLRL,
11}
12
13/// Selectable reasoning rules for fine-grained control
14#[derive(Debug, Clone, Default)]
15pub struct RuleSet {
16    pub subclass_transitivity: bool,
17    pub subproperty_transitivity: bool,
18    pub domain_range: bool,
19    pub inverse_of: bool,
20    pub symmetric_property: bool,
21    pub transitive_property: bool,
22}
23
24impl RuleSet {
25    /// All RDFS rules enabled
26    pub fn rdfs() -> Self {
27        Self {
28            subclass_transitivity: true,
29            subproperty_transitivity: true,
30            domain_range: true,
31            inverse_of: false,
32            symmetric_property: false,
33            transitive_property: false,
34        }
35    }
36
37    /// All OWL-RL rules enabled
38    pub fn owlrl() -> Self {
39        Self {
40            subclass_transitivity: true,
41            subproperty_transitivity: true,
42            domain_range: true,
43            inverse_of: true,
44            symmetric_property: true,
45            transitive_property: true,
46        }
47    }
48
49    /// Parse from comma-separated rule names
50    pub fn from_str(rules: &str) -> Self {
51        let mut ruleset = Self::default();
52        for rule in rules.split(',').map(|s| s.trim().to_lowercase()) {
53            match rule.as_str() {
54                "subclass" | "subclass_transitivity" => ruleset.subclass_transitivity = true,
55                "subproperty" | "subproperty_transitivity" => {
56                    ruleset.subproperty_transitivity = true
57                }
58                "domain_range" | "dr" => ruleset.domain_range = true,
59                "inverse" | "inverse_of" => ruleset.inverse_of = true,
60                "symmetric" | "symmetric_property" => ruleset.symmetric_property = true,
61                "transitive" | "transitive_property" => ruleset.transitive_property = true,
62                "rdfs" => ruleset = Self::rdfs(),
63                "owlrl" | "owl-rl" => ruleset = Self::owlrl(),
64                _ => {}
65            }
66        }
67        ruleset
68    }
69}
70
71/// Reasoner for deriving implicit knowledge from RDF triples
72pub struct SynapseReasoner {
73    strategy: ReasoningStrategy,
74    rules: RuleSet,
75}
76
77impl SynapseReasoner {
78    pub fn new(strategy: ReasoningStrategy) -> Self {
79        let rules = match strategy {
80            ReasoningStrategy::RDFS => RuleSet::rdfs(),
81            ReasoningStrategy::OWLRL => RuleSet::owlrl(),
82            ReasoningStrategy::None => RuleSet::default(),
83        };
84        Self { strategy, rules }
85    }
86
87    pub fn with_rules(strategy: ReasoningStrategy, rules: RuleSet) -> Self {
88        Self { strategy, rules }
89    }
90
91    /// Get current rule configuration
92    pub fn rules(&self) -> &RuleSet {
93        &self.rules
94    }
95
96    /// Apply reasoning to the store and return inferred triples
97    pub fn apply(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
98        match self.strategy {
99            ReasoningStrategy::None => Ok(Vec::new()),
100            ReasoningStrategy::RDFS => self.apply_rdfs_reasoning(store),
101            ReasoningStrategy::OWLRL => self.apply_owl_reasoning(store),
102        }
103    }
104
105    fn apply_rdfs_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
106        let mut inferred = Vec::new();
107        let sub_class_of = NamedNode::new("http://www.w3.org/2000/01/rdf-schema#subClassOf")?;
108
109        for quad in store.iter() {
110            if let Ok(q) = quad {
111                if q.predicate == sub_class_of {
112                    // q is (B subClassOf C)
113                    // Find all A such that (A subClassOf B)
114                    let subject_b = q.subject.clone();
115                    if let Subject::NamedNode(subj_node) = subject_b {
116                        for inner_quad in store.iter() {
117                            if let Ok(iq) = inner_quad {
118                                if iq.predicate == sub_class_of
119                                    && iq.object == subj_node.clone().into()
120                                {
121                                    // Transitivity: A subClassOf C
122                                    inferred.push((
123                                        iq.subject.to_string(),
124                                        sub_class_of.to_string(),
125                                        q.object.to_string(),
126                                    ));
127                                }
128                            }
129                        }
130                    }
131                }
132            }
133        }
134
135        Ok(inferred)
136    }
137
138    fn apply_owl_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
139        let mut inferred = Vec::new();
140        let rules = &self.rules;
141
142        // 1. Symmetric Property
143        if rules.symmetric_property {
144            let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
145            let symmetric_class =
146                NamedNode::new("http://www.w3.org/2002/07/owl#SymmetricProperty")?;
147
148            for quad in store.quads_for_pattern(
149                None,
150                Some(type_prop.as_ref()),
151                Some(symmetric_class.as_ref().into()),
152                None,
153            ) {
154                if let Ok(q) = quad {
155                    // Check if subject is a NamedNode (properties must be named)
156                    if let Subject::NamedNode(p_node) = q.subject {
157                        let p_ref = p_node.as_ref();
158
159                        // Find all triples using p: x p y
160                        for edge in store.quads_for_pattern(None, Some(p_ref), None, None) {
161                            if let Ok(e) = edge {
162                                // Infer: y p x
163                                if let Term::NamedNode(obj_node) = e.object {
164                                    let s_str = e.subject.to_string();
165                                    let p_str = p_node.to_string();
166                                    let o_str = obj_node.to_string();
167                                    inferred.push((o_str, p_str, s_str));
168                                }
169                            }
170                        }
171                    }
172                }
173            }
174        }
175
176        // 2. Transitive Property
177        if rules.transitive_property {
178            let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
179            let transitive_class =
180                NamedNode::new("http://www.w3.org/2002/07/owl#TransitiveProperty")?;
181
182            for quad in store.quads_for_pattern(
183                None,
184                Some(type_prop.as_ref()),
185                Some(transitive_class.as_ref().into()),
186                None,
187            ) {
188                if let Ok(q) = quad {
189                    if let Subject::NamedNode(p_node) = q.subject {
190                        let p_ref = p_node.as_ref();
191
192                        // Naive transitive: x p y ("xy")
193                        for xy in store.quads_for_pattern(None, Some(p_ref), None, None) {
194                            if let Ok(xy_quad) = xy {
195                                if let Term::NamedNode(y) = xy_quad.object {
196                                    // Find y p z ("yz")
197                                    for yz_quad in store
198                                        .quads_for_pattern(
199                                            Some(y.as_ref().into()),
200                                            Some(p_ref),
201                                            None,
202                                            None,
203                                        )
204                                        .flatten()
205                                    {
206                                        inferred.push((
207                                            xy_quad.subject.to_string(),
208                                            p_node.to_string(),
209                                            yz_quad.object.to_string(),
210                                        ));
211                                    }
212                                }
213                            }
214                        }
215                    }
216                }
217            }
218        }
219
220        // 3. InverseOf
221        if rules.inverse_of {
222            let inverse_prop = NamedNode::new("http://www.w3.org/2002/07/owl#inverseOf")?;
223
224            for quad in store.quads_for_pattern(None, Some(inverse_prop.as_ref()), None, None) {
225                if let Ok(q) = quad {
226                    if let Subject::NamedNode(p1_node) = q.subject {
227                        let p1_ref = p1_node.as_ref();
228                        if let Term::NamedNode(p2_node) = q.object {
229                            // p1 inverseOf p2. For every x p1 y, infer y p2 x
230                            for e in store
231                                .quads_for_pattern(None, Some(p1_ref), None, None)
232                                .flatten()
233                            {
234                                if let Term::NamedNode(y) = e.object {
235                                    inferred.push((
236                                        y.to_string(),
237                                        p2_node.to_string(),
238                                        e.subject.to_string(),
239                                    ));
240                                }
241                            }
242                        }
243                    }
244                }
245            }
246        }
247
248        Ok(inferred)
249    }
250    pub fn materialize(&self, store: &Store) -> Result<usize> {
251        let mut total_added = 0;
252
253        // Fixed point loop: repeat until no new triples are added
254        loop {
255            let inferred = self.apply(store)?;
256            let mut added_in_pass = 0;
257            let mut skipped = 0;
258
259            for (s, p, o) in inferred {
260                if let (Ok(subject), Ok(predicate), Ok(object)) =
261                    (NamedNode::new(&s), NamedNode::new(&p), NamedNode::new(&o))
262                {
263                    // Check if triple already exists to avoid duplicates
264                    let quad = Quad::new(
265                        subject.clone(),
266                        predicate.clone(),
267                        object.clone(),
268                        GraphName::DefaultGraph,
269                    );
270                    if store.contains(&quad)? {
271                        skipped += 1;
272                        continue;
273                    }
274                    // Insert new inferred triple
275                    let _ = store.insert(&quad);
276                    added_in_pass += 1;
277                }
278            }
279
280            if skipped > 0 {
281                eprintln!(
282                    "Reasoning pass: {} new triples, {} duplicates skipped",
283                    added_in_pass, skipped
284                );
285            }
286
287            if added_in_pass == 0 {
288                break;
289            }
290
291            total_added += added_in_pass;
292        }
293
294        Ok(total_added)
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301    use oxigraph::model::NamedNode;
302
303    #[test]
304    fn test_rdfs_transitivity() -> Result<()> {
305        let store = Store::new()?;
306        let sub_class_of = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
307
308        let a = NamedNode::new("http://example.org/A")?;
309        let b = NamedNode::new("http://example.org/B")?;
310        let c = NamedNode::new("http://example.org/C")?;
311        let pred = NamedNode::new(sub_class_of)?;
312
313        // A subClassOf B, B subClassOf C
314        store.insert(&Quad::new(
315            a.clone(),
316            pred.clone(),
317            b.clone(),
318            GraphName::DefaultGraph,
319        ))?;
320        store.insert(&Quad::new(
321            b.clone(),
322            pred.clone(),
323            c.clone(),
324            GraphName::DefaultGraph,
325        ))?;
326
327        let reasoner = SynapseReasoner::new(ReasoningStrategy::RDFS);
328        let inferred = reasoner.apply(&store)?;
329
330        // Should infer A subClassOf C
331        let mut found = false;
332        let expected_s = a.to_string();
333        let expected_o = c.to_string();
334
335        for (s, _p, o) in inferred {
336            if s == expected_s && o == expected_o {
337                found = true;
338                break;
339            }
340        }
341
342        assert!(found, "Inferred A subClassOf C not found");
343        Ok(())
344    }
345
346    #[test]
347    fn test_owl_reasoning_smoke() -> Result<()> {
348        let store = Store::new()?;
349        let reasoner = SynapseReasoner::new(ReasoningStrategy::OWLRL);
350
351        let inferred = reasoner.apply(&store)?;
352        // OWL Reasoning often has default axioms, so we just check it doesn't crash
353        // and returns at least something (usually standard RDF/OWL URIs)
354        println!("OWL Reasoner inferred {} default triples", inferred.len());
355        Ok(())
356    }
357}