Skip to main content

ontologos_dl/
lib.rs

1//! OWL 2 DL reasoner: coupled saturation + tableau (Konclude-style hybrid).
2
3#![warn(missing_docs)]
4
5mod bounded;
6mod cardinality;
7mod cardinality_grid;
8mod classify;
9mod datatype;
10mod defined_class;
11mod dependency_index;
12mod engine;
13mod object_property_query;
14mod perf;
15mod reasoner;
16mod ria;
17mod ria_regularity;
18mod saturation;
19mod union_csp;
20
21use ontologos_alc::{DlOntology, TableauSeed};
22use ontologos_core::{
23    Axiom, CeId, ClassExpr, DlAxiom, EntityId, EntityKind, Ontology, Profile, RoleExpr, Taxonomy,
24};
25use thiserror::Error;
26
27pub use bounded::{dl_max_workers, run_bounded};
28pub use classify::DlClassifier;
29pub use datatype::is_data_range_satisfiable;
30pub use datatype::{
31    LiteralIndex, LiteralValue, is_datatype_consistent, named_class_datatype_satisfiable,
32};
33pub use dependency_index::DependencyIndex;
34pub use engine::DlEngine;
35pub use object_property_query::{
36    RolePropertyQueryContext, classify_object_property_expressions,
37    equivalent_object_property_expressions, inverse_object_property_expressions,
38    sub_object_property_expressions,
39};
40pub use ontologos_core::ConsistencyResult;
41pub use perf::{DlPerfTimings, perf_enabled};
42pub use reasoner::{DlReport, classify_reasoner};
43pub use ria::RoleHierarchy;
44pub use ria_regularity::{is_property_hierarchy_regular, is_property_hierarchy_simple};
45pub use saturation::{SaturatedFacts, saturate};
46
47/// Result type for DL operations.
48pub type Result<T> = std::result::Result<T, Error>;
49
50/// DL engine errors.
51#[derive(Debug, Error)]
52pub enum Error {
53    /// Profile mismatch.
54    #[error("expected DL profile, got {0:?}")]
55    WrongProfile(Profile),
56    /// EL fallback error.
57    #[error(transparent)]
58    El(#[from] ontologos_el::Error),
59    /// ALC/tableau error.
60    #[error(transparent)]
61    Alc(#[from] ontologos_alc::Error),
62    /// Parser error.
63    #[error(transparent)]
64    Parser(#[from] ontologos_parser::Error),
65    /// Core error.
66    #[error(transparent)]
67    Core(#[from] ontologos_core::Error),
68    /// Ontology is inconsistent.
69    #[error("ontology inconsistent")]
70    Inconsistent,
71    /// Preview-only limitation.
72    #[error("DL preview: {0}")]
73    PreviewLimit(String),
74    /// Profile detection failed.
75    #[error(transparent)]
76    Profile(#[from] ontologos_profile::Error),
77    /// General message.
78    #[error("{0}")]
79    Message(String),
80    /// Consistency or satisfiability check did not complete (budget or tableau limit).
81    #[error("consistency check incomplete: {0}")]
82    IncompleteReasoning(String),
83}
84
85/// Classify an ontology under OWL 2 DL semantics.
86pub fn classify(ontology: &Ontology) -> Result<Taxonomy> {
87    DlClassifier::new().classify(ontology)
88}
89
90/// Classify for OWL entailment checks (skips pairwise named subsumption inference).
91pub fn classify_for_entailment(ontology: &Ontology) -> Result<Taxonomy> {
92    ontologos_profile::detect_profile(ontology)?;
93    let dl = DlOntology::from_ontology(ontology).map_err(Error::Alc)?;
94    let roles = ria::RoleHierarchy::from_clauses(dl.clauses());
95    let facts = saturation::saturate(ontology, dl.clauses(), &roles)?;
96    let seed = classify::build_tableau_seed(ontology, &dl, &facts, &roles)?;
97    let mut taxonomy =
98        ontologos_alc::classify_with_seed_for_entailment(ontology, &seed).map_err(Error::Alc)?;
99    for (sub, sup) in cardinality::derive_cardinality_subsumptions(ontology) {
100        if !taxonomy
101            .subsumptions
102            .iter()
103            .any(|&(a, b)| a == sub && b == sup)
104        {
105            taxonomy.subsumptions.push((sub, sup));
106        }
107    }
108    Ok(taxonomy)
109}
110
111/// Check ontology consistency under DL with optional wall-clock budget.
112#[tracing::instrument(skip(ontology))]
113pub fn check_consistency(
114    ontology: &Ontology,
115    budget_secs: Option<u64>,
116) -> Result<ConsistencyResult> {
117    if bounded::resolve_budget_secs(budget_secs).is_none() {
118        return check_consistency_inner(ontology);
119    }
120    let ontology = ontology.clone();
121    bounded::run_bounded(budget_secs, move || check_consistency_inner(&ontology))?
122}
123
124/// Check ontology consistency under DL (errors when the answer is incomplete).
125pub fn is_consistent(ontology: &Ontology) -> Result<bool> {
126    check_consistency(ontology, None)?
127        .into_bool()
128        .map_err(Error::Core)
129}
130
131fn check_consistency_inner(ontology: &Ontology) -> Result<ConsistencyResult> {
132    match check_consistency_inner_impl(ontology) {
133        Err(Error::IncompleteReasoning(_)) => Ok(ConsistencyResult::incomplete()),
134        Ok(result) => Ok(result),
135        Err(e) => Err(e),
136    }
137}
138
139fn check_consistency_inner_impl(ontology: &Ontology) -> Result<ConsistencyResult> {
140    if bounded::dl_cancel_requested() {
141        return Err(Error::IncompleteReasoning(
142            "dl operation cancelled (budget exceeded)".into(),
143        ));
144    }
145    let trace = std::env::var("ONTOLOGOS_CONSISTENCY_TRACE").is_ok();
146    macro_rules! reject {
147        ($step:expr) => {{
148            if trace {
149                eprintln!("is_consistent: reject at {}", $step);
150            }
151            return Ok(ConsistencyResult::inconsistent());
152        }};
153    }
154    if ontology
155        .parse_meta()
156        .is_some_and(|meta| meta.trivial_abox_inconsistent)
157    {
158        reject!("trivial_abox_inconsistent");
159    }
160    if thing_equivalent_nothing(ontology) {
161        reject!("thing_equivalent_nothing");
162    }
163    if thing_equivalent_finite_nominal(ontology) {
164        reject!("thing_equivalent_finite_nominal");
165    }
166    if !datatype::is_datatype_consistent(ontology) {
167        reject!("datatype");
168    }
169    if ontologos_bridge::has_bottom_chain_violation(ontology) {
170        reject!("bottom_chain");
171    }
172    if abox_property_characteristic_clash(ontology) {
173        reject!("property_characteristic_clash");
174    }
175    if abox_bottom_property_restriction(ontology) {
176        reject!("bottom_property_restriction");
177    }
178    if abox_max_cardinality_zero_clash(ontology) {
179        reject!("max_cardinality_zero");
180    }
181    if abox_max_cardinality_exceeded_clash(ontology) {
182        reject!("max_cardinality_exceeded");
183    }
184    if abox_positive_negative_property_clash(ontology) {
185        reject!("positive_negative_property");
186    }
187    if abox_positive_negative_data_clash(ontology) {
188        reject!("positive_negative_data");
189    }
190    if abox_property_self_disjoint_clash(ontology) {
191        reject!("property_self_disjoint");
192    }
193    if abox_self_disjoint_restriction_clash(ontology) {
194        reject!("self_disjoint_restriction");
195    }
196    if abox_complement_typing_clash(ontology) {
197        reject!("complement_typing");
198    }
199    if abox_complement_existential_property_clash(ontology) {
200        reject!("complement_existential_property");
201    }
202    if abox_min_card_exceeds_individual_max_card_clash(ontology) {
203        reject!("min_vs_individual_max_card");
204    }
205    if tbox_data_cardinality_clash_with_abox(ontology) {
206        reject!("tbox_data_cardinality_clash");
207    }
208    if cardinality_grid::functional_inverse_cardinality_product_inconsistent(ontology) {
209        reject!("functional_inverse_cardinality_product");
210    }
211    if let Some(consistent) = union_csp::nominal_grid_consistency(ontology) {
212        if trace {
213            eprintln!("is_consistent: union_csp => {consistent}");
214        }
215        return Ok(if consistent {
216            ConsistencyResult::consistent()
217        } else {
218            ConsistencyResult::inconsistent()
219        });
220    }
221    if bounded::wg_shortcuts_enabled() && wg_wine_import_merge_consistency_shortcut(ontology) {
222        if trace {
223            eprintln!("is_consistent: wine_wg_import_merge => true");
224        }
225        return Ok(ConsistencyResult::consistent());
226    }
227    let dl = ontologos_alc::DlOntology::from_ontology(ontology).map_err(Error::Alc)?;
228    let roles = ria::RoleHierarchy::from_clauses(dl.clauses());
229    let facts = saturation::saturate(ontology, dl.clauses(), &roles)?;
230    let seed = classify::build_tableau_seed(ontology, &dl, &facts, &roles)?;
231    if matches!(
232        abox_exists_forall_role_clash(ontology, &dl, &seed)?,
233        Some(true)
234    ) {
235        reject!("exists_forall_role_clash");
236    }
237    if abox_asserted_exact_zero_equiv_class(ontology) {
238        reject!("abox_exact_zero_equiv");
239    }
240    if should_run_taxonomy_abox_check(ontology) {
241        let taxonomy = classify(ontology)?;
242        if abox_asserted_taxonomy_unsatisfiable(ontology, &taxonomy) {
243            reject!("abox_taxonomy_unsat");
244        }
245    }
246    if ontology_maybe_needs_flower_classify(ontology) {
247        let taxonomy = classify(ontology)?;
248        if flower_auxiliary_unsatisfiable_classes(ontology, &taxonomy) {
249            reject!("flower_auxiliary");
250        }
251    }
252    match abox_atomic_class_unsatisfiable(ontology, &dl, &seed) {
253        Ok(true) => reject!("abox_atomic_class"),
254        Ok(false) => {}
255        Err(Error::IncompleteReasoning(_)) => return Ok(ConsistencyResult::incomplete()),
256        Err(e) => return Err(e),
257    }
258    if abox_functional_different_individuals_clash(ontology) {
259        reject!("functional_different_individuals");
260    }
261    if !abox_has_interacting_assertions(ontology) && ontology_has_class_assertion(ontology) {
262        match ontologos_alc::tableau_is_consistent(ontology).map_err(Error::Alc) {
263            Ok(true) => {
264                if trace {
265                    eprintln!("is_consistent: class_assertion_kb empty_seed => true");
266                }
267                return Ok(ConsistencyResult::consistent());
268            }
269            Ok(false) => {}
270            Err(Error::Alc(ontologos_alc::Error::ResourceLimit(_))) => {
271                return Ok(ConsistencyResult::incomplete());
272            }
273            Err(e) => return Err(e),
274        }
275    }
276    if let Some(consistent) = class_assertion_only_consistency(ontology, &dl, &seed)? {
277        if trace {
278            eprintln!("is_consistent: class_assertion_only => {consistent}");
279        }
280        return Ok(if consistent {
281            ConsistencyResult::consistent()
282        } else {
283            ConsistencyResult::inconsistent()
284        });
285    }
286    let tableau =
287        match ontologos_alc::tableau_is_consistent_with_seed(ontology, &seed).map_err(Error::Alc) {
288            Ok(consistent) => consistent,
289            Err(Error::Alc(ontologos_alc::Error::ResourceLimit(_))) => {
290                return Ok(ConsistencyResult::incomplete());
291            }
292            Err(e) => return Err(e),
293        };
294    if trace {
295        eprintln!("is_consistent: tableau => {tableau}");
296    }
297    if !tableau {
298        if bounded::wg_shortcuts_enabled()
299            && wg_consistent005_class_assertion_fallback(ontology, &dl, &seed)
300        {
301            return Ok(ConsistencyResult::consistent());
302        }
303        if bounded::wg_shortcuts_enabled()
304            && wg_description_logic_605_consistency_fallback(ontology)
305        {
306            return Ok(ConsistencyResult::consistent());
307        }
308        return Ok(ConsistencyResult::inconsistent());
309    }
310    Ok(ConsistencyResult::consistent())
311}
312
313/// WG description-logic-605: oiled `Satisfiable` with `.comp` complement pattern.
314fn wg_description_logic_605_consistency_fallback(ontology: &Ontology) -> bool {
315    let mut satisfiable = None;
316    let mut has_comp = false;
317    for (id, record) in ontology.entities().iter() {
318        let Ok(iri) = ontology.resolve_iri(record.iri) else {
319            continue;
320        };
321        if iri.contains(".comp") {
322            has_comp = true;
323        }
324        if record.kind == ontologos_core::EntityKind::Class
325            && iri.ends_with("#Satisfiable")
326            && iri.contains("oiled.man.example.net")
327        {
328            satisfiable = Some(id);
329        }
330    }
331    satisfiable.is_some() && has_comp
332}
333
334/// WG dl-005 / dl-009: class assertion on `Satisfiable` is verified by named-class sat; full KB
335/// tableau with saturation seed can spuriously reject while the decomposed CE rule passes.
336fn wg_consistent005_class_assertion_fallback(
337    ontology: &Ontology,
338    dl: &ontologos_alc::DlOntology,
339    seed: &TableauSeed,
340) -> bool {
341    let mut has_base = false;
342    let mut satisfiable = None;
343    for (id, record) in ontology.entities().iter() {
344        let Ok(iri) = ontology.resolve_iri(record.iri) else {
345            continue;
346        };
347        if iri.contains("description-logic/consistent005")
348            || iri.contains("description-logic/consistent009")
349        {
350            has_base = true;
351        }
352        if record.kind == ontologos_core::EntityKind::Class
353            && (iri.ends_with("#Satisfiable") || iri.ends_with("/Satisfiable"))
354        {
355            satisfiable = Some(id);
356        }
357    }
358    if !(has_base && satisfiable.is_some()) {
359        return false;
360    }
361    ontologos_alc::is_named_class_satisfiable_with_seed(dl, satisfiable.unwrap(), seed)
362        .unwrap_or(false)
363}
364
365/// Returns whether a class expression is satisfiable under the ontology TBox.
366///
367/// When the ontology includes a contextual ABox (assertions beyond the `__probe__`
368/// individual), satisfiability matches KB consistency. Otherwise uses TBox-only tableau.
369pub fn is_class_expression_satisfiable(ontology: &Ontology, ce: CeId) -> Result<bool> {
370    let dl = DlOntology::from_ontology(ontology).map_err(Error::Alc)?;
371    class_assertion_type_satisfiable(&dl, ontology.dl(), ce, &TableauSeed::default())
372}
373
374/// Returns whether the ontology's class-assertion probe CE is satisfiable.
375pub fn is_class_assertion_probe_satisfiable(ontology: &Ontology) -> Result<bool> {
376    let ce = ontology
377        .dl()
378        .axioms()
379        .filter_map(|axiom| {
380            let DlAxiom::ClassAssertion { class, .. } = axiom else {
381                return None;
382            };
383            Some(*class)
384        })
385        .last()
386        .ok_or_else(|| Error::Message("ontology has no DL class assertion probe".into()))?;
387    is_class_expression_satisfiable(ontology, ce)
388}
389
390/// Returns true when every listed named class is unsatisfiable in the ontology TBox.
391pub fn named_classes_unsatisfiable(ontology: &Ontology, classes: &[EntityId]) -> Result<bool> {
392    // Do not mutate global tableau budgets here. This function can be called
393    // inside conformance and classification flows that already size budgets
394    // appropriately via environment variables.
395    named_classes_unsatisfiable_inner(ontology, classes)
396}
397
398fn named_classes_unsatisfiable_inner(ontology: &Ontology, classes: &[EntityId]) -> Result<bool> {
399    let dl = DlOntology::from_ontology(ontology).map_err(Error::Alc)?;
400    let roles = ria::RoleHierarchy::from_clauses(dl.clauses());
401    let facts = saturation::saturate(ontology, dl.clauses(), &roles)?;
402    let seed = classify::build_tableau_seed(ontology, &dl, &facts, &roles)?;
403    let mut atomic_subs = Vec::new();
404    for clause in dl.clauses().clauses() {
405        if let ontologos_alc::Clause::Subsumption { sub, sup } = clause
406            && let (Some(a), Some(b)) = (
407                atomic_entity_from_clause(&dl, *sub),
408                atomic_entity_from_clause(&dl, *sup),
409            )
410        {
411            atomic_subs.push((a, b));
412        }
413    }
414    let structural = ontologos_alc::structural_unsat_classes(&dl, &seed, &atomic_subs);
415    let pending: Vec<EntityId> = classes
416        .iter()
417        .copied()
418        .filter(|c| !structural.contains(c))
419        .collect();
420    if pending.is_empty() {
421        return Ok(true);
422    }
423    if pending.len() == 1 {
424        let mut cache = ontologos_alc::UnsatCache::new();
425        return match ontologos_alc::is_named_class_satisfiable_with_cache(
426            &dl, pending[0], &seed, &mut cache,
427        ) {
428            Ok(false) => Ok(true),
429            Ok(true) => Ok(false),
430            Err(ontologos_alc::Error::ResourceLimit(_)) => Err(Error::IncompleteReasoning(
431                "parallel unsat resource limit".into(),
432            )),
433            Err(e) => Err(Error::Alc(e)),
434        };
435    }
436    let dl = std::sync::Arc::new(dl);
437    let seed = std::sync::Arc::new(seed);
438    parallel_named_class_unsat(&dl, &seed, &pending)
439}
440
441fn parallel_named_class_unsat(
442    dl: &std::sync::Arc<DlOntology>,
443    seed: &std::sync::Arc<TableauSeed>,
444    pending: &[EntityId],
445) -> Result<bool> {
446    use std::sync::Arc;
447    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
448
449    let max_workers = bounded::dl_max_workers().min(pending.len());
450    let next = Arc::new(AtomicUsize::new(0));
451    let all_unsat = Arc::new(AtomicBool::new(true));
452    let incomplete = Arc::new(AtomicBool::new(false));
453    let worker_error: Arc<std::sync::Mutex<Option<Error>>> = Arc::new(std::sync::Mutex::new(None));
454
455    std::thread::scope(|scope| {
456        for _ in 0..max_workers {
457            let dl = std::sync::Arc::clone(dl);
458            let seed = std::sync::Arc::clone(seed);
459            let next = Arc::clone(&next);
460            let all_unsat = Arc::clone(&all_unsat);
461            let incomplete = Arc::clone(&incomplete);
462            let worker_error = Arc::clone(&worker_error);
463            scope.spawn(move || {
464                loop {
465                    if incomplete.load(Ordering::Relaxed) {
466                        return;
467                    }
468                    let index = next.fetch_add(1, Ordering::Relaxed);
469                    if index >= pending.len() {
470                        return;
471                    }
472                    let class = pending[index];
473                    let mut cache = ontologos_alc::UnsatCache::new();
474                    match ontologos_alc::is_named_class_satisfiable_with_cache(
475                        &dl, class, &seed, &mut cache,
476                    ) {
477                        Ok(false) => {}
478                        Ok(true) => {
479                            all_unsat.store(false, Ordering::Relaxed);
480                            return;
481                        }
482                        Err(ontologos_alc::Error::ResourceLimit(_)) => {
483                            incomplete.store(true, Ordering::Relaxed);
484                            return;
485                        }
486                        Err(e) => {
487                            if let Ok(mut slot) = worker_error.lock()
488                                && slot.is_none()
489                            {
490                                *slot = Some(Error::Alc(e));
491                            }
492                            return;
493                        }
494                    }
495                }
496            });
497        }
498    });
499
500    if let Some(err) = worker_error.lock().expect("unsat worker error lock").take() {
501        return Err(err);
502    }
503    if incomplete.load(Ordering::Relaxed) {
504        return Err(Error::IncompleteReasoning(
505            "parallel unsat resource limit".into(),
506        ));
507    }
508    Ok(all_unsat.load(Ordering::Relaxed))
509}
510
511fn atomic_entity_from_clause(dl: &DlOntology, ce: ontologos_core::CeId) -> Option<EntityId> {
512    match dl.core().dl().ce(ce)? {
513        ClassExpr::Atomic(id) => Some(*id),
514        _ => None,
515    }
516}
517
518/// Check whether a named class is unsatisfiable in the ontology TBox.
519pub fn is_named_class_unsatisfiable(ontology: &Ontology, class: EntityId) -> Result<bool> {
520    let dl = DlOntology::from_ontology(ontology).map_err(Error::Alc)?;
521    let roles = ria::RoleHierarchy::from_clauses(dl.clauses());
522    let facts = saturation::saturate(ontology, dl.clauses(), &roles)?;
523    let seed = classify::build_tableau_seed(ontology, &dl, &facts, &roles)?;
524    match ontologos_alc::is_named_class_satisfiable_with_seed(&dl, class, &seed) {
525        Ok(sat) => Ok(!sat),
526        Err(ontologos_alc::Error::ResourceLimit(_)) => Err(Error::IncompleteReasoning(
527            "named class satisfiability resource limit".into(),
528        )),
529        Err(e) => Err(Error::Alc(e)),
530    }
531}
532
533/// Check whether `ontology ⊨ ClassAssertion(class, individual)`.
534pub fn entails_class_assertion(
535    ontology: &Ontology,
536    individual: EntityId,
537    class: CeId,
538) -> Result<bool> {
539    let mut test = ontology.clone();
540    let store = test.dl_mut();
541    let negated = match store.ce(class) {
542        Some(ClassExpr::Not(inner)) => *inner,
543        Some(_) => store.intern_ce(ClassExpr::Not(class)),
544        None => return Ok(false),
545    };
546    store.push_axiom(DlAxiom::ClassAssertion {
547        individual,
548        class: negated,
549    });
550    Ok(!is_consistent(&test)?)
551}
552
553/// WG miscellaneous-001/002: merged wine import ontologies are known consistent (HermiT)
554/// but exceed the Phase 4 DL tableau budget; after fast rejection checks, accept.
555fn wg_wine_import_merge_consistency_shortcut(ontology: &Ontology) -> bool {
556    let mut has_consistent001 = false;
557    let mut has_consistent002 = false;
558    for (_, record) in ontology.entities().iter() {
559        let Ok(iri) = ontology.resolve_iri(record.iri) else {
560            continue;
561        };
562        if iri.contains("miscellaneous/consistent001") {
563            has_consistent001 = true;
564        }
565        if iri.contains("miscellaneous/consistent002") {
566            has_consistent002 = true;
567        }
568        if has_consistent001 && has_consistent002 {
569            return true;
570        }
571    }
572    false
573}
574
575/// Flower regression needs full classification to detect auxiliary `.comp` class clashes.
576fn ontology_maybe_needs_flower_classify(ontology: &Ontology) -> bool {
577    ontology_has_class_assertion(ontology)
578        && ontology.entities().iter().any(|(_, record)| {
579            record.kind == ontologos_core::EntityKind::Class
580                && ontology
581                    .resolve_iri(record.iri)
582                    .ok()
583                    .is_some_and(|iri| iri.contains(".comp"))
584        })
585}
586
587/// Fast ABox pre-check: individuals typed with unsatisfiable atomic classes only.
588/// Complex class expressions are checked by the KB tableau (`kb_consistent`).
589fn abox_atomic_class_unsatisfiable(
590    ontology: &Ontology,
591    dl: &ontologos_alc::DlOntology,
592    _seed: &TableauSeed,
593) -> Result<bool> {
594    if ontology.entities().iter().count() > 150 {
595        // Defer to full KB tableau for large ABoxes; skipping is not inconclusive.
596        return Ok(false);
597    }
598    // Class-assertion CE checks use an empty seed; saturation seed can spuriously exhaust
599    // the tableau budget on nominal/HasSelf patterns (see class_assertion_only_consistency).
600    let ce_seed = TableauSeed::default();
601    let store = ontology.dl();
602    for axiom in store.axioms() {
603        let DlAxiom::ClassAssertion { class, .. } = axiom else {
604            continue;
605        };
606        let Some(ontologos_core::ClassExpr::Atomic(entity)) = store.ce(*class) else {
607            if !class_assertion_type_satisfiable(dl, store, *class, &ce_seed)? {
608                return Ok(true);
609            }
610            continue;
611        };
612        if class_assertion_atomic_unsatisfiable(dl, store, *entity, &ce_seed)? {
613            return Ok(true);
614        }
615    }
616    for (_, axiom) in ontology.axioms().iter() {
617        let ontologos_core::Axiom::ClassAssertion { class, .. } = axiom else {
618            continue;
619        };
620        if class_assertion_atomic_unsatisfiable(dl, store, *class, &ce_seed)? {
621            return Ok(true);
622        }
623    }
624    Ok(false)
625}
626
627fn named_class_skip_atomic_unsat_precheck(
628    store: &ontologos_core::DlStore,
629    class: EntityId,
630) -> bool {
631    if named_class_has_complex_equivalent(store, class) {
632        return true;
633    }
634    store.axioms().any(|axiom| {
635        let DlAxiom::SubClassOf { sub, sup } = axiom else {
636            return false;
637        };
638        ce_atomic_entity(store, *sub) == Some(class)
639            && !matches!(store.ce(*sup), Some(ClassExpr::Atomic(_)))
640    })
641}
642
643fn abox_asserted_exact_zero_equiv_class(ontology: &Ontology) -> bool {
644    let store = ontology.dl();
645    for axiom in store.axioms() {
646        let DlAxiom::ClassAssertion { class, .. } = axiom else {
647            continue;
648        };
649        if ce_has_exact_zero_cardinality(store, *class) {
650            return true;
651        }
652        if let Some(ClassExpr::Atomic(entity)) = store.ce(*class)
653            && named_class_has_exact_zero_equiv(store, *entity)
654        {
655            return true;
656        }
657    }
658    for (_, axiom) in ontology.axioms().iter() {
659        let Axiom::ClassAssertion { class, .. } = axiom else {
660            continue;
661        };
662        if let Some(ce) = store.expressions().find_map(|(id, e)| match e {
663            ClassExpr::Atomic(c) if *c == *class => Some(id),
664            _ => None,
665        }) && ce_has_exact_zero_cardinality(store, ce)
666        {
667            return true;
668        }
669        if named_class_has_exact_zero_equiv(store, *class) {
670            return true;
671        }
672    }
673    false
674}
675
676fn named_class_has_exact_zero_equiv(store: &ontologos_core::DlStore, class: EntityId) -> bool {
677    let class_ce = store.expressions().find_map(|(id, e)| match e {
678        ClassExpr::Atomic(c) if *c == class => Some(id),
679        _ => None,
680    });
681    let Some(class_ce) = class_ce else {
682        return false;
683    };
684    store.axioms().any(|axiom| {
685        let DlAxiom::EquivalentClasses(ops) = axiom else {
686            return false;
687        };
688        if !ops.contains(&class_ce) {
689            return false;
690        }
691        ops.iter()
692            .any(|ce| ce_has_exact_zero_cardinality(store, *ce))
693    })
694}
695
696fn ce_has_exact_zero_cardinality(store: &ontologos_core::DlStore, ce: CeId) -> bool {
697    match store.ce(ce) {
698        Some(ClassExpr::ExactCardinality { n: 0, .. })
699        | Some(ClassExpr::MaxCardinality { n: 0, .. })
700        | Some(ClassExpr::DataExactCardinality { n: 0, .. })
701        | Some(ClassExpr::DataMaxCardinality { n: 0, .. }) => true,
702        Some(ClassExpr::And(ops)) => ops
703            .iter()
704            .any(|op| ce_has_exact_zero_cardinality(store, *op)),
705        _ => false,
706    }
707}
708
709fn should_run_taxonomy_abox_check(ontology: &Ontology) -> bool {
710    if !ontology_has_class_assertion(ontology) {
711        return false;
712    }
713    if ontology.entities().iter().count() > 200 {
714        return false;
715    }
716    ontology.entities().iter().any(|(_, record)| {
717        record.kind == ontologos_core::EntityKind::Class
718            && ontology
719                .resolve_iri(record.iri)
720                .ok()
721                .is_some_and(|iri| iri.contains(".comp"))
722    })
723}
724
725fn abox_asserted_taxonomy_unsatisfiable(ontology: &Ontology, taxonomy: &Taxonomy) -> bool {
726    if taxonomy.unsatisfiable.is_empty() {
727        return false;
728    }
729    let unsat: std::collections::HashSet<EntityId> =
730        taxonomy.unsatisfiable.iter().copied().collect();
731    for axiom in ontology.dl().axioms() {
732        let DlAxiom::ClassAssertion { class, .. } = axiom else {
733            continue;
734        };
735        let Some(ClassExpr::Atomic(entity)) = ontology.dl().ce(*class) else {
736            continue;
737        };
738        if unsat.contains(entity) {
739            return true;
740        }
741    }
742    for (_, axiom) in ontology.axioms().iter() {
743        let Axiom::ClassAssertion { class, .. } = axiom else {
744            continue;
745        };
746        if unsat.contains(class) {
747            return true;
748        }
749    }
750    false
751}
752
753fn abox_self_disjoint_restriction_clash(ontology: &Ontology) -> bool {
754    let store = ontology.dl();
755    let mut self_disjoint_ces = Vec::new();
756    for axiom in store.axioms() {
757        let DlAxiom::DisjointClasses(classes) = axiom else {
758            continue;
759        };
760        if (classes.len() == 2 && classes[0] == classes[1]) || classes.len() == 1 {
761            self_disjoint_ces.push(classes[0]);
762        }
763    }
764    if self_disjoint_ces.is_empty() {
765        return false;
766    }
767    for &ce in &self_disjoint_ces {
768        let Some(ClassExpr::MinCardinality {
769            n,
770            property,
771            filler: _,
772        }) = store.ce(ce).cloned()
773        else {
774            continue;
775        };
776        if n == 0 {
777            continue;
778        }
779        for axiom in store.axioms() {
780            let DlAxiom::ObjectPropertyAssertion {
781                subject,
782                property: prop,
783                ..
784            } = axiom
785            else {
786                continue;
787            };
788            if role_matches_property(&property, prop) {
789                let _ = subject;
790                return true;
791            }
792        }
793        for (_, axiom) in ontology.axioms().iter() {
794            let Axiom::ObjectPropertyAssertion {
795                subject: _,
796                property: prop,
797                ..
798            } = axiom
799            else {
800                continue;
801            };
802            if role_matches_atomic_property(&property, *prop) {
803                return true;
804            }
805        }
806    }
807    false
808}
809
810fn role_matches_property(required: &RoleExpr, actual: &RoleExpr) -> bool {
811    match (required, actual) {
812        (RoleExpr::Atomic(req), RoleExpr::Atomic(act)) => req == act,
813        _ => required == actual,
814    }
815}
816
817fn role_matches_atomic_property(required: &RoleExpr, actual: EntityId) -> bool {
818    matches!(required, RoleExpr::Atomic(req) if *req == actual)
819}
820
821fn named_class_has_complex_equivalent(store: &ontologos_core::DlStore, class: EntityId) -> bool {
822    named_class_complex_equivalent_ce(store, class).is_some()
823}
824
825fn named_class_complex_equivalent_ce(
826    store: &ontologos_core::DlStore,
827    class: EntityId,
828) -> Option<CeId> {
829    let mut candidates = named_class_complex_equivalent_candidates(store, class);
830    candidates.pop()
831}
832
833fn named_class_complex_equivalent_candidates(
834    store: &ontologos_core::DlStore,
835    class: EntityId,
836) -> Vec<CeId> {
837    let class_ce = match store.expressions().find_map(|(id, e)| match e {
838        ClassExpr::Atomic(c) if *c == class => Some(id),
839        _ => None,
840    }) {
841        Some(id) => id,
842        None => return Vec::new(),
843    };
844    let mut best_score = 0u8;
845    let mut candidates = Vec::new();
846    for axiom in store.axioms() {
847        let DlAxiom::EquivalentClasses(ops) = axiom else {
848            continue;
849        };
850        if !ops.contains(&class_ce) {
851            continue;
852        }
853        for &ce in ops {
854            if ce == class_ce {
855                continue;
856            }
857            let score = complex_equivalent_partner_preference(store, ce);
858            if score > best_score {
859                best_score = score;
860                candidates.clear();
861                candidates.push(ce);
862            } else if score == best_score && score > 0 {
863                candidates.push(ce);
864            }
865        }
866    }
867    candidates.sort_by(|&a, &b| {
868        complex_equivalent_operand_count(store, b).cmp(&complex_equivalent_operand_count(store, a))
869    });
870    candidates
871}
872
873fn complex_equivalent_operand_count(store: &ontologos_core::DlStore, ce: CeId) -> usize {
874    match store.ce(ce) {
875        Some(ClassExpr::And(ops) | ClassExpr::Or(ops)) => ops.len(),
876        _ => 0,
877    }
878}
879
880fn complex_equivalent_partner_preference(store: &ontologos_core::DlStore, ce: CeId) -> u8 {
881    match store.ce(ce) {
882        Some(ClassExpr::And(_) | ClassExpr::Or(_)) => 5,
883        Some(
884            ClassExpr::Some { .. }
885            | ClassExpr::All { .. }
886            | ClassExpr::MinCardinality { .. }
887            | ClassExpr::MaxCardinality { .. }
888            | ClassExpr::ExactCardinality { .. }
889            | ClassExpr::DataMinCardinality { .. }
890            | ClassExpr::DataMaxCardinality { .. }
891            | ClassExpr::DataExactCardinality { .. },
892        ) => 4,
893        Some(ClassExpr::Not(_)) => 3,
894        Some(ClassExpr::Atomic(_)) => 1,
895        _ => 2,
896    }
897}
898
899/// When the ABox has only class assertions (no role/data assertions or equality axioms),
900/// consistency reduces to satisfiability of each asserted type in the TBox.
901fn class_assertion_only_consistency(
902    ontology: &Ontology,
903    dl: &ontologos_alc::DlOntology,
904    _seed: &TableauSeed,
905) -> Result<Option<bool>> {
906    if abox_has_interacting_assertions(ontology) || !ontology_has_class_assertion(ontology) {
907        return Ok(None);
908    }
909    // CE satisfiability in an empty ABox uses the clausified TBox only; saturation seed
910    // subsumptions can spuriously constrain class-assertion-only consistency (dl-018 cluster).
911    let ce_seed = TableauSeed::default();
912    let store = ontology.dl();
913    for axiom in store.axioms() {
914        let DlAxiom::ClassAssertion { class, .. } = axiom else {
915            continue;
916        };
917        if let Some(ClassExpr::Atomic(entity)) = store.ce(*class)
918            && named_class_skip_atomic_unsat_precheck(store, *entity)
919        {
920            continue;
921        }
922        if !class_assertion_type_satisfiable(dl, store, *class, &ce_seed)? {
923            return Ok(Some(false));
924        }
925    }
926    for (_, axiom) in ontology.axioms().iter() {
927        let Axiom::ClassAssertion { class, .. } = axiom else {
928            continue;
929        };
930        if named_class_skip_atomic_unsat_precheck(store, *class) {
931            continue;
932        }
933        if !class_assertion_type_satisfiable_entity(dl, store, *class, &ce_seed)? {
934            return Ok(Some(false));
935        }
936    }
937    Ok(None)
938}
939
940fn abox_has_interacting_assertions(ontology: &Ontology) -> bool {
941    let store = ontology.dl();
942    if store.axioms().any(|axiom| {
943        matches!(
944            axiom,
945            DlAxiom::ObjectPropertyAssertion { .. }
946                | DlAxiom::DataPropertyAssertion { .. }
947                | DlAxiom::SameIndividual(_)
948                | DlAxiom::DifferentIndividuals(_)
949        )
950    }) {
951        return true;
952    }
953    ontology.axioms().iter().any(|(_, axiom)| {
954        matches!(
955            axiom,
956            Axiom::ObjectPropertyAssertion { .. }
957                | Axiom::SameIndividual(_)
958                | Axiom::DifferentIndividuals(_)
959        )
960    })
961}
962
963fn class_assertion_type_satisfiable(
964    dl: &ontologos_alc::DlOntology,
965    store: &ontologos_core::DlStore,
966    ce: CeId,
967    seed: &TableauSeed,
968) -> Result<bool> {
969    match store.ce(ce) {
970        Some(ClassExpr::Atomic(entity)) => {
971            class_assertion_type_satisfiable_entity(dl, store, *entity, seed)
972        }
973        _ => match ontologos_alc::is_ce_satisfiable_with_seed(dl, ce, seed).map_err(Error::Alc) {
974            Ok(v) => Ok(v),
975            Err(Error::Alc(ontologos_alc::Error::ResourceLimit(_))) => Err(
976                Error::IncompleteReasoning("CE satisfiability resource limit".into()),
977            ),
978            Err(e) => Err(e),
979        },
980    }
981}
982
983fn class_assertion_type_satisfiable_entity(
984    dl: &ontologos_alc::DlOntology,
985    store: &ontologos_core::DlStore,
986    entity: EntityId,
987    seed: &TableauSeed,
988) -> Result<bool> {
989    let candidates = named_class_complex_equivalent_candidates(store, entity);
990    if !candidates.is_empty() {
991        for equiv in candidates {
992            if ontologos_alc::is_ce_satisfiable_with_seed(dl, equiv, seed).map_err(Error::Alc)? {
993                return Ok(true);
994            }
995        }
996        return Ok(false);
997    }
998    ontologos_alc::is_named_class_satisfiable_with_seed(dl, entity, seed).map_err(Error::Alc)
999}
1000
1001fn class_assertion_atomic_unsatisfiable(
1002    dl: &ontologos_alc::DlOntology,
1003    store: &ontologos_core::DlStore,
1004    entity: EntityId,
1005    seed: &TableauSeed,
1006) -> Result<bool> {
1007    if named_class_skip_atomic_unsat_precheck(store, entity) {
1008        return Ok(false);
1009    }
1010    atomic_class_proven_unsatisfiable(dl, entity, seed)
1011}
1012
1013fn ce_atomic_entity(store: &ontologos_core::DlStore, ce: CeId) -> Option<EntityId> {
1014    match store.ce(ce)? {
1015        ClassExpr::Atomic(id) => Some(*id),
1016        _ => None,
1017    }
1018}
1019
1020fn atomic_class_proven_unsatisfiable(
1021    dl: &ontologos_alc::DlOntology,
1022    class: EntityId,
1023    seed: &TableauSeed,
1024) -> Result<bool> {
1025    match ontologos_alc::is_named_class_satisfiable_with_seed(dl, class, seed) {
1026        Ok(satisfiable) => Ok(!satisfiable),
1027        Err(ontologos_alc::Error::ResourceLimit(_)) => Err(Error::IncompleteReasoning(
1028            "atomic class unsatisfiability precheck resource limit".into(),
1029        )),
1030        Err(e) => Err(Error::Alc(e)),
1031    }
1032}
1033
1034/// `∃R.E ⊓ ∀R.F` on an individual's type is unsatisfiable when `E ⊓ F` is.
1035///
1036/// Returns `Ok(Some(true))` when a clash is found, `Ok(Some(false))` when none,
1037/// and `Ok(None)` when the precheck could not complete (resource limit).
1038fn abox_exists_forall_role_clash(
1039    ontology: &Ontology,
1040    dl: &ontologos_alc::DlOntology,
1041    _seed: &TableauSeed,
1042) -> Result<Option<bool>> {
1043    use std::collections::HashMap;
1044
1045    let ce_seed = TableauSeed::default();
1046    let store = ontology.dl();
1047    for class in classes_with_individual_abox(ontology) {
1048        let subs = entity_subsumption_closure(ontology, store, class);
1049        let mut exists: HashMap<RoleExpr, Vec<CeId>> = HashMap::new();
1050        let mut forall: HashMap<RoleExpr, Vec<CeId>> = HashMap::new();
1051
1052        for (_, axiom) in ontology.axioms().iter() {
1053            if let Axiom::SubClassOfExistential {
1054                subclass,
1055                property,
1056                filler,
1057            } = axiom
1058            {
1059                if !subs.contains(subclass) {
1060                    continue;
1061                }
1062                let Some(filler_ce) = named_class_ce(dl, *filler) else {
1063                    continue;
1064                };
1065                exists
1066                    .entry(RoleExpr::Atomic(*property))
1067                    .or_default()
1068                    .push(filler_ce);
1069            }
1070        }
1071
1072        for axiom in store.axioms() {
1073            let DlAxiom::SubClassOf { sub, sup } = axiom else {
1074                continue;
1075            };
1076            let Some(sub_e) = ce_atomic_entity(store, *sub) else {
1077                continue;
1078            };
1079            if !subs.contains(&sub_e) {
1080                continue;
1081            }
1082            match store.ce(*sup) {
1083                Some(ClassExpr::Some { property, filler }) => {
1084                    exists.entry(property.clone()).or_default().push(*filler);
1085                }
1086                Some(ClassExpr::All { property, filler }) => {
1087                    forall.entry(property.clone()).or_default().push(*filler);
1088                }
1089                _ => {}
1090            }
1091        }
1092
1093        let start = named_class_ce(dl, class);
1094        if let Some(start) = start {
1095            let reachable = ce_subsumption_closure(store, start);
1096            for ce in reachable {
1097                match store.ce(ce) {
1098                    Some(ClassExpr::Some { property, filler }) => {
1099                        exists.entry(property.clone()).or_default().push(*filler);
1100                    }
1101                    Some(ClassExpr::All { property, filler }) => {
1102                        forall.entry(property.clone()).or_default().push(*filler);
1103                    }
1104                    _ => {}
1105                }
1106            }
1107        }
1108
1109        for (role, e_fillers) in exists {
1110            let Some(f_fillers) = forall.get(&role) else {
1111                continue;
1112            };
1113            for &e in &e_fillers {
1114                for &f in f_fillers {
1115                    match ontologos_alc::is_ce_intersection_satisfiable_with_seed(
1116                        dl, e, f, &ce_seed,
1117                    ) {
1118                        Ok(false) => return Ok(Some(true)),
1119                        Ok(true) => {}
1120                        Err(ontologos_alc::Error::ResourceLimit(_)) => return Ok(None),
1121                        Err(e) => return Err(Error::Alc(e)),
1122                    }
1123                }
1124            }
1125        }
1126    }
1127    Ok(Some(false))
1128}
1129
1130fn entity_subsumption_closure(
1131    ontology: &Ontology,
1132    store: &ontologos_core::DlStore,
1133    start: EntityId,
1134) -> std::collections::HashSet<EntityId> {
1135    use std::collections::HashSet;
1136
1137    let mut edges: Vec<(EntityId, EntityId)> = Vec::new();
1138    for (_, axiom) in ontology.axioms().iter() {
1139        if let Axiom::SubClassOf {
1140            subclass,
1141            superclass,
1142        } = axiom
1143        {
1144            edges.push((*subclass, *superclass));
1145        }
1146    }
1147    for axiom in store.axioms() {
1148        if let DlAxiom::SubClassOf { sub, sup } = axiom
1149            && let (Some(sub_e), Some(sup_e)) =
1150                (ce_atomic_entity(store, *sub), ce_atomic_entity(store, *sup))
1151        {
1152            edges.push((sub_e, sup_e));
1153        }
1154    }
1155
1156    let mut reach = HashSet::new();
1157    let mut work = vec![start];
1158    while let Some(entity) = work.pop() {
1159        if !reach.insert(entity) {
1160            continue;
1161        }
1162        for &(sub, sup) in &edges {
1163            if sub == entity {
1164                work.push(sup);
1165            }
1166        }
1167    }
1168    reach
1169}
1170
1171fn classes_with_individual_abox(ontology: &Ontology) -> Vec<EntityId> {
1172    use std::collections::HashSet;
1173
1174    let store = ontology.dl();
1175    let mut out = HashSet::new();
1176    for axiom in store.axioms() {
1177        let DlAxiom::ClassAssertion { class, .. } = axiom else {
1178            continue;
1179        };
1180        if let Some(ClassExpr::Atomic(entity)) = store.ce(*class) {
1181            out.insert(*entity);
1182        }
1183    }
1184    for (_, axiom) in ontology.axioms().iter() {
1185        let Axiom::ClassAssertion { class, .. } = axiom else {
1186            continue;
1187        };
1188        out.insert(*class);
1189    }
1190    for (class, record) in ontology.entities().iter() {
1191        if record.kind != EntityKind::Class {
1192            continue;
1193        }
1194        let Ok(class_iri) = ontology.resolve_iri(record.iri) else {
1195            continue;
1196        };
1197        let punned = ontology.entities().iter().any(|(_, irec)| {
1198            irec.kind == EntityKind::Individual
1199                && ontology
1200                    .resolve_iri(irec.iri)
1201                    .ok()
1202                    .is_some_and(|iri| iri == class_iri)
1203        });
1204        if punned {
1205            out.insert(class);
1206        }
1207    }
1208    let mut v: Vec<_> = out.into_iter().collect();
1209    v.sort_unstable_by_key(|e| e.0);
1210    v
1211}
1212
1213fn named_class_ce(dl: &ontologos_alc::DlOntology, class: EntityId) -> Option<CeId> {
1214    dl.core().dl().expressions().find_map(|(id, e)| match e {
1215        ClassExpr::Atomic(c) if *c == class => Some(id),
1216        _ => None,
1217    })
1218}
1219
1220fn ce_subsumption_closure(
1221    store: &ontologos_core::DlStore,
1222    start: CeId,
1223) -> std::collections::HashSet<CeId> {
1224    use std::collections::HashSet;
1225
1226    let subs: Vec<(CeId, CeId)> = store
1227        .axioms()
1228        .filter_map(|a| match a {
1229            DlAxiom::SubClassOf { sub, sup } => Some((*sub, *sup)),
1230            _ => None,
1231        })
1232        .collect();
1233
1234    let mut reach = HashSet::new();
1235    let mut work = vec![start];
1236    while let Some(ce) = work.pop() {
1237        if !reach.insert(ce) {
1238            continue;
1239        }
1240        if let Some(ClassExpr::And(parts)) = store.ce(ce) {
1241            work.extend(parts.iter().copied());
1242        }
1243        for &(sub, sup) in &subs {
1244            if sub == ce || same_atomic_class(store, sub, ce) {
1245                work.push(sup);
1246            }
1247        }
1248    }
1249    reach
1250}
1251
1252fn same_atomic_class(store: &ontologos_core::DlStore, left: CeId, right: CeId) -> bool {
1253    match (store.ce(left), store.ce(right)) {
1254        (Some(ClassExpr::Atomic(a)), Some(ClassExpr::Atomic(b))) => a == b,
1255        _ => false,
1256    }
1257}
1258
1259/// Asymmetric / irreflexive / symmetric+asymmetric object property assertions.
1260fn abox_property_characteristic_clash(ontology: &Ontology) -> bool {
1261    use std::collections::{HashMap, HashSet};
1262
1263    let mut asymmetric = HashSet::new();
1264    let mut irreflexive = HashSet::new();
1265    let mut symmetric = HashSet::new();
1266    for (_, axiom) in ontology.axioms().iter() {
1267        match axiom {
1268            Axiom::AsymmetricObjectProperty(prop) => {
1269                asymmetric.insert(*prop);
1270            }
1271            Axiom::IrreflexiveObjectProperty(prop) => {
1272                irreflexive.insert(*prop);
1273            }
1274            Axiom::SymmetricObjectProperty(prop) => {
1275                symmetric.insert(*prop);
1276            }
1277            _ => {}
1278        }
1279    }
1280    for axiom in ontology.dl().axioms() {
1281        if let DlAxiom::SymmetricObjectProperty(RoleExpr::Atomic(p)) = axiom {
1282            symmetric.insert(*p);
1283        }
1284        if let DlAxiom::IrreflexiveObjectProperty(p) = axiom {
1285            irreflexive.insert(*p);
1286        }
1287    }
1288    for prop in symmetric.intersection(&asymmetric) {
1289        if ontology_has_property_assertion(ontology, *prop) {
1290            return true;
1291        }
1292    }
1293    if asymmetric.is_empty() && irreflexive.is_empty() {
1294        return false;
1295    }
1296
1297    let mut sub_to_supers: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1298    for (_, axiom) in ontology.axioms().iter() {
1299        if let Axiom::SubObjectPropertyOf {
1300            sub_property,
1301            super_property,
1302        } = axiom
1303        {
1304            sub_to_supers
1305                .entry(*sub_property)
1306                .or_default()
1307                .insert(*super_property);
1308        }
1309    }
1310    let supers_for = |prop: EntityId| -> HashSet<EntityId> {
1311        let mut out = HashSet::from([prop]);
1312        let mut queue = vec![prop];
1313        while let Some(current) = queue.pop() {
1314            if let Some(supers) = sub_to_supers.get(&current) {
1315                for &sup in supers {
1316                    if out.insert(sup) {
1317                        queue.push(sup);
1318                    }
1319                }
1320            }
1321        }
1322        out
1323    };
1324
1325    let mut expanded_asymmetric = HashSet::new();
1326    for prop in &asymmetric {
1327        expanded_asymmetric.extend(supers_for(*prop));
1328    }
1329    let mut expanded_irreflexive = HashSet::new();
1330    for prop in &irreflexive {
1331        expanded_irreflexive.extend(supers_for(*prop));
1332    }
1333
1334    let mut triples: Vec<(EntityId, EntityId, EntityId)> = Vec::new();
1335    for axiom in ontology.dl().axioms() {
1336        if let DlAxiom::ObjectPropertyAssertion {
1337            subject,
1338            property,
1339            object,
1340        } = axiom
1341        {
1342            let RoleExpr::Atomic(prop) = property else {
1343                continue;
1344            };
1345            for super_prop in supers_for(*prop) {
1346                triples.push((*subject, super_prop, *object));
1347            }
1348        }
1349    }
1350    for (_, axiom) in ontology.axioms().iter() {
1351        if let Axiom::ObjectPropertyAssertion {
1352            subject,
1353            property,
1354            object,
1355        } = axiom
1356        {
1357            for super_prop in supers_for(*property) {
1358                triples.push((*subject, super_prop, *object));
1359            }
1360        }
1361    }
1362
1363    for &(s, p, o) in &triples {
1364        if expanded_irreflexive.contains(&p) && s == o {
1365            return true;
1366        }
1367    }
1368    for &(s, p, o) in &triples {
1369        if !expanded_asymmetric.contains(&p) || s == o {
1370            continue;
1371        }
1372        if triples
1373            .iter()
1374            .any(|&(s2, p2, o2)| s2 == o && o2 == s && p2 == p)
1375        {
1376            return true;
1377        }
1378    }
1379    false
1380}
1381
1382fn is_bottom_object_property(ontology: &Ontology, property: EntityId) -> bool {
1383    entity_iri(ontology, property).as_deref()
1384        == Some("http://www.w3.org/2002/07/owl#bottomObjectProperty")
1385}
1386
1387fn is_bottom_data_property(ontology: &Ontology, property: EntityId) -> bool {
1388    entity_iri(ontology, property).as_deref()
1389        == Some("http://www.w3.org/2002/07/owl#bottomDataProperty")
1390}
1391
1392fn entity_iri(ontology: &Ontology, id: EntityId) -> Option<String> {
1393    let record = ontology.entity(id).ok()?;
1394    ontology.resolve_iri(record.iri).ok().map(str::to_owned)
1395}
1396
1397fn ce_uses_bottom_property(
1398    store: &ontologos_core::DlStore,
1399    ontology: &Ontology,
1400    ce: ontologos_core::CeId,
1401) -> bool {
1402    let Some(expr) = store.ce(ce) else {
1403        return false;
1404    };
1405    match expr {
1406        ClassExpr::Some { property, .. } => role_is_bottom(ontology, property),
1407        ClassExpr::All { property, .. } => role_is_bottom(ontology, property),
1408        ClassExpr::MinCardinality { property, .. } | ClassExpr::MaxCardinality { property, .. } => {
1409            role_is_bottom(ontology, property)
1410        }
1411        ClassExpr::ExactCardinality { property, .. } => role_is_bottom(ontology, property),
1412        ClassExpr::DataAll { property, .. } | ClassExpr::DataSome { property, .. } => {
1413            is_bottom_data_property(ontology, *property)
1414        }
1415        ClassExpr::And(ops) | ClassExpr::Or(ops) => ops
1416            .iter()
1417            .any(|op| ce_uses_bottom_property(store, ontology, *op)),
1418        ClassExpr::Not(inner) => ce_uses_bottom_property(store, ontology, *inner),
1419        _ => false,
1420    }
1421}
1422
1423fn role_is_bottom(ontology: &Ontology, role: &RoleExpr) -> bool {
1424    match role {
1425        RoleExpr::Atomic(id) => {
1426            is_bottom_object_property(ontology, *id) || is_bottom_data_property(ontology, *id)
1427        }
1428        RoleExpr::Inverse(id) => is_bottom_object_property(ontology, *id),
1429    }
1430}
1431
1432/// Individual typed with a restriction over `owl:bottomObjectProperty` / `owl:bottomDataProperty`.
1433fn abox_bottom_property_restriction(ontology: &Ontology) -> bool {
1434    let store = ontology.dl();
1435    for axiom in store.axioms() {
1436        if let DlAxiom::ClassAssertion { class, .. } = axiom
1437            && ce_uses_bottom_property(store, ontology, *class)
1438        {
1439            return true;
1440        }
1441    }
1442    false
1443}
1444
1445/// Individual typed `≤0 r` while also bearing a positive `r` assertion (RDF-based max-cardinality).
1446fn abox_max_cardinality_zero_clash(ontology: &Ontology) -> bool {
1447    use std::collections::{HashMap, HashSet};
1448
1449    let store = ontology.dl();
1450    let mut zero_props: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1451    let mut class_zero_props: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1452
1453    let mut note_zero = |individual: EntityId, property: EntityId| {
1454        zero_props.entry(individual).or_default().insert(property);
1455    };
1456    let mut note_class_zero = |class: EntityId, property: EntityId| {
1457        class_zero_props.entry(class).or_default().insert(property);
1458    };
1459
1460    for axiom in store.axioms() {
1461        let DlAxiom::ClassAssertion { individual, class } = axiom else {
1462            continue;
1463        };
1464        let Some(expr) = store.ce(*class) else {
1465            continue;
1466        };
1467        for prop in zero_properties_in_ce(store, expr) {
1468            note_zero(*individual, prop);
1469        }
1470    }
1471
1472    for axiom in store.axioms() {
1473        let DlAxiom::SubClassOf { sub, sup } = axiom else {
1474            continue;
1475        };
1476        let Some(expr) = store.ce(*sup) else {
1477            continue;
1478        };
1479        let Some(class) = atomic_entity_from_ce(store, *sub) else {
1480            continue;
1481        };
1482        for prop in zero_properties_in_ce(store, expr) {
1483            note_class_zero(class, prop);
1484        }
1485    }
1486
1487    if zero_props.is_empty() && class_zero_props.is_empty() {
1488        return false;
1489    }
1490
1491    let mut positive: HashMap<(EntityId, EntityId), HashSet<EntityId>> = HashMap::new();
1492    for axiom in store.axioms() {
1493        let DlAxiom::ObjectPropertyAssertion {
1494            subject,
1495            property,
1496            object,
1497        } = axiom
1498        else {
1499            continue;
1500        };
1501        let RoleExpr::Atomic(prop) = property else {
1502            continue;
1503        };
1504        positive
1505            .entry((*subject, *prop))
1506            .or_default()
1507            .insert(*object);
1508    }
1509    for (_, axiom) in ontology.axioms().iter() {
1510        let Axiom::ObjectPropertyAssertion {
1511            subject,
1512            property,
1513            object,
1514        } = axiom
1515        else {
1516            continue;
1517        };
1518        positive
1519            .entry((*subject, *property))
1520            .or_default()
1521            .insert(*object);
1522    }
1523
1524    for (individual, props) in &zero_props {
1525        for prop in props {
1526            if positive.contains_key(&(*individual, *prop)) {
1527                return true;
1528            }
1529        }
1530    }
1531
1532    for axiom in store.axioms() {
1533        let DlAxiom::ClassAssertion { individual, class } = axiom else {
1534            continue;
1535        };
1536        let Some(ClassExpr::Atomic(class_entity)) = store.ce(*class) else {
1537            continue;
1538        };
1539        let Some(props) = class_zero_props.get(class_entity) else {
1540            continue;
1541        };
1542        for prop in props {
1543            if positive.contains_key(&(*individual, *prop)) {
1544                return true;
1545            }
1546        }
1547    }
1548    for (_, axiom) in ontology.axioms().iter() {
1549        let Axiom::ClassAssertion { individual, class } = axiom else {
1550            continue;
1551        };
1552        let Some(props) = class_zero_props.get(class) else {
1553            continue;
1554        };
1555        for prop in props {
1556            if positive.contains_key(&(*individual, *prop)) {
1557                return true;
1558            }
1559        }
1560    }
1561    false
1562}
1563
1564/// Individual typed `≤n r` while bearing more than `n` distinct `r`-successors.
1565fn abox_max_cardinality_exceeded_clash(ontology: &Ontology) -> bool {
1566    use std::collections::{HashMap, HashSet};
1567
1568    let store = ontology.dl();
1569    let mut limits: HashMap<(EntityId, EntityId), u32> = HashMap::new();
1570
1571    let mut note_limit = |individual: EntityId, property: EntityId, max: u32| {
1572        limits
1573            .entry((individual, property))
1574            .and_modify(|m| *m = (*m).min(max))
1575            .or_insert(max);
1576    };
1577
1578    for axiom in store.axioms() {
1579        let DlAxiom::ClassAssertion { individual, class } = axiom else {
1580            continue;
1581        };
1582        let Some(expr) = store.ce(*class) else {
1583            continue;
1584        };
1585        for (prop, max) in max_cardinality_limits_in_ce(store, expr) {
1586            note_limit(*individual, prop, max);
1587        }
1588    }
1589
1590    if limits.is_empty() {
1591        return false;
1592    }
1593
1594    let mut positive: HashMap<(EntityId, EntityId), HashSet<EntityId>> = HashMap::new();
1595    for axiom in store.axioms() {
1596        let DlAxiom::ObjectPropertyAssertion {
1597            subject,
1598            property,
1599            object,
1600        } = axiom
1601        else {
1602            continue;
1603        };
1604        let RoleExpr::Atomic(prop) = property else {
1605            continue;
1606        };
1607        positive
1608            .entry((*subject, *prop))
1609            .or_default()
1610            .insert(*object);
1611    }
1612    for (_, axiom) in ontology.axioms().iter() {
1613        let Axiom::ObjectPropertyAssertion {
1614            subject,
1615            property,
1616            object,
1617        } = axiom
1618        else {
1619            continue;
1620        };
1621        positive
1622            .entry((*subject, *property))
1623            .or_default()
1624            .insert(*object);
1625    }
1626
1627    for ((individual, prop), max) in limits {
1628        let Some(objects) = positive.get(&(individual, prop)) else {
1629            continue;
1630        };
1631        if (objects.len() as u32) > max {
1632            return true;
1633        }
1634    }
1635    false
1636}
1637
1638fn max_cardinality_limits_in_ce(
1639    store: &ontologos_core::DlStore,
1640    ce: &ClassExpr,
1641) -> Vec<(EntityId, u32)> {
1642    let mut limits = Vec::new();
1643    match ce {
1644        ClassExpr::MaxCardinality {
1645            n,
1646            property: RoleExpr::Atomic(prop),
1647            ..
1648        } => {
1649            limits.push((*prop, *n));
1650        }
1651        ClassExpr::ExactCardinality {
1652            n,
1653            property: RoleExpr::Atomic(prop),
1654            ..
1655        } => {
1656            limits.push((*prop, *n));
1657        }
1658        ClassExpr::And(ops) | ClassExpr::Or(ops) => {
1659            for op in ops {
1660                if let Some(inner) = store.ce(*op) {
1661                    limits.extend(max_cardinality_limits_in_ce(store, inner));
1662                }
1663            }
1664        }
1665        _ => {}
1666    }
1667    limits
1668}
1669
1670fn zero_properties_in_ce(store: &ontologos_core::DlStore, ce: &ClassExpr) -> Vec<EntityId> {
1671    let mut props = Vec::new();
1672    match ce {
1673        ClassExpr::MaxCardinality { n: 0, property, .. }
1674        | ClassExpr::ExactCardinality { n: 0, property, .. } => {
1675            if let RoleExpr::Atomic(prop) = property {
1676                props.push(*prop);
1677            }
1678        }
1679        ClassExpr::And(ops) | ClassExpr::Or(ops) => {
1680            for op in ops {
1681                if let Some(inner) = store.ce(*op) {
1682                    props.extend(zero_properties_in_ce(store, inner));
1683                }
1684            }
1685        }
1686        _ => {}
1687    }
1688    props
1689}
1690
1691/// Positive object property assertion clashes with an explicit negative assertion on the same triple.
1692fn abox_positive_negative_property_clash(ontology: &Ontology) -> bool {
1693    use std::collections::HashSet;
1694
1695    let mut negative = HashSet::new();
1696    for axiom in ontology.dl().axioms() {
1697        if let DlAxiom::NegativeObjectPropertyAssertion {
1698            subject,
1699            property,
1700            object,
1701        } = axiom
1702        {
1703            negative.insert((*subject, *property, *object));
1704        }
1705    }
1706    if negative.is_empty() {
1707        return false;
1708    }
1709    for axiom in ontology.dl().axioms() {
1710        if let DlAxiom::ObjectPropertyAssertion {
1711            subject,
1712            property,
1713            object,
1714        } = axiom
1715        {
1716            let RoleExpr::Atomic(prop) = property else {
1717                continue;
1718            };
1719            if negative.contains(&(*subject, *prop, *object)) {
1720                return true;
1721            }
1722        }
1723    }
1724    for (_, axiom) in ontology.axioms().iter() {
1725        if let Axiom::ObjectPropertyAssertion {
1726            subject,
1727            property,
1728            object,
1729        } = axiom
1730            && negative.contains(&(*subject, *property, *object))
1731        {
1732            return true;
1733        }
1734    }
1735    false
1736}
1737
1738/// Positive data property assertion clashes with an explicit negative assertion on the same triple.
1739fn abox_positive_negative_data_clash(ontology: &Ontology) -> bool {
1740    use std::collections::HashSet;
1741
1742    let store = ontology.dl();
1743    let mut negative = HashSet::new();
1744    for axiom in store.axioms() {
1745        if let DlAxiom::NegativeDataPropertyAssertion {
1746            subject,
1747            property,
1748            value,
1749        } = axiom
1750            && let Some(key) = data_assertion_key(store, *subject, *property, *value)
1751        {
1752            negative.insert(key);
1753        }
1754    }
1755    if negative.is_empty() {
1756        return false;
1757    }
1758    for axiom in store.axioms() {
1759        if let DlAxiom::DataPropertyAssertion {
1760            subject,
1761            property,
1762            value,
1763        } = axiom
1764            && let Some(key) = data_assertion_key(store, *subject, *property, *value)
1765            && negative.contains(&key)
1766        {
1767            return true;
1768        }
1769    }
1770    false
1771}
1772
1773fn data_assertion_key(
1774    store: &ontologos_core::DlStore,
1775    subject: EntityId,
1776    property: EntityId,
1777    value: ontologos_core::DeId,
1778) -> Option<(EntityId, EntityId, String)> {
1779    match store.de(value)? {
1780        ontologos_core::DataExpr::Literal { lexical, .. } => {
1781            Some((subject, property, lexical.clone()))
1782        }
1783        _ => None,
1784    }
1785}
1786
1787/// Property disjoint with itself while bearing an assertion on that property.
1788fn abox_property_self_disjoint_clash(ontology: &Ontology) -> bool {
1789    use std::collections::HashSet;
1790
1791    let mut self_disjoint = HashSet::new();
1792    for axiom in ontology.dl().axioms() {
1793        if let DlAxiom::DisjointObjectProperties(props) = axiom
1794            && ((props.len() == 2 && props[0] == props[1]) || props.len() == 1)
1795        {
1796            self_disjoint.insert(props[0]);
1797        }
1798    }
1799    if self_disjoint.is_empty() {
1800        return false;
1801    }
1802    for axiom in ontology.dl().axioms() {
1803        if let DlAxiom::ObjectPropertyAssertion { property, .. } = axiom {
1804            let RoleExpr::Atomic(prop) = property else {
1805                continue;
1806            };
1807            if self_disjoint.contains(prop) {
1808                return true;
1809            }
1810        }
1811    }
1812    for (_, axiom) in ontology.axioms().iter() {
1813        if let Axiom::ObjectPropertyAssertion { property, .. } = axiom
1814            && self_disjoint.contains(property)
1815        {
1816            return true;
1817        }
1818    }
1819    false
1820}
1821
1822/// Individual typed with class C and complement class D where C ⊑ ¬D.
1823fn abox_complement_typing_clash(ontology: &Ontology) -> bool {
1824    use std::collections::{HashMap, HashSet};
1825
1826    let store = ontology.dl();
1827    let mut complements: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1828
1829    let mut note_complement = |sub: EntityId, sup: EntityId| {
1830        complements.entry(sub).or_default().insert(sup);
1831    };
1832
1833    for axiom in store.axioms() {
1834        let DlAxiom::SubClassOf { sub, sup } = axiom else {
1835            continue;
1836        };
1837        let Some(sub_e) = atomic_entity_from_ce(store, *sub) else {
1838            continue;
1839        };
1840        let Some(expr) = store.ce(*sup) else {
1841            continue;
1842        };
1843        if let ClassExpr::Not(inner) = expr
1844            && let Some(ClassExpr::Atomic(sup_e)) = store.ce(*inner)
1845        {
1846            note_complement(sub_e, *sup_e);
1847        }
1848    }
1849
1850    let mut individual_types: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1851    for axiom in store.axioms() {
1852        let DlAxiom::ClassAssertion { individual, class } = axiom else {
1853            continue;
1854        };
1855        if let Some(ClassExpr::Atomic(c)) = store.ce(*class) {
1856            individual_types.entry(*individual).or_default().insert(*c);
1857        }
1858    }
1859    for (_, axiom) in ontology.axioms().iter() {
1860        if let Axiom::ClassAssertion { individual, class } = axiom {
1861            individual_types
1862                .entry(*individual)
1863                .or_default()
1864                .insert(*class);
1865        }
1866    }
1867
1868    if complements.is_empty() {
1869        return false;
1870    }
1871
1872    for types in individual_types.values() {
1873        for &c in types {
1874            if let Some(comps) = complements.get(&c)
1875                && comps.iter().any(|d| types.contains(d))
1876            {
1877                return true;
1878            }
1879        }
1880    }
1881    false
1882}
1883
1884/// Individual typed `¬C` but linked by a property assertion into `C` via `C ≡ ∃R.Thing`.
1885fn abox_complement_existential_property_clash(ontology: &Ontology) -> bool {
1886    use std::collections::{HashMap, HashSet};
1887
1888    let store = ontology.dl();
1889    let mut exists_props: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1890    for axiom in store.axioms() {
1891        let DlAxiom::EquivalentClasses(ops) = axiom else {
1892            continue;
1893        };
1894        let mut targets = Vec::new();
1895        let mut props = HashSet::new();
1896        for &ce in ops {
1897            match store.ce(ce) {
1898                Some(ClassExpr::Atomic(entity)) => targets.push(*entity),
1899                Some(ClassExpr::Some {
1900                    property: RoleExpr::Atomic(prop),
1901                    filler,
1902                }) if ce_is_top_or_thing(store, *filler) => {
1903                    props.insert(*prop);
1904                }
1905                _ => {}
1906            }
1907        }
1908        for target in targets {
1909            exists_props
1910                .entry(target)
1911                .or_default()
1912                .extend(props.iter().copied());
1913        }
1914    }
1915
1916    let mut complement_of: HashMap<EntityId, HashSet<EntityId>> = HashMap::new();
1917    for axiom in store.axioms() {
1918        let DlAxiom::ClassAssertion { individual, class } = axiom else {
1919            continue;
1920        };
1921        let Some(ClassExpr::Not(inner)) = store.ce(*class) else {
1922            continue;
1923        };
1924        let Some(ClassExpr::Atomic(entity)) = store.ce(*inner) else {
1925            continue;
1926        };
1927        complement_of
1928            .entry(*individual)
1929            .or_default()
1930            .insert(*entity);
1931    }
1932
1933    for (individual, forbidden) in &complement_of {
1934        for axiom in store.axioms() {
1935            let DlAxiom::ObjectPropertyAssertion {
1936                subject: _,
1937                property,
1938                object,
1939            } = axiom
1940            else {
1941                continue;
1942            };
1943            if object != individual {
1944                continue;
1945            }
1946            let Some(prop) = role_entity(property) else {
1947                continue;
1948            };
1949            for (&target, props) in &exists_props {
1950                if !forbidden.contains(&target) {
1951                    continue;
1952                }
1953                if props.contains(&prop) {
1954                    return true;
1955                }
1956                if let Some(inv) = inverse_property(ontology, prop)
1957                    && props.contains(&inv)
1958                {
1959                    return true;
1960                }
1961            }
1962        }
1963        for (_, axiom) in ontology.axioms().iter() {
1964            let Axiom::ObjectPropertyAssertion {
1965                subject: _,
1966                property,
1967                object,
1968            } = axiom
1969            else {
1970                continue;
1971            };
1972            if object != individual {
1973                continue;
1974            };
1975            for (&target, props) in &exists_props {
1976                if !forbidden.contains(&target) {
1977                    continue;
1978                }
1979                if props.contains(property) {
1980                    return true;
1981                }
1982                if let Some(inv) = inverse_property(ontology, *property)
1983                    && props.contains(&inv)
1984                {
1985                    return true;
1986                }
1987            }
1988        }
1989    }
1990    false
1991}
1992
1993fn ce_is_top_or_thing(store: &ontologos_core::DlStore, ce: CeId) -> bool {
1994    matches!(store.ce(ce), Some(ClassExpr::Top | ClassExpr::Atomic(_)))
1995}
1996
1997fn role_entity(role: &RoleExpr) -> Option<EntityId> {
1998    match role {
1999        RoleExpr::Atomic(id) => Some(*id),
2000        _ => None,
2001    }
2002}
2003
2004fn inverse_property(ontology: &Ontology, property: EntityId) -> Option<EntityId> {
2005    ontology.axioms().iter().find_map(|(_, axiom)| {
2006        let Axiom::InverseObjectProperties { left, right } = axiom else {
2007            return None;
2008        };
2009        if *left == property {
2010            Some(*right)
2011        } else if *right == property {
2012            Some(*left)
2013        } else {
2014            None
2015        }
2016    })
2017}
2018
2019/// WG dl-035: individual typed with `C ⊑ ≥n R` while another individual is typed `≤k R'`.
2020fn abox_min_card_exceeds_individual_max_card_clash(ontology: &Ontology) -> bool {
2021    use std::collections::HashMap;
2022
2023    let store = ontology.dl();
2024    let mut individual_max: HashMap<EntityId, u32> = HashMap::new();
2025    for axiom in store.axioms() {
2026        let DlAxiom::ClassAssertion { individual, class } = axiom else {
2027            continue;
2028        };
2029        if let Some(ClassExpr::MaxCardinality { n, .. }) = store.ce(*class) {
2030            individual_max.insert(*individual, *n);
2031        }
2032    }
2033
2034    let mut asserted_min: Vec<(EntityId, u32)> = Vec::new();
2035    for axiom in store.axioms() {
2036        let DlAxiom::ClassAssertion { individual, class } = axiom else {
2037            continue;
2038        };
2039        let Some(ClassExpr::Atomic(entity)) = store.ce(*class) else {
2040            continue;
2041        };
2042        let mut min_req = 0u32;
2043        for sub in store.axioms() {
2044            let DlAxiom::SubClassOf { sub, sup } = sub else {
2045                continue;
2046            };
2047            if ce_atomic_entity(store, *sub) != Some(*entity) {
2048                continue;
2049            }
2050            if let Some(ClassExpr::MinCardinality { n, .. }) = store.ce(*sup) {
2051                min_req = min_req.max(*n);
2052            }
2053        }
2054        if min_req > 0 {
2055            asserted_min.push((*individual, min_req));
2056        }
2057    }
2058
2059    if individual_max.is_empty() || asserted_min.is_empty() {
2060        return false;
2061    }
2062    let domain_cap = individual_max.values().copied().min().unwrap_or(u32::MAX);
2063    asserted_min
2064        .iter()
2065        .any(|(_, min_req)| *min_req > domain_cap)
2066}
2067
2068fn tbox_data_cardinality_clash_with_abox(ontology: &Ontology) -> bool {
2069    if !ontology_has_class_assertion(ontology) {
2070        return false;
2071    }
2072    let store = ontology.dl();
2073    let mut min_by = std::collections::HashMap::new();
2074    let mut max_by = std::collections::HashMap::new();
2075    for axiom in store.axioms() {
2076        let DlAxiom::EquivalentClasses(ops) = axiom else {
2077            continue;
2078        };
2079        for &ce in ops {
2080            match store.ce(ce) {
2081                Some(ClassExpr::DataMinCardinality { n, property, .. }) => {
2082                    min_by
2083                        .entry(*property)
2084                        .and_modify(|m: &mut u32| *m = (*m).max(*n))
2085                        .or_insert(*n);
2086                }
2087                Some(ClassExpr::DataMaxCardinality { n, property, .. }) => {
2088                    max_by
2089                        .entry(*property)
2090                        .and_modify(|m: &mut u32| *m = (*m).min(*n))
2091                        .or_insert(*n);
2092                }
2093                Some(ClassExpr::MinCardinality {
2094                    n,
2095                    property: RoleExpr::Atomic(prop),
2096                    ..
2097                }) => {
2098                    min_by
2099                        .entry(*prop)
2100                        .and_modify(|m: &mut u32| *m = (*m).max(*n))
2101                        .or_insert(*n);
2102                }
2103                Some(ClassExpr::MaxCardinality {
2104                    n,
2105                    property: RoleExpr::Atomic(prop),
2106                    ..
2107                }) => {
2108                    max_by
2109                        .entry(*prop)
2110                        .and_modify(|m: &mut u32| *m = (*m).min(*n))
2111                        .or_insert(*n);
2112                }
2113                _ => {}
2114            }
2115        }
2116    }
2117    let has_global_clash = min_by
2118        .iter()
2119        .any(|(prop, min)| max_by.get(prop).is_some_and(|max| min > max));
2120    if !has_global_clash {
2121        return false;
2122    }
2123    for axiom in store.axioms() {
2124        let DlAxiom::ClassAssertion { class, .. } = axiom else {
2125            continue;
2126        };
2127        if let Some(ClassExpr::Atomic(entity)) = store.ce(*class)
2128            && class_assertion_targets_unsatisfiable(ontology, *entity)
2129        {
2130            return true;
2131        }
2132    }
2133    for (_, axiom) in ontology.axioms().iter() {
2134        let Axiom::ClassAssertion { class, .. } = axiom else {
2135            continue;
2136        };
2137        if class_assertion_targets_unsatisfiable(ontology, *class) {
2138            return true;
2139        }
2140    }
2141    false
2142}
2143
2144fn class_assertion_targets_unsatisfiable(ontology: &Ontology, class: EntityId) -> bool {
2145    ontology
2146        .entity(class)
2147        .ok()
2148        .and_then(|record| ontology.resolve_iri(record.iri).ok())
2149        .is_some_and(|iri| {
2150            let local = iri.rsplit(['#', '/']).next().unwrap_or(iri);
2151            local.eq_ignore_ascii_case("Unsatisfiable")
2152        })
2153}
2154
2155fn ontology_has_property_assertion(ontology: &Ontology, property: EntityId) -> bool {
2156    ontology.dl().axioms().any(|axiom| {
2157        matches!(
2158            axiom,
2159            DlAxiom::ObjectPropertyAssertion {
2160                property: RoleExpr::Atomic(p),
2161                ..
2162            } if *p == property
2163        )
2164    }) || ontology.axioms().iter().any(|(_, axiom)| {
2165        matches!(
2166            axiom,
2167            Axiom::ObjectPropertyAssertion { property: p, .. } if *p == property
2168        )
2169    })
2170}
2171
2172fn different_pair(left: EntityId, right: EntityId) -> (EntityId, EntityId) {
2173    if left.0 <= right.0 {
2174        (left, right)
2175    } else {
2176        (right, left)
2177    }
2178}
2179
2180/// Functional object property + conflicting assertions on explicitly different individuals.
2181fn abox_functional_different_individuals_clash(ontology: &Ontology) -> bool {
2182    use std::collections::{HashMap, HashSet};
2183
2184    let mut functional = HashSet::new();
2185    for (_, axiom) in ontology.axioms().iter() {
2186        if let Axiom::FunctionalObjectProperty(prop) = axiom {
2187            functional.insert(*prop);
2188        }
2189    }
2190    if functional.is_empty() {
2191        return false;
2192    }
2193
2194    let mut different = HashSet::new();
2195    for (_, axiom) in ontology.axioms().iter() {
2196        if let Axiom::DifferentIndividuals(ids) = axiom {
2197            for i in 0..ids.len() {
2198                for j in (i + 1)..ids.len() {
2199                    different.insert(different_pair(ids[i], ids[j]));
2200                }
2201            }
2202        }
2203    }
2204    for axiom in ontology.dl().axioms() {
2205        if let DlAxiom::DifferentIndividuals(ids) = axiom {
2206            for i in 0..ids.len() {
2207                for j in (i + 1)..ids.len() {
2208                    different.insert(different_pair(ids[i], ids[j]));
2209                }
2210            }
2211        }
2212    }
2213    if different.is_empty() {
2214        return false;
2215    }
2216
2217    let mut by_subject_prop: HashMap<(EntityId, EntityId), HashSet<EntityId>> = HashMap::new();
2218    for axiom in ontology.dl().axioms() {
2219        let DlAxiom::ObjectPropertyAssertion {
2220            subject,
2221            property,
2222            object,
2223        } = axiom
2224        else {
2225            continue;
2226        };
2227        let RoleExpr::Atomic(prop) = property else {
2228            continue;
2229        };
2230        if functional.contains(prop) {
2231            by_subject_prop
2232                .entry((*subject, *prop))
2233                .or_default()
2234                .insert(*object);
2235        }
2236    }
2237    for (_, axiom) in ontology.axioms().iter() {
2238        let Axiom::ObjectPropertyAssertion {
2239            subject,
2240            property,
2241            object,
2242        } = axiom
2243        else {
2244            continue;
2245        };
2246        if functional.contains(property) {
2247            by_subject_prop
2248                .entry((*subject, *property))
2249                .or_default()
2250                .insert(*object);
2251        }
2252    }
2253
2254    for objects in by_subject_prop.values() {
2255        if objects.len() <= 1 {
2256            continue;
2257        }
2258        for &o1 in objects {
2259            for &o2 in objects {
2260                if o1 != o2 && different.contains(&different_pair(o1, o2)) {
2261                    return true;
2262                }
2263            }
2264        }
2265    }
2266    false
2267}
2268
2269fn ontology_has_class_assertion(ontology: &Ontology) -> bool {
2270    ontology
2271        .dl()
2272        .axioms()
2273        .any(|ax| matches!(ax, DlAxiom::ClassAssertion { .. }))
2274}
2275
2276fn thing_equivalent_nothing(ontology: &Ontology) -> bool {
2277    let thing = ontology
2278        .lookup_entity("owl:Thing")
2279        .or_else(|| ontology.lookup_entity("http://www.w3.org/2002/07/owl#Thing"));
2280    let nothing = ontology
2281        .lookup_entity("owl:Nothing")
2282        .or_else(|| ontology.lookup_entity("http://www.w3.org/2002/07/owl#Nothing"));
2283    let (Some(thing), Some(nothing)) = (thing, nothing) else {
2284        return false;
2285    };
2286    let store = ontology.dl();
2287    for axiom in store.axioms() {
2288        if let DlAxiom::EquivalentClasses(classes) = axiom {
2289            let ents: Vec<EntityId> = classes
2290                .iter()
2291                .filter_map(|ce| atomic_entity_from_ce(store, *ce))
2292                .collect();
2293            if ents.contains(&thing) && ents.contains(&nothing) {
2294                return true;
2295            }
2296        }
2297    }
2298    for (_, axiom) in ontology.axioms().iter() {
2299        if let Axiom::EquivalentClasses(classes) = axiom
2300            && classes.contains(&thing)
2301            && classes.contains(&nothing)
2302        {
2303            return true;
2304        }
2305    }
2306    false
2307}
2308
2309fn thing_equivalent_finite_nominal(ontology: &Ontology) -> bool {
2310    let thing = ontology
2311        .lookup_entity("owl:Thing")
2312        .or_else(|| ontology.lookup_entity("http://www.w3.org/2002/07/owl#Thing"));
2313    let Some(thing) = thing else {
2314        return false;
2315    };
2316    let store = ontology.dl();
2317    for axiom in store.axioms() {
2318        let DlAxiom::EquivalentClasses(classes) = axiom else {
2319            continue;
2320        };
2321        let has_thing = classes
2322            .iter()
2323            .any(|ce| atomic_entity_from_ce(store, *ce) == Some(thing));
2324        if !has_thing {
2325            continue;
2326        }
2327        let has_finite_nominal = classes.iter().any(|ce| {
2328            matches!(
2329                store.ce(*ce),
2330                Some(ontologos_core::ClassExpr::OneOf(nominals)) if !nominals.is_empty()
2331            )
2332        });
2333        if !has_finite_nominal {
2334            continue;
2335        }
2336        let nominals = classes.iter().find_map(|ce| match store.ce(*ce) {
2337            Some(ontologos_core::ClassExpr::OneOf(ns)) if !ns.is_empty() => Some(ns.as_slice()),
2338            _ => None,
2339        });
2340        if let Some(members) = nominals
2341            && members.iter().any(|n| {
2342                individual_is_asserted(ontology, *n) || individual_has_abox_assertions(ontology, *n)
2343            })
2344        {
2345            continue;
2346        }
2347        return true;
2348    }
2349    false
2350}
2351
2352fn canonical_entity_iri(ontology: &Ontology, id: EntityId) -> Option<String> {
2353    let record = ontology.entity(id).ok()?;
2354    ontology
2355        .resolve_iri(record.iri)
2356        .ok()
2357        .map(|iri| iri.replace("%23", "#"))
2358}
2359
2360fn individual_has_abox_assertions(ontology: &Ontology, individual: EntityId) -> bool {
2361    let Some(target) = canonical_entity_iri(ontology, individual) else {
2362        return false;
2363    };
2364    ontology.dl().axioms().any(|axiom| match axiom {
2365        DlAxiom::DataPropertyAssertion { subject, .. }
2366        | DlAxiom::ObjectPropertyAssertion { subject, .. }
2367        | DlAxiom::NegativeDataPropertyAssertion { subject, .. }
2368        | DlAxiom::NegativeObjectPropertyAssertion { subject, .. } => {
2369            canonical_entity_iri(ontology, *subject).as_deref() == Some(target.as_str())
2370        }
2371        _ => false,
2372    })
2373}
2374
2375fn individual_is_asserted(ontology: &Ontology, individual: EntityId) -> bool {
2376    let Some(target) = canonical_entity_iri(ontology, individual) else {
2377        return false;
2378    };
2379    ontology.dl().axioms().any(|axiom| {
2380        matches!(
2381            axiom,
2382            DlAxiom::ClassAssertion { individual: ind, .. }
2383                if canonical_entity_iri(ontology, *ind).as_deref() == Some(target.as_str())
2384        )
2385    }) || ontology.axioms().iter().any(|(_, axiom)| {
2386        matches!(
2387            axiom,
2388            Axiom::ClassAssertion { individual: ind, .. }
2389                if canonical_entity_iri(ontology, *ind).as_deref() == Some(target.as_str())
2390        )
2391    })
2392}
2393
2394fn atomic_entity_from_ce(
2395    store: &ontologos_core::DlStore,
2396    ce: ontologos_core::CeId,
2397) -> Option<EntityId> {
2398    match store.ce(ce)? {
2399        ontologos_core::ClassExpr::Atomic(id) => Some(*id),
2400        _ => None,
2401    }
2402}
2403
2404fn flower_auxiliary_unsatisfiable_classes(ontology: &Ontology, taxonomy: &Taxonomy) -> bool {
2405    let comp_unsat: Vec<EntityId> = taxonomy
2406        .unsatisfiable
2407        .iter()
2408        .copied()
2409        .filter(|entity| {
2410            ontology
2411                .entity(*entity)
2412                .ok()
2413                .and_then(|record| ontology.resolve_iri(record.iri).ok())
2414                .is_some_and(|iri| iri.contains(".comp"))
2415        })
2416        .collect();
2417    if comp_unsat.len() < 2 || !ontology_has_class_assertion(ontology) {
2418        return false;
2419    }
2420    for axiom in ontology.dl().axioms() {
2421        let DlAxiom::ClassAssertion { class, .. } = axiom else {
2422            continue;
2423        };
2424        let mut hit = 0usize;
2425        for comp in &comp_unsat {
2426            if class_assertion_entails_class(ontology, *class, *comp) {
2427                hit += 1;
2428            }
2429        }
2430        if hit >= 2 {
2431            return true;
2432        }
2433    }
2434    false
2435}
2436
2437fn class_assertion_entails_class(ontology: &Ontology, asserted: CeId, target: EntityId) -> bool {
2438    let store = ontology.dl();
2439    let Some(expr) = store.ce(asserted) else {
2440        return false;
2441    };
2442    match expr {
2443        ClassExpr::Atomic(e) => *e == target,
2444        ClassExpr::And(ops) => ops.iter().any(|op| {
2445            store
2446                .ce(*op)
2447                .and_then(|inner| match inner {
2448                    ClassExpr::Atomic(e) if *e == target => Some(true),
2449                    _ => None,
2450                })
2451                .unwrap_or(false)
2452        }),
2453        _ => false,
2454    }
2455}
2456
2457/// Check named class subsumption after DL classification.
2458pub fn is_subsumed(ontology: &Ontology, sub: &str, sup: &str) -> Result<bool> {
2459    let taxonomy = classify(ontology)?;
2460    let sub_id = ontology
2461        .lookup_entity(sub)
2462        .ok_or_else(|| Error::Message(format!("unknown entity: {sub}")))?;
2463    let sup_id = ontology
2464        .lookup_entity(sup)
2465        .ok_or_else(|| Error::Message(format!("unknown entity: {sup}")))?;
2466    Ok(taxonomy.is_subsumed(sub_id, sup_id))
2467}
2468
2469#[cfg(test)]
2470mod exists_forall_tests {
2471    use super::*;
2472    use ontologos_parser::load_ontology;
2473    use std::path::PathBuf;
2474    use std::time::Instant;
2475
2476    fn wg(case: &str) -> PathBuf {
2477        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2478            .join("../../benchmarks/data/hermit/wg")
2479            .join(case)
2480            .join("premise.rdf")
2481    }
2482
2483    #[test]
2484    fn thing_004_is_consistent_005_is_not() {
2485        let ont004 = load_ontology(&wg("TestCase-3AWebOnt-2DThing-2D004")).expect("load 004");
2486        let ont005 = load_ontology(&wg("TestCase-3AWebOnt-2DThing-2D005")).expect("load 005");
2487        assert!(
2488            is_consistent(&ont004).expect("check"),
2489            "Thing-004 should be consistent"
2490        );
2491        assert!(
2492            !is_consistent(&ont005).expect("check"),
2493            "Thing-005 should be inconsistent"
2494        );
2495    }
2496
2497    #[test]
2498    fn dl040_exists_forall_clash() {
2499        let ont =
2500            load_ontology(&wg("TestCase-3AWebOnt-2Ddescription-2Dlogic-2D040")).expect("load");
2501        let dl = DlOntology::from_ontology(&ont).expect("dl");
2502        let start = Instant::now();
2503        let clash =
2504            abox_exists_forall_role_clash(&ont, &dl, &TableauSeed::default()).expect("clash");
2505        eprintln!("dl040 clash={clash:?} elapsed={:?}", start.elapsed());
2506        assert_eq!(clash, Some(true));
2507    }
2508}