1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! # OWL 2 EL Axiom and Concept Types
//!
//! Core data types for the OWL 2 EL profile: concept expressions, axioms,
//! normalised axiom forms, and classification results.
use std::collections::{HashMap, HashSet};
use thiserror::Error;
/// Errors from OWL 2 EL reasoning
#[derive(Debug, Error)]
pub enum ElError {
#[error("Ontology is inconsistent: {0}")]
Inconsistency(String),
#[error("Invalid axiom: {0}")]
InvalidAxiom(String),
#[error("Maximum work items ({0}) exceeded during classification")]
MaxWorkExceeded(usize),
}
/// An EL concept expression
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ElConcept {
/// owl:Thing (top concept)
Top,
/// owl:Nothing (bottom concept)
Bottom,
/// Named atomic concept (class IRI)
Named(String),
/// ObjectIntersectionOf(C1, C2, ...)
Intersection(Vec<ElConcept>),
/// ObjectSomeValuesFrom(role, filler)
SomeValues {
role: String,
filler: Box<ElConcept>,
},
}
impl ElConcept {
/// Create a named concept
pub fn named(iri: impl Into<String>) -> Self {
Self::Named(iri.into())
}
/// Create an intersection
pub fn intersection(concepts: Vec<ElConcept>) -> Self {
match concepts.len() {
0 => Self::Top,
1 => concepts.into_iter().next().unwrap_or(Self::Top),
_ => Self::Intersection(concepts),
}
}
/// Create a SomeValuesFrom restriction
pub fn some_values(role: impl Into<String>, filler: ElConcept) -> Self {
Self::SomeValues {
role: role.into(),
filler: Box::new(filler),
}
}
/// Return atomic name if this is Named
pub fn as_named(&self) -> Option<&str> {
if let Self::Named(n) = self {
Some(n)
} else {
None
}
}
}
/// An OWL 2 EL axiom
#[derive(Debug, Clone)]
pub enum ElAxiom {
/// C ⊑ D (subclass)
SubConceptOf { sub: ElConcept, sup: ElConcept },
/// C ≡ D (equivalent class)
EquivalentConcepts(ElConcept, ElConcept),
/// Individual x : C (concept assertion)
ConceptAssertion {
individual: String,
concept: ElConcept,
},
/// (x, y) : r (role assertion)
RoleAssertion {
subject: String,
role: String,
object: String,
},
/// r1 o r2 ⊑ s (property chain, binary or longer)
PropertyChain {
chain: Vec<String>,
result_role: String,
},
/// r1 ⊑ r2 (sub-property)
SubRole { sub: String, sup: String },
/// r rdf:type owl:TransitiveProperty
TransitiveRole(String),
}
/// Normalized axiom form for the EL completion algorithm.
/// All complex axioms are reduced to one of these normal forms.
#[derive(Debug, Clone)]
pub enum NormalAxiom {
/// A ⊑ B (two atomic concept names)
AtomSubAtom(String, String),
/// A ⊓ B ⊑ C (binary intersection on left)
InterAtomSubAtom(String, String, String),
/// A ⊑ ∃r.B (existential on right)
AtomSubSome(String, String, String),
/// ∃r.A ⊑ B (existential on left)
SomeSubAtom(String, String, String),
/// Transitive role r
TransRole(String),
/// Role chain: r1 o r2 ⊑ s
RoleChain(String, String, String),
/// Sub-role: r ⊑ s
SubRole(String, String),
}
/// Result of EL classification
#[derive(Debug, Clone)]
pub struct ElClassification {
/// subsumption_hierarchy: concept → set of all named superclasses (including transitive)
pub subsumption_hierarchy: HashMap<String, HashSet<String>>,
/// Groups of equivalent classes (each group contains mutually equivalent names)
pub equivalent_classes: Vec<Vec<String>>,
/// Role successor sets: (individual_or_concept, role) → set of successors
pub role_successors: HashMap<(String, String), HashSet<String>>,
/// ABox: per-individual concept memberships
pub individual_types: HashMap<String, HashSet<String>>,
/// Number of saturation loop iterations performed
pub iterations: usize,
/// Total subsumption relationships computed
pub subsumptions_computed: usize,
}
impl ElClassification {
pub(crate) fn new() -> Self {
Self {
subsumption_hierarchy: HashMap::new(),
equivalent_classes: Vec::new(),
role_successors: HashMap::new(),
individual_types: HashMap::new(),
iterations: 0,
subsumptions_computed: 0,
}
}
/// Get all named superclasses of a concept (direct and indirect)
pub fn get_superclasses(&self, class: &str) -> Vec<String> {
self.subsumption_hierarchy
.get(class)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default()
}
/// Get all named subclasses of a concept
pub fn get_subclasses(&self, class: &str) -> Vec<String> {
self.subsumption_hierarchy
.iter()
.filter(|(_, supers)| supers.contains(class))
.map(|(sub, _)| sub.clone())
.collect()
}
/// Check if `sub` ⊑ `sup`
pub fn is_subclass_of(&self, sub: &str, sup: &str) -> bool {
sub == sup
|| self
.subsumption_hierarchy
.get(sub)
.map(|s| s.contains(sup))
.unwrap_or(false)
}
/// Get all concept names an individual belongs to
pub fn get_individual_types(&self, individual: &str) -> Vec<String> {
self.individual_types
.get(individual)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default()
}
}