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