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_routed, 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        self.classify_el_tbox(ontology, record_traces)
92    }
93
94    /// Classify the TBox for SWRL forward chaining (SWRL rules do not block EL).
95    pub fn classify_for_swrl(&self, ontology: &Ontology) -> Result<Taxonomy> {
96        normal_form::validate_el_profile_for_swrl(ontology)?;
97        self.classify_el_tbox(ontology, false)
98            .map(|report| report.taxonomy)
99    }
100
101    fn classify_el_tbox(&self, ontology: &Ontology, record_traces: bool) -> Result<ElReport> {
102        let mut graph = graph::CompletionGraph::seed(ontology).with_traces(record_traces);
103        graph.saturate();
104        let mut taxonomy = taxonomy_extract::extract_taxonomy(ontology, &graph);
105        taxonomy.canonicalize_entity_aliases(ontology);
106        let trace = graph.into_trace();
107        Ok(ElReport { taxonomy, trace })
108    }
109
110    /// Classify and return report plus session for subsequent incremental runs.
111    pub fn classify_with_session(
112        &self,
113        ontology: &mut Ontology,
114        session: Option<ElSession>,
115        record_traces: bool,
116    ) -> Result<(ElReport, ElSession)> {
117        self.classify_incremental(ontology, session, record_traces)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use ontologos_core::{Axiom, EntityKind, Ontology};
124
125    use super::*;
126
127    fn class(ontology: &mut Ontology, iri: &str) -> ontologos_core::EntityId {
128        ontology.entity_id(iri, EntityKind::Class).expect("class")
129    }
130
131    #[test]
132    fn transitive_subclass_chain() {
133        let mut ontology = Ontology::new();
134        let a = class(&mut ontology, "http://ex.org/A");
135        let b = class(&mut ontology, "http://ex.org/B");
136        let c = class(&mut ontology, "http://ex.org/C");
137        ontology
138            .add_axiom(Axiom::SubClassOf {
139                subclass: a,
140                superclass: b,
141            })
142            .unwrap();
143        ontology
144            .add_axiom(Axiom::SubClassOf {
145                subclass: b,
146                superclass: c,
147            })
148            .unwrap();
149
150        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
151        assert!(taxonomy.is_subsumed(a, c));
152        assert!(taxonomy.direct_superclasses(a).contains(&b));
153        assert!(taxonomy.direct_superclasses(a).contains(&c) || taxonomy.is_subsumed(a, c));
154    }
155
156    #[test]
157    fn existential_filler_subsumption_in_taxonomy() {
158        use ontologos_core::TraceConclusion;
159
160        let mut ontology = Ontology::new();
161        let a = class(&mut ontology, "http://ex.org/A");
162        let b = class(&mut ontology, "http://ex.org/B");
163        let c = class(&mut ontology, "http://ex.org/C");
164        let r = ontology
165            .entity_id("http://ex.org/r", EntityKind::ObjectProperty)
166            .expect("property");
167        ontology
168            .add_axiom(Axiom::SubClassOfExistential {
169                subclass: a,
170                property: r,
171                filler: b,
172            })
173            .unwrap();
174        ontology
175            .add_axiom(Axiom::SubClassOf {
176                subclass: b,
177                superclass: c,
178            })
179            .unwrap();
180
181        let report = ElClassifier::new()
182            .classify_with_options(&ontology, true)
183            .expect("classify");
184        assert!(report.trace.steps.iter().any(|step| {
185            step.rule == "ex_filler_sub"
186                && matches!(
187                    &step.conclusion,
188                    TraceConclusion::Existential {
189                        class,
190                        property,
191                        filler
192                    } if *class == a && *property == r && *filler == c
193                )
194        }));
195    }
196
197    #[test]
198    fn el_trace_records_transitive_subsumption() {
199        let mut ontology = Ontology::new();
200        let a = class(&mut ontology, "http://ex.org/A");
201        let b = class(&mut ontology, "http://ex.org/B");
202        let c = class(&mut ontology, "http://ex.org/C");
203        ontology
204            .add_axiom(Axiom::SubClassOf {
205                subclass: a,
206                superclass: b,
207            })
208            .unwrap();
209        ontology
210            .add_axiom(Axiom::SubClassOf {
211                subclass: b,
212                superclass: c,
213            })
214            .unwrap();
215
216        let report = ElClassifier::new()
217            .classify_with_options(&ontology, true)
218            .unwrap();
219        assert!(
220            report
221                .trace
222                .steps
223                .iter()
224                .any(|s| s.rule == "sub_trans_forward")
225        );
226    }
227
228    #[test]
229    fn equivalent_classes_cluster() {
230        let mut ontology = Ontology::new();
231        let a = class(&mut ontology, "http://ex.org/A");
232        let b = class(&mut ontology, "http://ex.org/B");
233        ontology
234            .add_axiom(Axiom::EquivalentClasses(vec![a, b]))
235            .unwrap();
236
237        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
238        assert!(
239            taxonomy.equivalent_classes(a).is_some()
240                || taxonomy.is_subsumed(a, b) && taxonomy.is_subsumed(b, a)
241        );
242    }
243
244    #[test]
245    fn el_classification_forbidden_includes_complex_tbox_constructs() {
246        let mut constructs = std::collections::BTreeSet::new();
247        constructs.insert(ontologos_core::OwlConstruct::ObjectUnionOf);
248        assert!(!ontologos_profile::el_classification_forbidden_in(&constructs).is_empty());
249    }
250
251    #[test]
252    fn symmetric_property_does_not_block_forced_el_classification() {
253        let mut ontology = Ontology::new();
254        let p = ontology
255            .entity_id("http://ex.org/p", EntityKind::ObjectProperty)
256            .expect("property");
257        ontology
258            .add_axiom(Axiom::SymmetricObjectProperty(p))
259            .unwrap();
260
261        ElClassifier::new()
262            .classify(&ontology)
263            .expect("ignored characteristic axioms do not block EL");
264    }
265
266    #[test]
267    fn multiple_property_domains_infer_all() {
268        let mut ontology = Ontology::new();
269        let a = class(&mut ontology, "http://ex.org/A");
270        let d1 = class(&mut ontology, "http://ex.org/D1");
271        let d2 = class(&mut ontology, "http://ex.org/D2");
272        let p = ontology
273            .entity_id("http://ex.org/p", EntityKind::ObjectProperty)
274            .expect("property");
275        ontology
276            .add_axiom(Axiom::SubClassOfExistential {
277                subclass: a,
278                property: p,
279                filler: a,
280            })
281            .unwrap();
282        ontology
283            .add_axiom(Axiom::ObjectPropertyDomain {
284                property: p,
285                domain: d1,
286            })
287            .unwrap();
288        ontology
289            .add_axiom(Axiom::ObjectPropertyDomain {
290                property: p,
291                domain: d2,
292            })
293            .unwrap();
294
295        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
296        assert!(taxonomy.is_subsumed(a, d1));
297        assert!(taxonomy.is_subsumed(a, d2));
298    }
299}