Skip to main content

owl_dl_core/
definitions.rs

1//! Definition extraction for lazy unfolding.
2//!
3//! A *definition* is an axiom of the shape `A ≡ C` where `A` is a named
4//! atomic class and `C` is a complex concept. The Phase 2 tableau treats
5//! these specially: `A` stays in node labels as an atom, and the body `C`
6//! is unfolded only when needed (a contradiction or a forced rule fires
7//! on `A`). That's "lazy unfolding".
8//!
9//! What this module extracts:
10//!
11//! - Exact `EquivalentClasses` axioms with two members, one atomic and
12//!   one complex, are picked up.
13//! - Multi-way `EquivalentClasses` and `SubClassOf`-only equivalences
14//!   are ignored — Phase 1 absorption is the right place for the harder
15//!   cases.
16//! - When multiple definitions disagree about a name, the first one wins
17//!   (silently). Merging by intersection is a Phase 4 refinement.
18//!
19//! Cycle handling is deferred to the tableau's blocking machinery.
20
21use crate::ConceptPool;
22use crate::ir::{ClassId, ConceptExpr, ConceptId};
23use crate::ontology::{Axiom, InternalOntology};
24
25/// Map from named-class id to its definition body, if it has one.
26#[derive(Debug, Default, Clone)]
27pub struct Definitions {
28    defs: Vec<Option<ConceptId>>,
29}
30
31impl Definitions {
32    /// Body of `c`'s definition, or `None` if `c` is primitive.
33    #[must_use]
34    pub fn body_of(&self, c: ClassId) -> Option<ConceptId> {
35        self.defs.get(c.index() as usize).copied().flatten()
36    }
37
38    /// Whether `c` has a definition body.
39    #[must_use]
40    pub fn is_defined(&self, c: ClassId) -> bool {
41        self.body_of(c).is_some()
42    }
43
44    /// Total number of named classes covered by this table (including
45    /// primitives without a definition).
46    #[must_use]
47    pub fn num_classes(&self) -> usize {
48        self.defs.len()
49    }
50
51    /// Count of names that have an attached definition body.
52    #[must_use]
53    pub fn num_defined(&self) -> usize {
54        self.defs.iter().filter(|o| o.is_some()).count()
55    }
56
57    /// Iterate over `(class, body)` pairs in class-id order.
58    pub fn iter(&self) -> impl Iterator<Item = (ClassId, ConceptId)> + '_ {
59        (0u32..)
60            .zip(self.defs.iter())
61            .filter_map(|(i, body)| body.map(|b| (ClassId::new(i), b)))
62    }
63}
64
65/// Walk an ontology's axioms and collect lazy-unfolding-eligible definitions.
66#[must_use]
67pub fn extract_definitions(ontology: &InternalOntology) -> Definitions {
68    let n = ontology.vocabulary.num_classes();
69    let mut defs: Vec<Option<ConceptId>> = vec![None; n];
70
71    for axiom in &ontology.axioms {
72        if let Axiom::EquivalentClasses(ids) = axiom
73            && ids.len() == 2
74            && let Some((name, body)) = pick_def_pair(ids[0], ids[1], &ontology.concepts)
75        {
76            let idx = name.index() as usize;
77            if defs[idx].is_none() {
78                defs[idx] = Some(body);
79            }
80            // First-definition-wins; subsequent disagreements are dropped.
81        }
82    }
83
84    Definitions { defs }
85}
86
87/// Given two `ConceptId`s, return `(named, body)` if exactly one side is atomic.
88fn pick_def_pair(a: ConceptId, b: ConceptId, pool: &ConceptPool) -> Option<(ClassId, ConceptId)> {
89    let a_atom = match pool.get(a) {
90        ConceptExpr::Atomic(c) => Some(*c),
91        _ => None,
92    };
93    let b_atom = match pool.get(b) {
94        ConceptExpr::Atomic(c) => Some(*c),
95        _ => None,
96    };
97    match (a_atom, b_atom) {
98        (Some(name), None) => Some((name, b)),
99        (None, Some(name)) => Some((name, a)),
100        _ => None,
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    #![allow(clippy::many_single_char_names)]
107
108    use super::*;
109    use crate::ir::{Role, RoleId};
110
111    fn ontology_with(classes: &[&str]) -> InternalOntology {
112        let mut o = InternalOntology::new();
113        for c in classes {
114            o.vocabulary.intern_class(c);
115        }
116        o
117    }
118
119    fn cid(o: &InternalOntology, name: &str) -> ClassId {
120        o.vocabulary.class_id(name).expect("class missing")
121    }
122
123    fn atom(o: &mut InternalOntology, name: &str) -> ConceptId {
124        let c = cid(o, name);
125        o.concepts.atomic(c)
126    }
127
128    #[test]
129    fn empty_ontology_has_no_definitions() {
130        let o = ontology_with(&["A", "B"]);
131        let d = extract_definitions(&o);
132        assert_eq!(d.num_classes(), 2);
133        assert_eq!(d.num_defined(), 0);
134        assert!(!d.is_defined(cid(&o, "A")));
135    }
136
137    #[test]
138    fn equiv_atomic_to_complex_creates_definition() {
139        // A ≡ ∃R.B
140        let mut o = ontology_with(&["A", "B"]);
141        let a = atom(&mut o, "A");
142        let b = atom(&mut o, "B");
143        let r = Role::named(RoleId::new(0));
144        let some_b = o.concepts.some(r, b);
145        o.axioms.push(Axiom::EquivalentClasses(vec![a, some_b]));
146        let d = extract_definitions(&o);
147        let a_id = cid(&o, "A");
148        assert!(d.is_defined(a_id));
149        assert_eq!(d.body_of(a_id), Some(some_b));
150        // B is not defined — it's primitive.
151        assert!(!d.is_defined(cid(&o, "B")));
152    }
153
154    #[test]
155    fn equiv_complex_to_atomic_creates_definition_with_swap() {
156        // Same as above but operand order flipped.
157        let mut o = ontology_with(&["A", "B"]);
158        let a = atom(&mut o, "A");
159        let b = atom(&mut o, "B");
160        let r = Role::named(RoleId::new(0));
161        let some_b = o.concepts.some(r, b);
162        o.axioms.push(Axiom::EquivalentClasses(vec![some_b, a]));
163        let d = extract_definitions(&o);
164        assert_eq!(d.body_of(cid(&o, "A")), Some(some_b));
165    }
166
167    #[test]
168    fn two_atomic_members_is_not_a_definition() {
169        // A ≡ B  — both atomic, told-subsumer territory, not lazy unfolding.
170        let mut o = ontology_with(&["A", "B"]);
171        let a = atom(&mut o, "A");
172        let b = atom(&mut o, "B");
173        o.axioms.push(Axiom::EquivalentClasses(vec![a, b]));
174        let d = extract_definitions(&o);
175        assert_eq!(d.num_defined(), 0);
176    }
177
178    #[test]
179    fn three_or_more_members_skipped() {
180        let mut o = ontology_with(&["A", "B"]);
181        let a = atom(&mut o, "A");
182        let b = atom(&mut o, "B");
183        let r = Role::named(RoleId::new(0));
184        let some_b = o.concepts.some(r, b);
185        o.axioms.push(Axiom::EquivalentClasses(vec![a, b, some_b]));
186        let d = extract_definitions(&o);
187        assert_eq!(d.num_defined(), 0);
188    }
189
190    #[test]
191    fn two_complex_members_skipped() {
192        let mut o = ontology_with(&["A", "B"]);
193        let a = atom(&mut o, "A");
194        let b = atom(&mut o, "B");
195        let r = Role::named(RoleId::new(0));
196        let some_b = o.concepts.some(r, b);
197        let not_a = o.concepts.not(a);
198        o.axioms.push(Axiom::EquivalentClasses(vec![some_b, not_a]));
199        let d = extract_definitions(&o);
200        assert_eq!(d.num_defined(), 0);
201    }
202
203    #[test]
204    fn first_definition_wins_when_conflicts() {
205        // A ≡ ∃R.B  and  A ≡ ∃R.C  — keep first.
206        let mut o = ontology_with(&["A", "B", "C"]);
207        let a = atom(&mut o, "A");
208        let b = atom(&mut o, "B");
209        let cc = atom(&mut o, "C");
210        let r = Role::named(RoleId::new(0));
211        let some_b = o.concepts.some(r, b);
212        let some_c = o.concepts.some(r, cc);
213        o.axioms.push(Axiom::EquivalentClasses(vec![a, some_b]));
214        o.axioms.push(Axiom::EquivalentClasses(vec![a, some_c]));
215        let d = extract_definitions(&o);
216        assert_eq!(d.body_of(cid(&o, "A")), Some(some_b));
217    }
218
219    #[test]
220    fn idempotent_when_same_def_appears_twice() {
221        let mut o = ontology_with(&["A", "B"]);
222        let a = atom(&mut o, "A");
223        let b = atom(&mut o, "B");
224        let r = Role::named(RoleId::new(0));
225        let some_b = o.concepts.some(r, b);
226        o.axioms.push(Axiom::EquivalentClasses(vec![a, some_b]));
227        o.axioms.push(Axiom::EquivalentClasses(vec![a, some_b]));
228        let d = extract_definitions(&o);
229        assert_eq!(d.num_defined(), 1);
230    }
231
232    #[test]
233    fn multiple_distinct_class_definitions() {
234        // A ≡ ∃R.X, B ≡ ∃S.Y
235        let mut o = ontology_with(&["A", "B", "X", "Y"]);
236        let a = atom(&mut o, "A");
237        let b = atom(&mut o, "B");
238        let x = atom(&mut o, "X");
239        let y = atom(&mut o, "Y");
240        let r = Role::named(RoleId::new(0));
241        let s = Role::named(RoleId::new(1));
242        let some_x = o.concepts.some(r, x);
243        let some_y = o.concepts.some(s, y);
244        o.axioms.push(Axiom::EquivalentClasses(vec![a, some_x]));
245        o.axioms.push(Axiom::EquivalentClasses(vec![b, some_y]));
246        let d = extract_definitions(&o);
247        assert_eq!(d.num_defined(), 2);
248        let pairs: Vec<(ClassId, ConceptId)> = d.iter().collect();
249        assert_eq!(pairs, vec![(cid(&o, "A"), some_x), (cid(&o, "B"), some_y)]);
250    }
251
252    #[test]
253    fn sub_class_only_is_not_a_definition() {
254        // A ⊑ ∃R.B (not equivalence) — not a definition.
255        let mut o = ontology_with(&["A", "B"]);
256        let a = atom(&mut o, "A");
257        let b = atom(&mut o, "B");
258        let r = Role::named(RoleId::new(0));
259        let some_b = o.concepts.some(r, b);
260        o.axioms.push(Axiom::SubClassOf {
261            sub: a,
262            sup: some_b,
263        });
264        let d = extract_definitions(&o);
265        assert_eq!(d.num_defined(), 0);
266    }
267}