Skip to main content

synapse_core/
reasoner.rs

1use anyhow::Result;
2use oxigraph::model::{GraphName, NamedNode, Quad, Subject, Term};
3use oxigraph::store::Store;
4
5#[derive(Debug, PartialEq, Eq, Hash, Clone)]
6pub enum ReasoningStrategy {
7    None,
8    RDFS,
9    OWLRL,
10}
11
12pub struct SynapseReasoner {
13    pub strategy: ReasoningStrategy,
14}
15
16impl SynapseReasoner {
17    pub fn new(strategy: ReasoningStrategy) -> Self {
18        Self { strategy }
19    }
20
21    /// Apply reasoning to a store and return inferred triples (without inserting)
22    pub fn apply(&self, store: &Store) -> Result<Vec<(String, String, String)>> {
23        let mut inferred = Vec::new();
24
25        match self.strategy {
26            ReasoningStrategy::None => {}
27            ReasoningStrategy::RDFS => {
28                // RDFS: SubClassOf Transitivity
29                // If A subClassOf B, and B subClassOf C -> A subClassOf C
30                let subclass_prop =
31                    NamedNode::new("http://www.w3.org/2000/01/rdf-schema#subClassOf")?;
32
33                for q1 in store
34                    .quads_for_pattern(None, Some(subclass_prop.as_ref()), None, None)
35                    .flatten()
36                {
37                    if let Subject::NamedNode(a) = q1.subject {
38                        if let Term::NamedNode(b) = q1.object {
39                            for q2 in store
40                                .quads_for_pattern(
41                                    Some(b.as_ref().into()),
42                                    Some(subclass_prop.as_ref()),
43                                    None,
44                                    None,
45                                )
46                                .flatten()
47                            {
48                                if let Term::NamedNode(c) = q2.object {
49                                    inferred.push((
50                                        a.as_str().to_string(),
51                                        subclass_prop.as_str().to_string(),
52                                        c.as_str().to_string(),
53                                    ));
54                                }
55                            }
56                        }
57                    }
58                }
59            }
60            ReasoningStrategy::OWLRL => {
61                // OWL-RL: TransitiveProperty
62                // If p is TransitiveProperty, and x p y, y p z -> x p z
63                let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
64                let transitive_class =
65                    NamedNode::new("http://www.w3.org/2002/07/owl#TransitiveProperty")?;
66
67                // Find all transitive properties
68                for q in store
69                    .quads_for_pattern(
70                        None,
71                        Some(type_prop.as_ref()),
72                        Some(transitive_class.as_ref().into()),
73                        None,
74                    )
75                    .flatten()
76                {
77                    if let Subject::NamedNode(p_node) = q.subject {
78                        let p_ref = p_node.as_ref();
79
80                        // Naive transitive: x p y ("xy")
81                        for xy_quad in store
82                            .quads_for_pattern(None, Some(p_ref), None, None)
83                            .flatten()
84                        {
85                            if let Subject::NamedNode(x) = xy_quad.subject {
86                                if let Term::NamedNode(y) = xy_quad.object {
87                                    // Find y p z ("yz")
88                                    for yz_quad in store
89                                        .quads_for_pattern(
90                                            Some(y.as_ref().into()),
91                                            Some(p_ref),
92                                            None,
93                                            None,
94                                        )
95                                        .flatten()
96                                    {
97                                        if let Term::NamedNode(z) = yz_quad.object {
98                                            inferred.push((
99                                                x.as_str().to_string(),
100                                                p_node.as_str().to_string(),
101                                                z.as_str().to_string(),
102                                            ));
103                                        }
104                                    }
105                                }
106                            }
107                        }
108                    }
109                }
110
111                // OWL-RL: SymmetricProperty
112                // If p is SymmetricProperty, and x p y -> y p x
113                let symmetric_class =
114                    NamedNode::new("http://www.w3.org/2002/07/owl#SymmetricProperty")?;
115
116                for q in store
117                    .quads_for_pattern(
118                        None,
119                        Some(type_prop.as_ref()),
120                        Some(symmetric_class.as_ref().into()),
121                        None,
122                    )
123                    .flatten()
124                {
125                    if let Subject::NamedNode(p_node) = q.subject {
126                        let p_ref = p_node.as_ref();
127
128                        for e in store
129                            .quads_for_pattern(None, Some(p_ref), None, None)
130                            .flatten()
131                        {
132                            // Infer: y p x
133                            if let Subject::NamedNode(s_node) = e.subject {
134                                if let Term::NamedNode(obj_node) = e.object {
135                                    inferred.push((
136                                        obj_node.as_str().to_string(),
137                                        p_node.as_str().to_string(),
138                                        s_node.as_str().to_string(),
139                                    ));
140                                }
141                            }
142                        }
143                    }
144                }
145
146                // OWL-RL: inverseOf
147                // If p1 inverseOf p2, and x p1 y -> y p2 x
148                let inverse_prop = NamedNode::new("http://www.w3.org/2002/07/owl#inverseOf")?;
149
150                for q in store
151                    .quads_for_pattern(None, Some(inverse_prop.as_ref()), None, None)
152                    .flatten()
153                {
154                    if let Subject::NamedNode(p1_node) = q.subject {
155                        let p1_ref = p1_node.as_ref();
156                        if let Term::NamedNode(p2_node) = q.object {
157                            // p1 inverseOf p2. For every x p1 y, infer y p2 x
158                            for e in store
159                                .quads_for_pattern(None, Some(p1_ref), None, None)
160                                .flatten()
161                            {
162                                if let Subject::NamedNode(x) = e.subject {
163                                    if let Term::NamedNode(y) = e.object {
164                                        inferred.push((
165                                            y.as_str().to_string(),
166                                            p2_node.as_str().to_string(),
167                                            x.as_str().to_string(),
168                                        ));
169                                    }
170                                }
171                            }
172                        }
173                    }
174                }
175            }
176        }
177
178        Ok(inferred)
179    }
180
181    /// Apply reasoning and persist inferred triples
182    pub fn materialize(&self, store: &Store) -> Result<usize> {
183        let mut total_inferred = 0;
184
185        // Fixed-point iteration loop
186        loop {
187            let inferred = self.apply(store)?;
188            if inferred.is_empty() {
189                break;
190            }
191
192            let mut new_triples = 0;
193            for (s, p, o) in inferred {
194                let s_node = NamedNode::new(s)?;
195                let p_node = NamedNode::new(p)?;
196                let o_node = NamedNode::new(o)?;
197
198                let quad = Quad::new(s_node, p_node, o_node, GraphName::DefaultGraph);
199
200                // Only count if actually new
201                // Note: store.contains checks exact match including graph name.
202                // We insert into DefaultGraph.
203                if !store.contains(&quad)? {
204                    store.insert(&quad)?;
205                    new_triples += 1;
206                }
207            }
208
209            if new_triples == 0 {
210                break;
211            }
212            total_inferred += new_triples;
213        }
214
215        Ok(total_inferred)
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_rdfs_transitivity() -> Result<()> {
225        let store = Store::new()?;
226        let reasoner = SynapseReasoner::new(ReasoningStrategy::RDFS);
227
228        let a = NamedNode::new("http://example.org/A")?;
229        let b = NamedNode::new("http://example.org/B")?;
230        let c = NamedNode::new("http://example.org/C")?;
231        let sub_class_of = NamedNode::new("http://www.w3.org/2000/01/rdf-schema#subClassOf")?;
232
233        store.insert(&Quad::new(
234            a.clone(),
235            sub_class_of.clone(),
236            b.clone(),
237            GraphName::DefaultGraph,
238        ))?;
239        store.insert(&Quad::new(
240            b.clone(),
241            sub_class_of.clone(),
242            c.clone(),
243            GraphName::DefaultGraph,
244        ))?;
245
246        let inferred = reasoner.apply(&store)?;
247        assert!(!inferred.is_empty());
248        assert!(inferred.contains(&(
249            a.as_str().to_string(),
250            sub_class_of.as_str().to_string(),
251            c.as_str().to_string()
252        )));
253
254        Ok(())
255    }
256
257    #[test]
258    fn test_owl_transitive_property() -> Result<()> {
259        let store = Store::new()?;
260        let reasoner = SynapseReasoner::new(ReasoningStrategy::OWLRL);
261
262        let p = NamedNode::new("http://example.org/ancestorOf")?;
263        let type_prop = NamedNode::new("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?;
264        let trans_class = NamedNode::new("http://www.w3.org/2002/07/owl#TransitiveProperty")?;
265
266        // p a TransitiveProperty
267        store.insert(&Quad::new(
268            p.clone(),
269            type_prop,
270            trans_class,
271            GraphName::DefaultGraph,
272        ))?;
273
274        let x = NamedNode::new("http://example.org/grandparent")?;
275        let y = NamedNode::new("http://example.org/parent")?;
276        let z = NamedNode::new("http://example.org/child")?;
277
278        // x p y
279        store.insert(&Quad::new(
280            x.clone(),
281            p.clone(),
282            y.clone(),
283            GraphName::DefaultGraph,
284        ))?;
285        // y p z
286        store.insert(&Quad::new(
287            y.clone(),
288            p.clone(),
289            z.clone(),
290            GraphName::DefaultGraph,
291        ))?;
292
293        let inferred = reasoner.apply(&store)?;
294        assert!(inferred.contains(&(
295            x.as_str().to_string(),
296            p.as_str().to_string(),
297            z.as_str().to_string()
298        )));
299
300        Ok(())
301    }
302}