1use std::collections::{BTreeMap, HashSet};
29
30pub const REASON_RULESET: &str = "owl-rl-subset/v1";
35
36const RDF_TYPE: &str = "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>";
39const RDFS_SUBCLASS_OF: &str = "<http://www.w3.org/2000/01/rdf-schema#subClassOf>";
40const RDFS_SUBPROPERTY_OF: &str = "<http://www.w3.org/2000/01/rdf-schema#subPropertyOf>";
41const RDFS_DOMAIN: &str = "<http://www.w3.org/2000/01/rdf-schema#domain>";
42const RDFS_RANGE: &str = "<http://www.w3.org/2000/01/rdf-schema#range>";
43
44const OWL_INVERSE_OF: &str = "<http://www.w3.org/2002/07/owl#inverseOf>";
45const OWL_SYMMETRIC_PROPERTY: &str = "<http://www.w3.org/2002/07/owl#SymmetricProperty>";
46const OWL_TRANSITIVE_PROPERTY: &str = "<http://www.w3.org/2002/07/owl#TransitiveProperty>";
47const OWL_FUNCTIONAL_PROPERTY: &str = "<http://www.w3.org/2002/07/owl#FunctionalProperty>";
48const OWL_DISJOINT_WITH: &str = "<http://www.w3.org/2002/07/owl#disjointWith>";
49const OWL_SAME_AS: &str = "<http://www.w3.org/2002/07/owl#sameAs>";
50const OWL_DIFFERENT_FROM: &str = "<http://www.w3.org/2002/07/owl#differentFrom>";
51const OWL_NOTHING: &str = "<http://www.w3.org/2002/07/owl#Nothing>";
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Inconsistency {
56 pub kind: &'static str,
58 pub detail: String,
60}
61
62#[derive(Debug, Clone, Default)]
64#[must_use]
65pub struct Reasoning {
66 pub inferred: Vec<(String, String, String)>,
68 pub inconsistencies: Vec<Inconsistency>,
70}
71
72type Triple = (String, String, String);
73
74pub fn reason(base_triples: &[Triple]) -> Reasoning {
78 let base: HashSet<Triple> = base_triples.iter().cloned().collect();
81 let mut all: HashSet<Triple> = base.clone();
82
83 materialize(&mut all);
84
85 let mut inferred: Vec<Triple> = all.iter().filter(|t| !base.contains(*t)).cloned().collect();
86 inferred.sort();
87
88 let inconsistencies = detect_inconsistencies(&all);
89
90 Reasoning {
91 inferred,
92 inconsistencies,
93 }
94}
95
96fn materialize(all: &mut HashSet<Triple>) {
98 loop {
99 let snapshot: Vec<Triple> = all.iter().cloned().collect();
102 let mut new: Vec<Triple> = Vec::new();
103
104 let subclass: Vec<(&str, &str)> = snapshot
106 .iter()
107 .filter(|(_, p, _)| p == RDFS_SUBCLASS_OF)
108 .map(|(s, _, o)| (s.as_str(), o.as_str()))
109 .collect();
110 let subprop: Vec<(&str, &str)> = snapshot
111 .iter()
112 .filter(|(_, p, _)| p == RDFS_SUBPROPERTY_OF)
113 .map(|(s, _, o)| (s.as_str(), o.as_str()))
114 .collect();
115 let domains: Vec<(&str, &str)> = snapshot
116 .iter()
117 .filter(|(_, p, _)| p == RDFS_DOMAIN)
118 .map(|(s, _, o)| (s.as_str(), o.as_str()))
119 .collect();
120 let ranges: Vec<(&str, &str)> = snapshot
121 .iter()
122 .filter(|(_, p, _)| p == RDFS_RANGE)
123 .map(|(s, _, o)| (s.as_str(), o.as_str()))
124 .collect();
125 let inverses: Vec<(&str, &str)> = snapshot
126 .iter()
127 .filter(|(_, p, _)| p == OWL_INVERSE_OF)
128 .map(|(s, _, o)| (s.as_str(), o.as_str()))
129 .collect();
130 let symmetric: HashSet<&str> = snapshot
131 .iter()
132 .filter(|(_, p, o)| p == RDF_TYPE && o == OWL_SYMMETRIC_PROPERTY)
133 .map(|(s, _, _)| s.as_str())
134 .collect();
135 let transitive: HashSet<&str> = snapshot
136 .iter()
137 .filter(|(_, p, o)| p == RDF_TYPE && o == OWL_TRANSITIVE_PROPERTY)
138 .map(|(s, _, _)| s.as_str())
139 .collect();
140
141 let emit = |s: &str, p: &str, o: &str, new: &mut Vec<Triple>| {
142 let t = (s.to_string(), p.to_string(), o.to_string());
143 if !all.contains(&t) {
144 new.push(t);
145 }
146 };
147
148 for (s, p, o) in &snapshot {
149 let (s, p, o) = (s.as_str(), p.as_str(), o.as_str());
150
151 if p == RDFS_SUBCLASS_OF {
153 for (d2, e) in &subclass {
154 if *d2 == o {
155 emit(s, RDFS_SUBCLASS_OF, e, &mut new);
156 }
157 }
158 }
159
160 if p == RDFS_SUBPROPERTY_OF {
162 for (q2, r) in &subprop {
163 if *q2 == o {
164 emit(s, RDFS_SUBPROPERTY_OF, r, &mut new);
165 }
166 }
167 }
168
169 if p == RDF_TYPE {
170 for (c, d) in &subclass {
172 if *c == o {
173 emit(s, RDF_TYPE, d, &mut new);
174 }
175 }
176 } else {
177 for (p2, q) in &subprop {
179 if *p2 == p {
180 emit(s, q, o, &mut new);
181 }
182 }
183 for (pr, c) in &domains {
185 if *pr == p {
186 emit(s, RDF_TYPE, c, &mut new);
187 }
188 }
189 for (pr, c) in &ranges {
191 if *pr == p {
192 emit(o, RDF_TYPE, c, &mut new);
193 }
194 }
195 for (a, b) in &inverses {
197 if *a == p {
198 emit(o, b, s, &mut new);
199 }
200 if *b == p {
201 emit(o, a, s, &mut new);
202 }
203 }
204 if symmetric.contains(p) {
206 emit(o, p, s, &mut new);
207 }
208 if transitive.contains(p) {
210 for (s2, p2, z) in &snapshot {
211 if p2 == p && s2 == o {
212 emit(s, p, z, &mut new);
213 }
214 }
215 }
216 }
217 }
218
219 if new.is_empty() {
220 break;
221 }
222 for t in new {
223 all.insert(t);
224 }
225 }
226}
227
228fn detect_inconsistencies(all: &HashSet<Triple>) -> Vec<Inconsistency> {
230 let mut out: Vec<Inconsistency> = Vec::new();
231
232 let types: Vec<(&str, &str)> = all
234 .iter()
235 .filter(|(_, p, _)| p == RDF_TYPE)
236 .map(|(s, _, o)| (s.as_str(), o.as_str()))
237 .collect();
238
239 let mut disjoint: HashSet<(&str, &str)> = HashSet::new();
241 for (s, p, o) in all {
242 if p == OWL_DISJOINT_WITH {
243 disjoint.insert((s.as_str(), o.as_str()));
244 disjoint.insert((o.as_str(), s.as_str()));
245 }
246 }
247
248 let mut same_as: HashSet<(&str, &str)> = HashSet::new();
250 let mut different_from: HashSet<(&str, &str)> = HashSet::new();
251 for (s, p, o) in all {
252 if p == OWL_SAME_AS {
253 same_as.insert((s.as_str(), o.as_str()));
254 same_as.insert((o.as_str(), s.as_str()));
255 } else if p == OWL_DIFFERENT_FROM {
256 different_from.insert((s.as_str(), o.as_str()));
257 different_from.insert((o.as_str(), s.as_str()));
258 }
259 }
260
261 let mut classes_of: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
266 for (x, c) in &types {
267 classes_of.entry(*x).or_default().push(*c);
268 }
269 let mut seen_disjoint: HashSet<(&str, &str, &str)> = HashSet::new();
270 for (x, cs) in &classes_of {
271 for i in 0..cs.len() {
272 for j in (i + 1)..cs.len() {
273 let (c, d) = (cs[i], cs[j]);
274 if c == d || !disjoint.contains(&(c, d)) {
275 continue;
276 }
277 let (lo, hi) = if c < d { (c, d) } else { (d, c) };
279 if seen_disjoint.insert((x, lo, hi)) {
280 out.push(Inconsistency {
281 kind: "disjoint-classes",
282 detail: format!(
283 "{x} is typed as both {lo} and {hi}, which are owl:disjointWith"
284 ),
285 });
286 }
287 }
288 }
289 }
290
291 let mut seen_same: HashSet<(&str, &str)> = HashSet::new();
293 for (x, y) in &same_as {
294 if different_from.contains(&(*x, *y)) {
295 let (lo, hi) = if x < y { (*x, *y) } else { (*y, *x) };
296 if seen_same.insert((lo, hi)) {
297 out.push(Inconsistency {
298 kind: "sameas-differentfrom",
299 detail: format!("{lo} and {hi} are both owl:sameAs and owl:differentFrom"),
300 });
301 }
302 }
303 }
304
305 let functional: HashSet<&str> = all
307 .iter()
308 .filter(|(_, p, o)| p == RDF_TYPE && o == OWL_FUNCTIONAL_PROPERTY)
309 .map(|(s, _, _)| s.as_str())
310 .collect();
311 if !functional.is_empty() {
312 let mut values_of: BTreeMap<(&str, &str), Vec<&str>> = BTreeMap::new();
316 for (s, p, o) in all {
317 if functional.contains(p.as_str()) {
318 values_of
319 .entry((s.as_str(), p.as_str()))
320 .or_default()
321 .push(o.as_str());
322 }
323 }
324 let mut seen_func: HashSet<(&str, &str, &str, &str)> = HashSet::new();
325 for ((s, p), vals) in &values_of {
326 for i in 0..vals.len() {
327 for j in (i + 1)..vals.len() {
328 let (o, o2) = (vals[i], vals[j]);
329 if o == o2 || same_as.contains(&(o, o2)) {
331 continue;
332 }
333 let (lo, hi) = if o < o2 { (o, o2) } else { (o2, o) };
334 if seen_func.insert((s, p, lo, hi)) {
335 out.push(Inconsistency {
336 kind: "functional-property",
337 detail: format!(
338 "functional property {p} on {s} has distinct values {lo} and {hi}"
339 ),
340 });
341 }
342 }
343 }
344 }
345 }
346
347 for (x, c) in &types {
349 if *c == OWL_NOTHING {
350 out.push(Inconsistency {
351 kind: "owl-nothing",
352 detail: format!("{x} is a member of owl:Nothing (the empty class)"),
353 });
354 }
355 }
356
357 out.sort_by(|a, b| (a.kind, &a.detail).cmp(&(b.kind, &b.detail)));
358 out
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 fn t(s: &str, p: &str, o: &str) -> Triple {
366 (s.to_string(), p.to_string(), o.to_string())
367 }
368
369 fn inferred_has(r: &Reasoning, s: &str, p: &str, o: &str) -> bool {
371 r.inferred.contains(&t(s, p, o))
372 }
373
374 #[test]
377 fn subclass_transitivity() {
378 let base = vec![
379 t("<c>", RDFS_SUBCLASS_OF, "<d>"),
380 t("<d>", RDFS_SUBCLASS_OF, "<e>"),
381 ];
382 let r = reason(&base);
383 assert!(inferred_has(&r, "<c>", RDFS_SUBCLASS_OF, "<e>"));
384 }
385
386 #[test]
387 fn type_propagation_over_subclass() {
388 let base = vec![t("<x>", RDF_TYPE, "<c>"), t("<c>", RDFS_SUBCLASS_OF, "<d>")];
389 let r = reason(&base);
390 assert!(inferred_has(&r, "<x>", RDF_TYPE, "<d>"));
391 }
392
393 #[test]
394 fn subproperty_propagation_and_transitivity() {
395 let base = vec![
396 t("<p>", RDFS_SUBPROPERTY_OF, "<q>"),
397 t("<q>", RDFS_SUBPROPERTY_OF, "<r>"),
398 t("<x>", "<p>", "<y>"),
399 ];
400 let r = reason(&base);
401 assert!(inferred_has(&r, "<p>", RDFS_SUBPROPERTY_OF, "<r>"));
403 assert!(inferred_has(&r, "<x>", "<q>", "<y>"));
404 assert!(inferred_has(&r, "<x>", "<r>", "<y>"));
405 }
406
407 #[test]
408 fn domain_typing() {
409 let base = vec![t("<p>", RDFS_DOMAIN, "<C>"), t("<x>", "<p>", "<y>")];
410 let r = reason(&base);
411 assert!(inferred_has(&r, "<x>", RDF_TYPE, "<C>"));
412 }
413
414 #[test]
415 fn range_typing() {
416 let base = vec![t("<p>", RDFS_RANGE, "<C>"), t("<x>", "<p>", "<y>")];
417 let r = reason(&base);
418 assert!(inferred_has(&r, "<y>", RDF_TYPE, "<C>"));
419 }
420
421 #[test]
422 fn inverse_of_both_directions() {
423 let base = vec![
424 t("<p>", OWL_INVERSE_OF, "<q>"),
425 t("<x>", "<p>", "<y>"),
426 t("<a>", "<q>", "<b>"),
427 ];
428 let r = reason(&base);
429 assert!(inferred_has(&r, "<y>", "<q>", "<x>"));
430 assert!(inferred_has(&r, "<b>", "<p>", "<a>"));
431 }
432
433 #[test]
434 fn symmetric_property() {
435 let base = vec![
436 t("<p>", RDF_TYPE, OWL_SYMMETRIC_PROPERTY),
437 t("<x>", "<p>", "<y>"),
438 ];
439 let r = reason(&base);
440 assert!(inferred_has(&r, "<y>", "<p>", "<x>"));
441 }
442
443 #[test]
444 fn transitive_property() {
445 let base = vec![
446 t("<p>", RDF_TYPE, OWL_TRANSITIVE_PROPERTY),
447 t("<x>", "<p>", "<y>"),
448 t("<y>", "<p>", "<z>"),
449 ];
450 let r = reason(&base);
451 assert!(inferred_has(&r, "<x>", "<p>", "<z>"));
452 }
453
454 #[test]
457 fn disjoint_classes_detected() {
458 let base = vec![
459 t("<C>", OWL_DISJOINT_WITH, "<D>"),
460 t("<x>", RDF_TYPE, "<C>"),
461 t("<x>", RDF_TYPE, "<D>"),
462 ];
463 let r = reason(&base);
464 assert!(r
465 .inconsistencies
466 .iter()
467 .any(|i| i.kind == "disjoint-classes"));
468 }
469
470 #[test]
471 fn disjoint_classes_via_subclass_propagation() {
472 let base = vec![
475 t("<C>", RDFS_SUBCLASS_OF, "<D>"),
476 t("<D>", OWL_DISJOINT_WITH, "<E>"),
477 t("<x>", RDF_TYPE, "<C>"),
478 t("<x>", RDF_TYPE, "<E>"),
479 ];
480 let r = reason(&base);
481 assert!(
482 r.inconsistencies
483 .iter()
484 .any(|i| i.kind == "disjoint-classes"),
485 "expected a disjoint-classes clash exposed by subClassOf propagation"
486 );
487 }
488
489 #[test]
490 fn sameas_differentfrom_detected() {
491 let base = vec![
492 t("<x>", OWL_SAME_AS, "<y>"),
493 t("<y>", OWL_DIFFERENT_FROM, "<x>"),
494 ];
495 let r = reason(&base);
496 assert!(r
497 .inconsistencies
498 .iter()
499 .any(|i| i.kind == "sameas-differentfrom"));
500 }
501
502 #[test]
503 fn functional_property_clash_detected() {
504 let base = vec![
505 t("<p>", RDF_TYPE, OWL_FUNCTIONAL_PROPERTY),
506 t("<x>", "<p>", "<y>"),
507 t("<x>", "<p>", "<z>"),
508 ];
509 let r = reason(&base);
510 assert!(r
511 .inconsistencies
512 .iter()
513 .any(|i| i.kind == "functional-property"));
514 }
515
516 #[test]
517 fn functional_property_sameas_is_coherent() {
518 let base = vec![
520 t("<p>", RDF_TYPE, OWL_FUNCTIONAL_PROPERTY),
521 t("<x>", "<p>", "<y>"),
522 t("<x>", "<p>", "<z>"),
523 t("<y>", OWL_SAME_AS, "<z>"),
524 ];
525 let r = reason(&base);
526 assert!(!r
527 .inconsistencies
528 .iter()
529 .any(|i| i.kind == "functional-property"));
530 }
531
532 #[test]
533 fn owl_nothing_detected() {
534 let base = vec![t("<x>", RDF_TYPE, OWL_NOTHING)];
535 let r = reason(&base);
536 assert!(r.inconsistencies.iter().any(|i| i.kind == "owl-nothing"));
537 }
538
539 #[test]
540 fn coherent_graph_has_no_inconsistencies() {
541 let base = vec![
542 t("<C>", RDFS_SUBCLASS_OF, "<D>"),
543 t("<x>", RDF_TYPE, "<C>"),
544 t("<p>", RDF_TYPE, OWL_TRANSITIVE_PROPERTY),
545 t("<x>", "<p>", "<y>"),
546 t("<y>", "<p>", "<z>"),
547 ];
548 let r = reason(&base);
549 assert!(
550 r.inconsistencies.is_empty(),
551 "expected coherent graph, got {:?}",
552 r.inconsistencies
553 );
554 assert!(!r.inferred.is_empty());
556 }
557
558 #[test]
559 fn inferred_excludes_base_triples() {
560 let base = vec![t("<x>", RDF_TYPE, "<c>"), t("<c>", RDFS_SUBCLASS_OF, "<d>")];
561 let r = reason(&base);
562 for b in &base {
564 assert!(!r.inferred.contains(b));
565 }
566 assert!(inferred_has(&r, "<x>", RDF_TYPE, "<d>"));
567 }
568
569 #[test]
570 fn end_to_end_small_ontology() {
571 let cause = "<http://ex/Cause>";
574 let event = "<http://ex/Event>";
575 let causes = "<http://ex/causes>";
576 let healthy = "<http://ex/Healthy>";
577 let sick = "<http://ex/Sick>";
578 let base = vec![
579 t(cause, RDFS_SUBCLASS_OF, event),
580 t(causes, RDF_TYPE, OWL_TRANSITIVE_PROPERTY),
581 t(healthy, OWL_DISJOINT_WITH, sick),
582 t("<http://ex/a>", RDF_TYPE, cause),
583 t("<http://ex/a>", causes, "<http://ex/b>"),
584 t("<http://ex/b>", causes, "<http://ex/c>"),
585 t("<http://ex/p>", RDF_TYPE, healthy),
586 t("<http://ex/p>", RDF_TYPE, sick),
587 ];
588 let r = reason(&base);
589 assert!(inferred_has(&r, "<http://ex/a>", RDF_TYPE, event));
591 assert!(inferred_has(&r, "<http://ex/a>", causes, "<http://ex/c>"));
592 assert!(r
594 .inconsistencies
595 .iter()
596 .any(|i| i.kind == "disjoint-classes" && i.detail.contains("http://ex/p")));
597 }
598}