Skip to main content

strixonomy_reasoner/
abox.rs

1//! ABox reasoning: consistency, realization, instance checking, inferred assertions.
2
3use crate::adapter::ReasonerId;
4use crate::error::{ReasonerError, Result};
5use crate::result::{
6    entity_iri, ConsistencyDetail, InferredAssertions, InstanceCheckResult, RealizationEntry,
7    RealizationResult, SameAsCluster,
8};
9use ontologos_core::{EntityId, EntityKind, Ontology, Profile, Reasoner, ReasonerConfig};
10use ontologos_facade::{
11    check_consistency as facade_check_consistency, is_entailed_axiom, EntailmentCheck,
12};
13use std::collections::{BTreeMap, BTreeSet};
14use std::sync::atomic::{AtomicBool, Ordering};
15use std::sync::Arc;
16use std::time::Instant;
17
18/// Cap named individuals processed in a single realize pass.
19const MAX_REALIZE_INDIVIDUALS: usize = 500;
20/// Cap total class-assertion entailment checks across a realize pass.
21const MAX_REALIZE_ENTAILMENT_CHECKS: usize = 50_000;
22
23/// Thread-local cancel flag installed for reasoner runs (engine-level cancel).
24static ENGINE_CANCEL: std::sync::Mutex<Option<Arc<AtomicBool>>> = std::sync::Mutex::new(None);
25
26/// Install a cancel flag for subsequent Ontologos operations on this thread.
27pub fn install_cancel_flag(flag: Arc<AtomicBool>) {
28    ontologos_core::set_current_cancel(Some(Arc::clone(&flag)));
29    if let Ok(mut guard) = ENGINE_CANCEL.lock() {
30        *guard = Some(flag);
31    }
32}
33
34/// Clear the cancel flag after a reasoner run completes.
35pub fn clear_cancel_flag() {
36    ontologos_core::set_current_cancel(None);
37    if let Ok(mut guard) = ENGINE_CANCEL.lock() {
38        *guard = None;
39    }
40}
41
42/// Returns true when an in-flight reasoner run should abort.
43pub fn cancel_requested() -> bool {
44    if ontologos_core::cancel_requested() {
45        return true;
46    }
47    ENGINE_CANCEL
48        .lock()
49        .ok()
50        .and_then(|g| g.as_ref().map(|f| f.load(Ordering::Acquire)))
51        .unwrap_or(false)
52}
53
54fn profile_for(id: ReasonerId) -> Profile {
55    match id {
56        ReasonerId::El => Profile::El,
57        ReasonerId::Rl => Profile::Rl,
58        ReasonerId::Rdfs => Profile::Rdfs,
59        ReasonerId::Dl => Profile::Dl,
60        ReasonerId::Auto => Profile::Auto,
61    }
62}
63
64fn build_reasoner(ontology: &Ontology, id: ReasonerId) -> Result<Reasoner> {
65    let config = ReasonerConfig { explanations: true, ..ReasonerConfig::default() };
66    Reasoner::builder()
67        .profile(profile_for(id))
68        .config(config)
69        .build(ontology.clone())
70        .map_err(|e| ReasonerError::Classify(e.to_string()))
71}
72
73/// Full consistency: TBox unsatisfiable classes + ABox / ontology consistency via Ontologos.
74pub fn check_full_consistency(
75    ontology: &Ontology,
76    id: ReasonerId,
77    unsatisfiable: &[String],
78) -> Result<ConsistencyDetail> {
79    if cancel_requested() {
80        return Err(ReasonerError::Cancelled);
81    }
82    let mut reasoner = build_reasoner(ontology, id)?;
83    let facade = facade_check_consistency(&mut reasoner)
84        .map_err(|e| ReasonerError::Classify(e.to_string()))?;
85
86    let mut abox_clashes = Vec::new();
87    if matches!(id, ReasonerId::Rl | ReasonerId::Rdfs | ReasonerId::Auto | ReasonerId::El) {
88        let (clashes, _) = ontologos_rl::abox::collect_same_as_clashes(ontology);
89        abox_clashes.extend(clashes);
90        if let Ok(ok) = ontologos_rl::abox::is_abox_consistent(ontology) {
91            if !ok && abox_clashes.is_empty() {
92                abox_clashes.push("ABox is inconsistent (sameAs/differentFrom clash)".into());
93            }
94        }
95    }
96
97    let ontology_consistent = facade.complete && facade.consistent && abox_clashes.is_empty();
98    let consistent = ontology_consistent && unsatisfiable.is_empty();
99
100    Ok(ConsistencyDetail {
101        consistent,
102        complete: facade.complete,
103        ontology_consistent,
104        abox_clashes,
105        unsatisfiable: unsatisfiable.to_vec(),
106    })
107}
108
109/// Check whether `individual` is an instance of `class` under the given profile.
110pub fn check_instance(
111    ontology: &Ontology,
112    id: ReasonerId,
113    individual_iri: &str,
114    class_iri: &str,
115) -> Result<InstanceCheckResult> {
116    if cancel_requested() {
117        return Err(ReasonerError::Cancelled);
118    }
119    let started = Instant::now();
120    let mut reasoner = build_reasoner(ontology, id)?;
121    let entailed = is_entailed_axiom(
122        &mut reasoner,
123        EntailmentCheck::ClassAssertion {
124            individual: individual_iri.to_string(),
125            class: class_iri.to_string(),
126        },
127    )
128    .map_err(map_entailment_error)?;
129
130    Ok(InstanceCheckResult {
131        individual_iri: individual_iri.to_string(),
132        class_iri: class_iri.to_string(),
133        entailed,
134        profile_used: id.as_str().to_string(),
135        duration_ms: started.elapsed().as_millis() as u64,
136    })
137}
138
139fn map_entailment_error(e: impl std::fmt::Display) -> ReasonerError {
140    let msg = e.to_string();
141    if cancel_requested() || msg.to_ascii_lowercase().contains("cancel") {
142        ReasonerError::Cancelled
143    } else {
144        ReasonerError::Classify(msg)
145    }
146}
147
148fn named_individuals(ontology: &Ontology) -> Result<Vec<(EntityId, String)>> {
149    let mut out = Vec::new();
150    for (id, record) in ontology.entities().iter() {
151        if record.kind != EntityKind::Individual {
152            continue;
153        }
154        let iri = entity_iri(ontology, id).map_err(ReasonerError::Ontology)?;
155        if iri.starts_with("http://www.w3.org/2002/07/owl#")
156            || iri.starts_with("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
157        {
158            continue;
159        }
160        out.push((id, iri));
161    }
162    out.sort_by(|a, b| a.1.cmp(&b.1));
163    Ok(out)
164}
165
166fn named_classes(ontology: &Ontology) -> Result<Vec<(EntityId, String)>> {
167    let mut out = Vec::new();
168    for (id, record) in ontology.entities().iter() {
169        if record.kind != EntityKind::Class && record.kind != EntityKind::ClassIndividual {
170            continue;
171        }
172        let iri = entity_iri(ontology, id).map_err(ReasonerError::Ontology)?;
173        if iri == "http://www.w3.org/2002/07/owl#Nothing"
174            || iri == "http://www.w3.org/2002/07/owl#Thing"
175        {
176            continue;
177        }
178        out.push((id, iri));
179    }
180    out.sort_by(|a, b| a.1.cmp(&b.1));
181    Ok(out)
182}
183
184fn most_specific_types(
185    ontology: &Ontology,
186    hierarchy_parents: &BTreeMap<String, Vec<String>>,
187    types: &[String],
188) -> Vec<String> {
189    let type_set: BTreeSet<&str> = types.iter().map(|s| s.as_str()).collect();
190    types
191        .iter()
192        .filter(|t| {
193            // Keep type T if no other entailed type is a strict subclass of T.
194            !types.iter().any(|other| {
195                if other == *t {
196                    return false;
197                }
198                is_subclass_of(hierarchy_parents, other, t) || {
199                    let Some(other_id) = ontology.lookup_entity(other) else {
200                        return false;
201                    };
202                    let Some(t_id) = ontology.lookup_entity(t) else {
203                        return false;
204                    };
205                    ontology.direct_superclasses(other_id).contains(&t_id)
206                        && type_set.contains(other.as_str())
207                }
208            })
209        })
210        .cloned()
211        .collect()
212}
213
214fn is_subclass_of(parents: &BTreeMap<String, Vec<String>>, child: &str, ancestor: &str) -> bool {
215    if child == ancestor {
216        return false;
217    }
218    let mut stack = vec![child.to_string()];
219    let mut seen = BTreeSet::new();
220    while let Some(cur) = stack.pop() {
221        if !seen.insert(cur.clone()) {
222            continue;
223        }
224        let Some(supers) = parents.get(&cur) else {
225            continue;
226        };
227        for p in supers {
228            if p == ancestor {
229                return true;
230            }
231            stack.push(p.clone());
232        }
233    }
234    false
235}
236
237fn check_class_assertion(
238    reasoner: &mut Reasoner,
239    individual_iri: &str,
240    class_iri: &str,
241) -> Result<bool> {
242    if cancel_requested() {
243        return Err(ReasonerError::Cancelled);
244    }
245    match is_entailed_axiom(
246        reasoner,
247        EntailmentCheck::ClassAssertion {
248            individual: individual_iri.to_string(),
249            class: class_iri.to_string(),
250        },
251    ) {
252        Ok(v) => Ok(v),
253        Err(e) => {
254            if cancel_requested() || e.to_string().to_ascii_lowercase().contains("cancel") {
255                Err(ReasonerError::Cancelled)
256            } else {
257                // Incomplete / unsupported entailment → treat as not entailed, but surface cancel.
258                Err(ReasonerError::Classify(e.to_string()))
259            }
260        }
261    }
262}
263
264/// Realize all named individuals under the given profile.
265pub fn realize(ontology: &Ontology, id: ReasonerId) -> Result<RealizationResult> {
266    if cancel_requested() {
267        return Err(ReasonerError::Cancelled);
268    }
269    let started = Instant::now();
270    let mut reasoner = build_reasoner(ontology, id)?;
271    let mut individuals = named_individuals(ontology)?;
272    let classes = named_classes(ontology)?;
273
274    let mut truncated = false;
275    if individuals.len() > MAX_REALIZE_INDIVIDUALS {
276        individuals.truncate(MAX_REALIZE_INDIVIDUALS);
277        truncated = true;
278    }
279
280    // Build hierarchy parents from asserted structure.
281    let mut parents: BTreeMap<String, Vec<String>> = BTreeMap::new();
282    for (class_id, class_iri) in &classes {
283        for &sup in ontology.direct_superclasses(*class_id) {
284            if let Ok(sup_iri) = entity_iri(ontology, sup) {
285                parents.entry(class_iri.clone()).or_default().push(sup_iri);
286            }
287        }
288    }
289
290    let mut entries = Vec::new();
291    let mut entailment_checks = 0usize;
292    let mut entailment_errors = 0usize;
293    'individuals: for (ind_id, ind_iri) in &individuals {
294        if cancel_requested() {
295            return Err(ReasonerError::Cancelled);
296        }
297        let asserted: Vec<String> = ontology
298            .classes_of(*ind_id)
299            .iter()
300            .filter_map(|cid| entity_iri(ontology, *cid).ok())
301            .filter(|iri| {
302                iri != "http://www.w3.org/2002/07/owl#Thing"
303                    && iri != "http://www.w3.org/2002/07/owl#NamedIndividual"
304            })
305            .collect();
306
307        let mut types = BTreeSet::new();
308        for a in &asserted {
309            types.insert(a.clone());
310        }
311
312        let mut candidates: BTreeSet<String> = asserted.iter().cloned().collect();
313        for (_, class_iri) in &classes {
314            candidates.insert(class_iri.clone());
315        }
316
317        for class_iri in &candidates {
318            if types.contains(class_iri) {
319                continue;
320            }
321            if entailment_checks >= MAX_REALIZE_ENTAILMENT_CHECKS {
322                truncated = true;
323                break 'individuals;
324            }
325            entailment_checks += 1;
326            match check_class_assertion(&mut reasoner, ind_iri, class_iri) {
327                Ok(true) => {
328                    types.insert(class_iri.clone());
329                }
330                Ok(false) => {}
331                Err(ReasonerError::Cancelled) => return Err(ReasonerError::Cancelled),
332                // Non-cancel entailment failures: count as incomplete, do not treat as not-entailed (#361).
333                Err(_) => {
334                    entailment_errors += 1;
335                    truncated = true;
336                }
337            }
338        }
339
340        let type_list: Vec<String> = types.into_iter().collect();
341        let most_specific = most_specific_types(ontology, &parents, &type_list);
342        let inferred: Vec<String> =
343            type_list.iter().filter(|t| !asserted.contains(t)).cloned().collect();
344
345        entries.push(RealizationEntry {
346            individual_iri: ind_iri.clone(),
347            types: type_list,
348            most_specific,
349            asserted,
350            inferred,
351        });
352
353        if entailment_checks >= MAX_REALIZE_ENTAILMENT_CHECKS {
354            truncated = true;
355            break;
356        }
357    }
358
359    Ok(RealizationResult {
360        profile_used: id.as_str().to_string(),
361        individuals: entries,
362        duration_ms: started.elapsed().as_millis() as u64,
363        truncated,
364        entailment_errors,
365    })
366}
367
368/// Collect inferred ABox assertions, reusing an existing realization when available.
369pub fn inferred_assertions_from_realization(
370    ontology: &Ontology,
371    id: ReasonerId,
372    realization: &RealizationResult,
373) -> Result<InferredAssertions> {
374    if cancel_requested() {
375        return Err(ReasonerError::Cancelled);
376    }
377
378    let mut class_assertions = Vec::new();
379    for entry in &realization.individuals {
380        for class_iri in &entry.inferred {
381            class_assertions.push(crate::result::InferredClassAssertion {
382                individual_iri: entry.individual_iri.clone(),
383                class_iri: class_iri.clone(),
384            });
385        }
386    }
387
388    let asserted_ops: BTreeSet<(String, String, String)> = {
389        let mut set = BTreeSet::new();
390        let inds = named_individuals(ontology)?;
391        for (subj_id, subj_iri) in &inds {
392            for &(prop_id, obj_id) in ontology.object_assertions_of(*subj_id) {
393                let Ok(prop_iri) = entity_iri(ontology, prop_id) else {
394                    continue;
395                };
396                let Ok(obj_iri) = entity_iri(ontology, obj_id) else {
397                    continue;
398                };
399                set.insert((subj_iri.clone(), prop_iri, obj_iri));
400            }
401        }
402        set
403    };
404
405    let mut object_property_assertions = Vec::new();
406    let mut working = ontology.clone();
407    // RL ABox materialization is the shared path for role inferences today.
408    if matches!(
409        id,
410        ReasonerId::Rl | ReasonerId::Rdfs | ReasonerId::Auto | ReasonerId::El | ReasonerId::Dl
411    ) {
412        let _ = ontologos_rl::abox::materialize_abox(&mut working);
413    }
414    if cancel_requested() {
415        return Err(ReasonerError::Cancelled);
416    }
417
418    let individuals = named_individuals(&working)?;
419    let mut props = Vec::new();
420    for (pid, record) in working.entities().iter() {
421        if record.kind == EntityKind::ObjectProperty {
422            if let Ok(iri) = entity_iri(&working, pid) {
423                props.push((pid, iri));
424            }
425        }
426    }
427    for (subj_id, subj_iri) in &individuals {
428        if cancel_requested() {
429            return Err(ReasonerError::Cancelled);
430        }
431        for (prop_id, prop_iri) in &props {
432            let Ok(objs) =
433                ontologos_rl::abox::object_property_values(&mut working, *subj_id, *prop_id)
434            else {
435                continue;
436            };
437            for obj_id in objs {
438                let Ok(obj_iri) = entity_iri(&working, obj_id) else {
439                    continue;
440                };
441                if !asserted_ops.contains(&(subj_iri.clone(), prop_iri.clone(), obj_iri.clone())) {
442                    object_property_assertions.push(
443                        crate::result::InferredObjectPropertyAssertion {
444                            subject_iri: subj_iri.clone(),
445                            property_iri: prop_iri.clone(),
446                            object_iri: obj_iri,
447                        },
448                    );
449                }
450            }
451        }
452    }
453
454    let closure = ontologos_rl::abox::same_as_closure(ontology);
455    let mut same_as_clusters = Vec::new();
456    for cluster in &closure.clusters {
457        if cluster.len() < 2 {
458            continue;
459        }
460        let mut iris: Vec<String> =
461            cluster.iter().filter_map(|id| entity_iri(ontology, *id).ok()).collect();
462        iris.sort();
463        iris.dedup();
464        if iris.len() >= 2 {
465            same_as_clusters.push(SameAsCluster { individuals: iris });
466        }
467    }
468
469    Ok(InferredAssertions { class_assertions, object_property_assertions, same_as_clusters })
470}
471
472/// Collect inferred ABox assertions (class types, object properties, sameAs clusters).
473pub fn inferred_assertions(ontology: &Ontology, id: ReasonerId) -> Result<InferredAssertions> {
474    let realization = realize(ontology, id)?;
475    inferred_assertions_from_realization(ontology, id, &realization)
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn cancel_requested_aborts_realize() {
484        let flag = Arc::new(AtomicBool::new(true));
485        install_cancel_flag(Arc::clone(&flag));
486        let ontology = Ontology::new();
487        let err = realize(&ontology, ReasonerId::El).expect_err("cancelled");
488        clear_cancel_flag();
489        assert!(matches!(err, ReasonerError::Cancelled));
490    }
491}