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