Skip to main content

ontocore_reasoner/
result.rs

1use ontocore_catalog::{ClassHierarchy, SubclassEdge};
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ReasonerWarning {
7    pub code: String,
8    pub message: String,
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub suggested_profile: Option<String>,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct InferredHierarchy {
15    pub edges: Vec<SubclassEdge>,
16    pub unsatisfiable: Vec<String>,
17    pub combined: ClassHierarchy,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ClassificationResult {
22    pub profile_used: String,
23    /// `true` when no **named class** is unsatisfiable (⊑ `owl:Nothing`).
24    /// Does not detect all ontology inconsistencies (e.g. some ABox clashes).
25    pub consistent: bool,
26    pub unsatisfiable: Vec<String>,
27    pub inferred: InferredHierarchy,
28    pub new_inferences: Vec<SubclassEdge>,
29    pub warnings: Vec<ReasonerWarning>,
30    pub duration_ms: u64,
31    pub subsumption_count: usize,
32    pub inferred_axiom_count: usize,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ConsistencyResult {
37    /// Class-level consistency only (see [`ClassificationResult::consistent`]).
38    pub consistent: bool,
39    pub unsatisfiable: Vec<String>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ReasonerSnapshot {
44    pub profile_used: String,
45    pub consistent: bool,
46    pub unsatisfiable: Vec<String>,
47    pub inferred: InferredHierarchy,
48    pub new_inferences: Vec<SubclassEdge>,
49    pub warnings: Vec<ReasonerWarning>,
50    pub duration_ms: u64,
51    pub classified_at: u64,
52}
53
54impl From<ClassificationResult> for ReasonerSnapshot {
55    fn from(result: ClassificationResult) -> Self {
56        Self {
57            profile_used: result.profile_used,
58            consistent: result.consistent,
59            unsatisfiable: result.unsatisfiable,
60            inferred: result.inferred,
61            new_inferences: result.new_inferences,
62            warnings: result.warnings,
63            duration_ms: result.duration_ms,
64            classified_at: std::time::SystemTime::now()
65                .duration_since(std::time::UNIX_EPOCH)
66                .map(|d| d.as_secs())
67                .unwrap_or(0),
68        }
69    }
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct ExplanationRequest {
74    pub class_iri: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct ExplanationStep {
79    pub index: usize,
80    pub rule: String,
81    pub display: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub subject_iri: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub object_iri: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct ExplanationResult {
90    pub class_iri: String,
91    pub steps: Vec<ExplanationStep>,
92    pub text: String,
93}
94
95pub fn build_inferred_hierarchy(
96    taxonomy_edges: &[(String, String)],
97    unsatisfiable: &[String],
98    asserted: &ClassHierarchy,
99) -> InferredHierarchy {
100    let asserted_set: BTreeSet<(String, String)> =
101        asserted.edges.iter().map(|e| (e.child.clone(), e.parent.clone())).collect();
102
103    let mut inferred_edges = Vec::new();
104    for (child, parent) in taxonomy_edges {
105        let pair = (child.clone(), parent.clone());
106        if !asserted_set.contains(&pair) {
107            inferred_edges.push(SubclassEdge { child: child.clone(), parent: parent.clone() });
108        }
109    }
110
111    let mut combined_edges = asserted.edges.clone();
112    let mut combined_set = asserted_set;
113    for edge in &inferred_edges {
114        let pair = (edge.child.clone(), edge.parent.clone());
115        if combined_set.insert(pair) {
116            combined_edges.push(edge.clone());
117        }
118    }
119
120    let combined = hierarchy_from_edges(combined_edges);
121    InferredHierarchy { edges: inferred_edges, unsatisfiable: unsatisfiable.to_vec(), combined }
122}
123
124pub fn new_inferences(asserted: &ClassHierarchy, inferred: &[SubclassEdge]) -> Vec<SubclassEdge> {
125    let asserted_set: BTreeSet<(String, String)> =
126        asserted.edges.iter().map(|e| (e.child.clone(), e.parent.clone())).collect();
127    inferred
128        .iter()
129        .filter(|e| !asserted_set.contains(&(e.child.clone(), e.parent.clone())))
130        .cloned()
131        .collect()
132}
133
134pub fn hierarchy_from_edges(edges: Vec<SubclassEdge>) -> ClassHierarchy {
135    let mut parents: BTreeMap<String, Vec<String>> = BTreeMap::new();
136    let mut children: BTreeMap<String, Vec<String>> = BTreeMap::new();
137
138    for edge in &edges {
139        parents.entry(edge.child.clone()).or_default().push(edge.parent.clone());
140        children.entry(edge.parent.clone()).or_default().push(edge.child.clone());
141    }
142
143    for list in parents.values_mut() {
144        list.sort();
145        list.dedup();
146    }
147    for list in children.values_mut() {
148        list.sort();
149        list.dedup();
150    }
151
152    ClassHierarchy { edges, parents, children }
153}
154
155pub fn taxonomy_to_iri_edges(
156    ontology: &ontologos_core::Ontology,
157    taxonomy: &ontologos_core::Taxonomy,
158) -> Result<Vec<(String, String)>, String> {
159    taxonomy
160        .subsumptions
161        .iter()
162        .map(|(sub, sup)| {
163            let child = entity_iri(ontology, *sub)?;
164            let parent = entity_iri(ontology, *sup)?;
165            Ok((child, parent))
166        })
167        .collect()
168}
169
170pub fn entity_iri(
171    ontology: &ontologos_core::Ontology,
172    id: ontologos_core::EntityId,
173) -> Result<String, String> {
174    let entity = ontology.entity(id).map_err(|e| e.to_string())?;
175    ontology.resolve_iri(entity.iri).map(|s| s.to_string()).map_err(|e| e.to_string())
176}
177
178pub fn unsatisfiable_iris(
179    ontology: &ontologos_core::Ontology,
180    taxonomy: &ontologos_core::Taxonomy,
181) -> Result<Vec<String>, String> {
182    taxonomy.unsatisfiable.iter().map(|id| entity_iri(ontology, *id)).collect()
183}
184
185/// Run EL classification to detect unsatisfiable classes (used after RL/RDFS saturation).
186pub fn detect_unsatisfiable_classes(
187    ontology: &ontologos_core::Ontology,
188) -> Result<Vec<String>, String> {
189    use ontologos_el::ElClassifier;
190    let taxonomy =
191        ElClassifier::new().classify(ontology).map_err(|e| format!("unsat detection: {e}"))?;
192    unsatisfiable_iris(ontology, &taxonomy)
193}