Skip to main content

strixonomy_owl/
compare.rs

1//! Cross-format semantic comparator for v0.21 round-trip gates.
2
3use crate::bridge::{bridge_ontology, OwlBridgeResult};
4use horned_owl::model::{RcAnnotatedComponent, RcStr};
5use horned_owl::ontology::component_mapped::ComponentMappedOntology;
6use std::collections::{BTreeMap, BTreeSet};
7use strixonomy_core::AXIOM_KIND_SUB_CLASS_OF;
8
9/// Diff between two semantic snapshots (empty means equivalent for compared facets).
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
11pub struct SemanticDiff {
12    pub missing_entities: Vec<String>,
13    pub extra_entities: Vec<String>,
14    pub label_mismatches: Vec<String>,
15    pub parent_mismatches: Vec<String>,
16    pub import_mismatches: Vec<String>,
17    pub ontology_iri_mismatch: Option<(Option<String>, Option<String>)>,
18}
19
20impl SemanticDiff {
21    pub fn is_empty(&self) -> bool {
22        self.missing_entities.is_empty()
23            && self.extra_entities.is_empty()
24            && self.label_mismatches.is_empty()
25            && self.parent_mismatches.is_empty()
26            && self.import_mismatches.is_empty()
27            && self.ontology_iri_mismatch.is_none()
28    }
29}
30
31/// Compare two Horned ontologies for the v0.21 acceptance facets.
32pub fn compare_ontologies(
33    left: ComponentMappedOntology<RcStr, RcAnnotatedComponent>,
34    right: ComponentMappedOntology<RcStr, RcAnnotatedComponent>,
35) -> SemanticDiff {
36    let left_bridge = bridge_ontology(left, "left", "", &BTreeMap::new());
37    let right_bridge = bridge_ontology(right, "right", "", &BTreeMap::new());
38    compare_bridges(&left_bridge, &right_bridge)
39}
40
41pub fn compare_bridges(left: &OwlBridgeResult, right: &OwlBridgeResult) -> SemanticDiff {
42    let mut diff = SemanticDiff::default();
43
44    if left.base_iri != right.base_iri {
45        diff.ontology_iri_mismatch = Some((left.base_iri.clone(), right.base_iri.clone()));
46    }
47
48    let left_iris: BTreeSet<_> = left.entities.iter().map(|e| e.iri.clone()).collect();
49    let right_iris: BTreeSet<_> = right.entities.iter().map(|e| e.iri.clone()).collect();
50    for iri in left_iris.difference(&right_iris) {
51        diff.missing_entities.push(iri.clone());
52    }
53    for iri in right_iris.difference(&left_iris) {
54        diff.extra_entities.push(iri.clone());
55    }
56
57    let right_by_iri: BTreeMap<_, _> = right.entities.iter().map(|e| (e.iri.clone(), e)).collect();
58    for le in &left.entities {
59        if let Some(re) = right_by_iri.get(&le.iri) {
60            let mut ll = le.labels.clone();
61            let mut rl = re.labels.clone();
62            ll.sort();
63            rl.sort();
64            if ll != rl {
65                diff.label_mismatches.push(le.iri.clone());
66            }
67        }
68    }
69
70    let left_parents = parents_map(left);
71    let right_parents = parents_map(right);
72    let subjects: BTreeSet<_> = left_parents.keys().chain(right_parents.keys()).cloned().collect();
73    for subject in subjects {
74        let lp = left_parents.get(&subject).cloned().unwrap_or_default();
75        let rp = right_parents.get(&subject).cloned().unwrap_or_default();
76        if lp != rp {
77            diff.parent_mismatches.push(subject);
78        }
79    }
80
81    let left_imports: BTreeSet<_> = left.imports.iter().map(|i| i.import_iri.clone()).collect();
82    let right_imports: BTreeSet<_> = right.imports.iter().map(|i| i.import_iri.clone()).collect();
83    for iri in left_imports.symmetric_difference(&right_imports) {
84        diff.import_mismatches.push(iri.clone());
85    }
86
87    diff
88}
89
90fn parents_map(bridge: &OwlBridgeResult) -> BTreeMap<String, BTreeSet<String>> {
91    let mut map: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
92    for ax in &bridge.axioms {
93        if ax.axiom_kind == AXIOM_KIND_SUB_CLASS_OF {
94            map.entry(ax.subject.clone()).or_default().insert(ax.object.clone());
95        }
96    }
97    map
98}