1#![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
36pub type Result<T> = std::result::Result<T, Error>;
38
39#[derive(Debug, Error)]
41pub enum Error {
42 #[error("expected profile {expected:?}, got {actual:?}")]
44 WrongProfile {
45 expected: ontologos_core::Profile,
47 actual: ontologos_core::Profile,
49 },
50 #[error("ontology is not in OWL EL profile (detected {detected:?})")]
52 NonElProfile {
53 detected: ontologos_profile::OwlProfile,
55 },
56 #[error(transparent)]
58 Profile(#[from] ontologos_profile::Error),
59 #[error(transparent)]
61 Core(#[from] ontologos_core::Error),
62 #[error("{0}")]
64 Message(String),
65}
66
67#[derive(Debug, Default)]
69pub struct ElClassifier;
70
71impl ElClassifier {
72 #[must_use]
74 pub fn new() -> Self {
75 Self
76 }
77
78 pub fn classify(&self, ontology: &Ontology) -> Result<Taxonomy> {
80 self.classify_with_options(ontology, false)
81 .map(|r| r.taxonomy)
82 }
83
84 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 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}