Skip to main content

ontologos_el/
lib.rs

1//! OWL EL completion-based classification.
2//!
3//! # Example
4//!
5//! ```no_run
6//! use ontologos_el::ElClassifier;
7//! use ontologos_parser::load_ontology;
8//!
9//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
10//! let ontology = load_ontology(std::path::Path::new("ontology.owl"))?;
11//! let taxonomy = ElClassifier::new().classify(&ontology)?;
12//! println!("subsumptions: {}", taxonomy.subsumption_count());
13//! # Ok(())
14//! # }
15//! ```
16
17#![warn(missing_docs)]
18
19mod engine;
20mod graph;
21mod normal_form;
22mod partition;
23mod reasoner;
24mod session;
25mod taxonomy_extract;
26mod trace;
27
28use ontologos_core::{Ontology, Taxonomy};
29use thiserror::Error;
30
31pub use engine::ElEngine;
32pub use reasoner::{classify_reasoner, classify_with_report};
33pub use session::{ElSession, take_el_session};
34pub use trace::ElReport;
35
36/// Result type for EL operations.
37pub type Result<T> = std::result::Result<T, Error>;
38
39/// EL engine errors.
40#[derive(Debug, Error)]
41pub enum Error {
42    /// Reasoner profile mismatch.
43    #[error("expected profile {expected:?}, got {actual:?}")]
44    WrongProfile {
45        /// Expected profile.
46        expected: ontologos_core::Profile,
47        /// Actual profile.
48        actual: ontologos_core::Profile,
49    },
50    /// Mapped axioms fall outside OWL EL.
51    #[error("ontology is not in OWL EL profile (detected {detected:?})")]
52    NonElProfile {
53        /// Profile detected by `ontologos-profile`.
54        detected: ontologos_profile::OwlProfile,
55    },
56    /// Profile detection failed.
57    #[error(transparent)]
58    Profile(#[from] ontologos_profile::Error),
59    /// Core error.
60    #[error(transparent)]
61    Core(#[from] ontologos_core::Error),
62    /// General configuration or validation error.
63    #[error("{0}")]
64    Message(String),
65}
66
67/// OWL EL classifier using completion rules.
68#[derive(Debug, Default)]
69pub struct ElClassifier;
70
71impl ElClassifier {
72    /// Create a new EL classifier instance.
73    #[must_use]
74    pub fn new() -> Self {
75        Self
76    }
77
78    /// Classify the ontology and return the extracted taxonomy.
79    pub fn classify(&self, ontology: &Ontology) -> Result<Taxonomy> {
80        self.classify_with_options(ontology, false)
81            .map(|r| r.taxonomy)
82    }
83
84    /// Classify with optional inference trace recording.
85    pub fn classify_with_options(
86        &self,
87        ontology: &Ontology,
88        record_traces: bool,
89    ) -> Result<ElReport> {
90        normal_form::validate_el_profile(ontology)?;
91        let mut graph = graph::CompletionGraph::seed(ontology).with_traces(record_traces);
92        graph.saturate();
93        let mut taxonomy = taxonomy_extract::extract_taxonomy(ontology, &graph);
94        taxonomy.canonicalize_entity_aliases(ontology);
95        let trace = graph.into_trace();
96        Ok(ElReport { taxonomy, trace })
97    }
98
99    /// Classify and return report plus session for subsequent incremental runs.
100    pub fn classify_with_session(
101        &self,
102        ontology: &mut Ontology,
103        session: Option<ElSession>,
104        record_traces: bool,
105    ) -> Result<(ElReport, ElSession)> {
106        self.classify_incremental(ontology, session, record_traces)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use ontologos_core::{Axiom, EntityKind, Ontology};
113
114    use super::*;
115
116    fn class(ontology: &mut Ontology, iri: &str) -> ontologos_core::EntityId {
117        ontology.entity_id(iri, EntityKind::Class).expect("class")
118    }
119
120    #[test]
121    fn transitive_subclass_chain() {
122        let mut ontology = Ontology::new();
123        let a = class(&mut ontology, "http://ex.org/A");
124        let b = class(&mut ontology, "http://ex.org/B");
125        let c = class(&mut ontology, "http://ex.org/C");
126        ontology
127            .add_axiom(Axiom::SubClassOf {
128                subclass: a,
129                superclass: b,
130            })
131            .unwrap();
132        ontology
133            .add_axiom(Axiom::SubClassOf {
134                subclass: b,
135                superclass: c,
136            })
137            .unwrap();
138
139        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
140        assert!(taxonomy.is_subsumed(a, c));
141        assert!(taxonomy.direct_superclasses(a).contains(&b));
142        assert!(taxonomy.direct_superclasses(a).contains(&c) || taxonomy.is_subsumed(a, c));
143    }
144
145    #[test]
146    fn existential_filler_subsumption_in_taxonomy() {
147        use ontologos_core::TraceConclusion;
148
149        let mut ontology = Ontology::new();
150        let a = class(&mut ontology, "http://ex.org/A");
151        let b = class(&mut ontology, "http://ex.org/B");
152        let c = class(&mut ontology, "http://ex.org/C");
153        let r = ontology
154            .entity_id("http://ex.org/r", EntityKind::ObjectProperty)
155            .expect("property");
156        ontology
157            .add_axiom(Axiom::SubClassOfExistential {
158                subclass: a,
159                property: r,
160                filler: b,
161            })
162            .unwrap();
163        ontology
164            .add_axiom(Axiom::SubClassOf {
165                subclass: b,
166                superclass: c,
167            })
168            .unwrap();
169
170        let report = ElClassifier::new()
171            .classify_with_options(&ontology, true)
172            .expect("classify");
173        assert!(report.trace.steps.iter().any(|step| {
174            step.rule == "ex_filler_sub"
175                && matches!(
176                    &step.conclusion,
177                    TraceConclusion::Existential {
178                        class,
179                        property,
180                        filler
181                    } if *class == a && *property == r && *filler == c
182                )
183        }));
184    }
185
186    #[test]
187    fn el_trace_records_transitive_subsumption() {
188        let mut ontology = Ontology::new();
189        let a = class(&mut ontology, "http://ex.org/A");
190        let b = class(&mut ontology, "http://ex.org/B");
191        let c = class(&mut ontology, "http://ex.org/C");
192        ontology
193            .add_axiom(Axiom::SubClassOf {
194                subclass: a,
195                superclass: b,
196            })
197            .unwrap();
198        ontology
199            .add_axiom(Axiom::SubClassOf {
200                subclass: b,
201                superclass: c,
202            })
203            .unwrap();
204
205        let report = ElClassifier::new()
206            .classify_with_options(&ontology, true)
207            .unwrap();
208        assert!(
209            report
210                .trace
211                .steps
212                .iter()
213                .any(|s| s.rule == "sub_trans_forward")
214        );
215    }
216
217    #[test]
218    fn equivalent_classes_cluster() {
219        let mut ontology = Ontology::new();
220        let a = class(&mut ontology, "http://ex.org/A");
221        let b = class(&mut ontology, "http://ex.org/B");
222        ontology
223            .add_axiom(Axiom::EquivalentClasses(vec![a, b]))
224            .unwrap();
225
226        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
227        assert!(
228            taxonomy.equivalent_classes(a).is_some()
229                || taxonomy.is_subsumed(a, b) && taxonomy.is_subsumed(b, a)
230        );
231    }
232
233    #[test]
234    fn el_classification_forbidden_includes_complex_tbox_constructs() {
235        let mut constructs = std::collections::BTreeSet::new();
236        constructs.insert(ontologos_core::OwlConstruct::ObjectUnionOf);
237        assert!(!ontologos_profile::el_classification_forbidden_in(&constructs).is_empty());
238    }
239
240    #[test]
241    fn symmetric_property_does_not_block_forced_el_classification() {
242        let mut ontology = Ontology::new();
243        let p = ontology
244            .entity_id("http://ex.org/p", EntityKind::ObjectProperty)
245            .expect("property");
246        ontology
247            .add_axiom(Axiom::SymmetricObjectProperty(p))
248            .unwrap();
249
250        ElClassifier::new()
251            .classify(&ontology)
252            .expect("ignored characteristic axioms do not block EL");
253    }
254
255    #[test]
256    fn multiple_property_domains_infer_all() {
257        let mut ontology = Ontology::new();
258        let a = class(&mut ontology, "http://ex.org/A");
259        let d1 = class(&mut ontology, "http://ex.org/D1");
260        let d2 = class(&mut ontology, "http://ex.org/D2");
261        let p = ontology
262            .entity_id("http://ex.org/p", EntityKind::ObjectProperty)
263            .expect("property");
264        ontology
265            .add_axiom(Axiom::SubClassOfExistential {
266                subclass: a,
267                property: p,
268                filler: a,
269            })
270            .unwrap();
271        ontology
272            .add_axiom(Axiom::ObjectPropertyDomain {
273                property: p,
274                domain: d1,
275            })
276            .unwrap();
277        ontology
278            .add_axiom(Axiom::ObjectPropertyDomain {
279                property: p,
280                domain: d2,
281            })
282            .unwrap();
283
284        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
285        assert!(taxonomy.is_subsumed(a, d1));
286        assert!(taxonomy.is_subsumed(a, d2));
287    }
288}