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" => ruleset.subproperty_transitivity = true,
56                "domain_range" | "dr" => ruleset.domain_range = true,
57                "inverse" | "inverse_of" => ruleset.inverse_of = true,
58                "symmetric" | "symmetric_property" => ruleset.symmetric_property = true,
59                "transitive" | "transitive_property" => ruleset.transitive_property = true,
60                "rdfs" => ruleset = Self::rdfs(),
61                "owlrl" | "owl-rl" => ruleset = Self::owlrl(),
62                _ => {}
63            }
64        }
65        ruleset
66    }
67}
68
69/// Reasoner for deriving implicit knowledge from RDF triples
70pub struct SynapseReasoner {
71    strategy: ReasoningStrategy,
72    rules: RuleSet,
73}
74
75impl SynapseReasoner {
76    pub fn new(strategy: ReasoningStrategy) -> Self {
77        let rules = match strategy {
78            ReasoningStrategy::RDFS => RuleSet::rdfs(),
79            ReasoningStrategy::OWLRL => RuleSet::owlrl(),
80            ReasoningStrategy::None => RuleSet::default(),
81        };
82        Self { strategy, rules }
83    }
84
85    pub fn with_rules(strategy: ReasoningStrategy, rules: RuleSet) -> Self {
86        Self { strategy, rules }
87    }
88
89    /// Get current rule configuration
90    pub fn rules(&self) -> &RuleSet {
91        &self.rules
92    }
93
94    /// Apply reasoning to the store and return inferred triples
95    pub fn apply(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
96        match self.strategy {
97            ReasoningStrategy::None => Ok(Vec::new()),
98            ReasoningStrategy::RDFS => self.apply_rdfs_reasoning(store),
99            ReasoningStrategy::OWLRL => self.apply_owl_reasoning(store),
100        }
101    }
102
103    fn apply_rdfs_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
104        let mut inferred = Vec::new();
105        let sub_class_of = NamedNode::new("http://www.w3.org/2000/01/rdf-schema#subClassOf")?;
106        
107        for quad in store.iter() {
108            if let Ok(q) = quad {
109                if q.predicate == sub_class_of {
110                    // q is (B subClassOf C)
111                    // Find all A such that (A subClassOf B)
112                    let subject_b = q.subject.clone();
113                    if let Subject::NamedNode(subj_node) = subject_b {
114                        for inner_quad in store.iter() {
115                            if let Ok(iq) = inner_quad {
116                                if iq.predicate == sub_class_of && iq.object == subj_node.clone().into() {
117                                    // Transitivity: A subClassOf C
118                                    inferred.push((
119                                        iq.subject.to_string(),
120                                        sub_class_of.to_string(),
121                                        q.object.to_string(),
122                                    ));
123                                }
124                            }
125                        }
126                    }
127                }
128            }
129        }
130        
131        Ok(inferred)
132    }
133
134    fn apply_owl_reasoning(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
135        let mut inferred = Vec::new();
136        let rules = &self.rules;
137
138        // 1. Symmetric Property
139        if rules.symmetric_property {
140            let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
141            let symmetric_class = NamedNode::new("http://www.w3.org/2002/07/owl#SymmetricProperty")?;
142            
143            for quad in store.quads_for_pattern(None, Some(type_prop.as_ref().into()), Some(symmetric_class.as_ref().into()), None) {
144                if let Ok(q) = quad {
145                    // Check if subject is a NamedNode (properties must be named)
146                    if let Subject::NamedNode(p_node) = q.subject {
147                         let p_ref = p_node.as_ref();
148                         
149                         // Find all triples using p: x p y
150                         for edge in store.quads_for_pattern(None, Some(p_ref.into()), None, None) {
151                             if let Ok(e) = edge {
152                                 // Infer: y p x
153                                 if let Term::NamedNode(obj_node) = e.object {
154                                     let s_str = e.subject.to_string();
155                                     let p_str = p_node.to_string();
156                                     let o_str = obj_node.to_string();
157                                     inferred.push((o_str, p_str, s_str));
158                                 }
159                             }
160                         }
161                    }
162                }
163            }
164        }
165
166        // 2. Transitive Property
167        if rules.transitive_property {
168            let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
169            let transitive_class = NamedNode::new("http://www.w3.org/2002/07/owl#TransitiveProperty")?;
170
171            for quad in store.quads_for_pattern(None, Some(type_prop.as_ref().into()), Some(transitive_class.as_ref().into()), None) {
172                if let Ok(q) = quad {
173                     if let Subject::NamedNode(p_node) = q.subject {
174                         let p_ref = p_node.as_ref();
175                         
176                         // Naive transitive: x p y ("xy")
177                         for xy in store.quads_for_pattern(None, Some(p_ref.into()), None, None) {
178                             if let Ok(xy_quad) = xy {
179                                 if let Term::NamedNode(y) = xy_quad.object {
180                                     // Find y p z ("yz")
181                                     for yz in store.quads_for_pattern(Some(y.as_ref().into()), Some(p_ref.into()), None, None) {
182                                         if let Ok(yz_quad) = yz {
183                                             inferred.push((
184                                                 xy_quad.subject.to_string(),
185                                                 p_node.to_string(),
186                                                 yz_quad.object.to_string()
187                                             ));
188                                         }
189                                     }
190                                 }
191                             }
192                         }
193                     }
194                }
195            }
196        }
197
198        // 3. InverseOf
199        if rules.inverse_of {
200            let inverse_prop = NamedNode::new("http://www.w3.org/2002/07/owl#inverseOf")?;
201            
202            for quad in store.quads_for_pattern(None, Some(inverse_prop.as_ref().into()), None, None) {
203                if let Ok(q) = quad {
204                    if let Subject::NamedNode(p1_node) = q.subject {
205                        let p1_ref = p1_node.as_ref();
206                        if let Term::NamedNode(p2_node) = q.object {
207                            // p1 inverseOf p2. For every x p1 y, infer y p2 x
208                            for edge in store.quads_for_pattern(None, Some(p1_ref.into()), None, None) {
209                                if let Ok(e) = edge {
210                                    if let Term::NamedNode(y) = e.object {
211                                        inferred.push((
212                                            y.to_string(),
213                                            p2_node.to_string(),
214                                            e.subject.to_string()
215                                        ));
216                                    }
217                                }
218                            }
219                        }
220                    }
221                }
222            }
223        }
224
225        Ok(inferred)
226    }
227    pub fn materialize(&self, store: &Store) -> Result<usize> {
228        let inferred = self.apply(store)?;
229        let mut count = 0;
230        let mut skipped = 0;
231        
232        for (s, p, o) in inferred {
233            if let (Ok(subject), Ok(predicate), Ok(object)) = (
234                NamedNode::new(&s),
235                NamedNode::new(&p),
236                NamedNode::new(&o),
237            ) {
238                // Check if triple already exists to avoid duplicates
239                let quad = Quad::new(subject.clone(), predicate.clone(), object.clone(), GraphName::DefaultGraph);
240                if store.contains(&quad)? {
241                    skipped += 1;
242                    continue;
243                }
244                // Insert new inferred triple
245                let _ = store.insert(&quad);
246                count += 1;
247            }
248        }
249        
250        if skipped > 0 {
251            eprintln!("Reasoning: {} new triples, {} duplicates skipped", count, skipped);
252        }
253        
254        Ok(count)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use oxigraph::model::NamedNode;
262
263    #[test]
264    fn test_rdfs_transitivity() -> Result<()> {
265        let store = Store::new()?;
266        let sub_class_of = "http://www.w3.org/2000/01/rdf-schema#subClassOf";
267        
268        let a = NamedNode::new("http://example.org/A")?;
269        let b = NamedNode::new("http://example.org/B")?;
270        let c = NamedNode::new("http://example.org/C")?;
271        let pred = NamedNode::new(sub_class_of)?;
272        
273        // A subClassOf B, B subClassOf C
274        store.insert(&Quad::new(a.clone(), pred.clone(), b.clone(), GraphName::DefaultGraph))?;
275        store.insert(&Quad::new(b.clone(), pred.clone(), c.clone(), GraphName::DefaultGraph))?;
276        
277        let reasoner = SynapseReasoner::new(ReasoningStrategy::RDFS);
278        let inferred = reasoner.apply(&store)?;
279        
280        // Should infer A subClassOf C
281        let mut found = false;
282        let expected_s = a.to_string();
283        let expected_o = c.to_string();
284        
285        for (s, _p, o) in inferred {
286            if s == expected_s && o == expected_o {
287                found = true;
288                break;
289            }
290        }
291        
292        assert!(found, "Inferred A subClassOf C not found");
293        Ok(())
294    }
295
296    #[test]
297    fn test_owl_reasoning_smoke() -> Result<()> {
298        let store = Store::new()?;
299        let reasoner = SynapseReasoner::new(ReasoningStrategy::OWLRL);
300        
301        let inferred = reasoner.apply(&store)?;
302        // OWL Reasoning often has default axioms, so we just check it doesn't crash
303        // and returns at least something (usually standard RDF/OWL URIs)
304        println!("OWL Reasoner inferred {} default triples", inferred.len());
305        Ok(())
306    }
307}