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 graph;
20mod normal_form;
21mod reasoner;
22mod route;
23mod taxonomy_extract;
24
25use ontologos_core::{Ontology, Taxonomy};
26use thiserror::Error;
27
28pub use reasoner::{classify_reasoner, try_classify_reasoner};
29pub use route::{classify_with_profile, resolve_profile_flag, ClassifyOutcome, ProfileFlag};
30
31/// Result type for EL operations.
32pub type Result<T> = std::result::Result<T, Error>;
33
34/// EL engine errors.
35#[derive(Debug, Error)]
36pub enum Error {
37    /// Reasoner profile mismatch.
38    #[error("expected profile {expected:?}, got {actual:?}")]
39    WrongProfile {
40        /// Expected profile.
41        expected: ontologos_core::Profile,
42        /// Actual profile.
43        actual: ontologos_core::Profile,
44    },
45    /// Mapped axioms fall outside OWL EL.
46    #[error("ontology is not in OWL EL profile (detected {detected:?})")]
47    NonElProfile {
48        /// Profile detected by `ontologos-profile`.
49        detected: ontologos_profile::OwlProfile,
50    },
51    /// Profile detection failed.
52    #[error("profile detection failed: {0}")]
53    Profile(String),
54    /// Auto-routing cannot classify this profile.
55    #[error("classification not supported for profile {0:?}")]
56    UnsupportedProfile(ontologos_profile::OwlProfile),
57    /// Core error.
58    #[error(transparent)]
59    Core(#[from] ontologos_core::Error),
60    /// RDFS engine error.
61    #[error(transparent)]
62    Rdfs(#[from] ontologos_rdfs::Error),
63    /// RL engine error.
64    #[error(transparent)]
65    Rl(#[from] ontologos_rl::Error),
66}
67
68/// OWL EL classifier using completion rules.
69#[derive(Debug, Default)]
70pub struct ElClassifier;
71
72impl ElClassifier {
73    /// Create a new EL classifier instance.
74    #[must_use]
75    pub fn new() -> Self {
76        Self
77    }
78
79    /// Classify the ontology and return the extracted taxonomy.
80    ///
81    /// Runs ELK-style goal-directed completion and transitive-reduction taxonomy
82    /// extraction. The ontology is not mutated.
83    pub fn classify(&self, ontology: &Ontology) -> Result<Taxonomy> {
84        normal_form::validate_el_profile(ontology)?;
85        let mut graph = graph::CompletionGraph::seed(ontology);
86        graph.saturate();
87        Ok(taxonomy_extract::extract_taxonomy(ontology, &graph))
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use ontologos_core::{Axiom, EntityKind, Ontology};
94
95    use super::*;
96
97    fn class(ontology: &mut Ontology, iri: &str) -> ontologos_core::EntityId {
98        ontology.entity_id(iri, EntityKind::Class).expect("class")
99    }
100
101    #[test]
102    fn transitive_subclass_chain() {
103        let mut ontology = Ontology::new();
104        let a = class(&mut ontology, "http://ex.org/A");
105        let b = class(&mut ontology, "http://ex.org/B");
106        let c = class(&mut ontology, "http://ex.org/C");
107        ontology
108            .add_axiom(Axiom::SubClassOf {
109                subclass: a,
110                superclass: b,
111            })
112            .unwrap();
113        ontology
114            .add_axiom(Axiom::SubClassOf {
115                subclass: b,
116                superclass: c,
117            })
118            .unwrap();
119
120        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
121        assert!(taxonomy.is_subsumed(a, c));
122        assert!(taxonomy.direct_superclasses(a).contains(&b));
123        assert!(taxonomy.direct_superclasses(a).contains(&c) || taxonomy.is_subsumed(a, c));
124    }
125
126    #[test]
127    fn existential_filler_subsumption_in_taxonomy() {
128        let mut ontology = Ontology::new();
129        let a = class(&mut ontology, "http://ex.org/A");
130        let b = class(&mut ontology, "http://ex.org/B");
131        let c = class(&mut ontology, "http://ex.org/C");
132        ontology
133            .add_axiom(Axiom::SubClassOf {
134                subclass: a,
135                superclass: b,
136            })
137            .unwrap();
138        ontology
139            .add_axiom(Axiom::SubClassOf {
140                subclass: b,
141                superclass: c,
142            })
143            .unwrap();
144
145        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
146        assert!(taxonomy.is_subsumed(a, c));
147        assert!(taxonomy.is_subsumed(a, b));
148    }
149
150    #[test]
151    fn equivalent_classes_cluster() {
152        let mut ontology = Ontology::new();
153        let a = class(&mut ontology, "http://ex.org/A");
154        let b = class(&mut ontology, "http://ex.org/B");
155        ontology
156            .add_axiom(Axiom::EquivalentClasses(vec![a, b]))
157            .unwrap();
158
159        let taxonomy = ElClassifier::new().classify(&ontology).unwrap();
160        assert!(
161            taxonomy.equivalent_classes(a).is_some()
162                || taxonomy.is_subsumed(a, b) && taxonomy.is_subsumed(b, a)
163        );
164    }
165}