1#![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
47pub type Result<T> = std::result::Result<T, Error>;
49
50#[derive(Debug, Error)]
52pub enum Error {
53 #[error("expected DL profile, got {0:?}")]
55 WrongProfile(Profile),
56 #[error(transparent)]
58 El(#[from] ontologos_el::Error),
59 #[error(transparent)]
61 Alc(#[from] ontologos_alc::Error),
62 #[error(transparent)]
64 Parser(#[from] ontologos_parser::Error),
65 #[error(transparent)]
67 Core(#[from] ontologos_core::Error),
68 #[error("ontology inconsistent")]
70 Inconsistent,
71 #[error("DL preview: {0}")]
73 PreviewLimit(String),
74 #[error(transparent)]
76 Profile(#[from] ontologos_profile::Error),
77 #[error("{0}")]
79 Message(String),
80 #[error("consistency check incomplete: {0}")]
82 IncompleteReasoning(String),
83}
84
85pub fn classify(ontology: &Ontology) -> Result<Taxonomy> {
87 DlClassifier::new().classify(ontology)
88}
89
90pub 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#[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
124pub 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
312fn 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
333fn 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
364pub 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
373pub 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
389pub fn named_classes_unsatisfiable(ontology: &Ontology, classes: &[EntityId]) -> Result<bool> {
391 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
517pub 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
532pub 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
552fn 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
574fn 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
586fn 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 return Ok(false);
596 }
597 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
898fn 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 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
1033fn 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
1258fn 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(¤t) {
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
1431fn 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
1444fn 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
1563fn 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
1690fn 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
1737fn 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
1786fn 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
1821fn 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
1883fn 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
2018fn 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
2179fn 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(¬hing) {
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(¬hing)
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
2456pub 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}