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_routed, 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 self.classify_el_tbox(ontology, record_traces)
92 }
93
94 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 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}