rustdl 0.3.2__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {rustdl-0.3.2 → rustdl-0.3.4}/Cargo.lock +9 -9
  2. {rustdl-0.3.2 → rustdl-0.3.4}/Cargo.toml +6 -6
  3. {rustdl-0.3.2 → rustdl-0.3.4}/PKG-INFO +1 -1
  4. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/convert.rs +5 -0
  5. rustdl-0.3.4/crates/owl-dl-core/src/disjunction_existential.rs +219 -0
  6. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/lib.rs +1 -0
  7. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/konclude_closure_diff.rs +40 -0
  8. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-saturation/src/lib.rs +195 -2
  9. {rustdl-0.3.2 → rustdl-0.3.4}/README.md +0 -0
  10. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/Cargo.toml +0 -0
  11. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/README.md +0 -0
  12. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/absorb.rs +0 -0
  13. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/clause.rs +0 -0
  14. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/convert_back.rs +0 -0
  15. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/data_axioms.rs +0 -0
  16. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/definitions.rs +0 -0
  17. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/ir.rs +0 -0
  18. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/locality.rs +0 -0
  19. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/normalize.rs +0 -0
  20. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/ontology.rs +0 -0
  21. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/residual_trigger.rs +0 -0
  22. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/role_hierarchy.rs +0 -0
  23. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/told.rs +0 -0
  24. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/transform.rs +0 -0
  25. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/src/vocab.rs +0 -0
  26. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/tests/concept_pool_proptest.proptest-regressions +0 -0
  27. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/tests/concept_pool_proptest.rs +0 -0
  28. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/tests/convert_round_trip_proptest.rs +0 -0
  29. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-core/tests/role_hierarchy_proptest.rs +0 -0
  30. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-datatypes/Cargo.toml +0 -0
  31. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-datatypes/README.md +0 -0
  32. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-datatypes/src/lib.rs +0 -0
  33. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/.gitignore +0 -0
  34. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/Cargo.toml +0 -0
  35. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/README.md +0 -0
  36. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/src/classify.rs +0 -0
  37. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/src/errors.rs +0 -0
  38. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/src/lib.rs +0 -0
  39. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/src/load.rs +0 -0
  40. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/src/materialize.rs +0 -0
  41. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/src/queries.rs +0 -0
  42. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/conftest.py +0 -0
  43. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/test_classify.py +0 -0
  44. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/test_examples.py +0 -0
  45. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/test_materialize.py +0 -0
  46. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/test_queries.py +0 -0
  47. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/test_smoke.py +0 -0
  48. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-py/tests/python/test_soundness.py +0 -0
  49. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/Cargo.toml +0 -0
  50. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/README.md +0 -0
  51. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/src/abox_check.rs +0 -0
  52. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/src/classify.rs +0 -0
  53. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/src/lib.rs +0 -0
  54. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/src/model_cache.rs +0 -0
  55. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/src/realize.rs +0 -0
  56. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/src/union_find.rs +0 -0
  57. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/abox_consistency.rs +0 -0
  58. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/anon349_diagnostic.rs +0 -0
  59. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/datatype_completeness.rs +0 -0
  60. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p1_direct_bot.ofn +0 -0
  61. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p1_no_bot.ofn +0 -0
  62. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p2_disjoint_different_individuals.ofn +0 -0
  63. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p2_disjoint_types.ofn +0 -0
  64. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p3_neg_opa.ofn +0 -0
  65. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p3_neg_opa_no_clash.ofn +0 -0
  66. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p3_role_hierarchy_neg_consistent.ofn +0 -0
  67. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p3_role_hierarchy_neg_inconsistent.ofn +0 -0
  68. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p4_same_different.ofn +0 -0
  69. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p4_same_without_different.ofn +0 -0
  70. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p5_functional_diff.ofn +0 -0
  71. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p5_functional_same_target.ofn +0 -0
  72. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p6_asymmetric.ofn +0 -0
  73. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p6_asymmetric_one_way.ofn +0 -0
  74. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p6_irreflexive.ofn +0 -0
  75. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p6_irreflexive_distinct.ofn +0 -0
  76. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p7_range_compatible.ofn +0 -0
  77. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/abox/p7_range_disjoint.ofn +0 -0
  78. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/data_cardinality_disjoint.ofn +0 -0
  79. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/data_property_domain-classified.owx +0 -0
  80. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/data_property_domain.ofn +0 -0
  81. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/datatype_definition-classified.owx +0 -0
  82. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/datatype_definition.ofn +0 -0
  83. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/datatype_facet.ofn +0 -0
  84. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/functional_data_property.ofn +0 -0
  85. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/sub_data_property-classified.owx +0 -0
  86. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/datatype/sub_data_property.ofn +0 -0
  87. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/functional-equiv-some-bug.ofn +0 -0
  88. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/named-pizza-country-bug.ofn +0 -0
  89. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-01.txt +0 -0
  90. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-02.txt +0 -0
  91. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-03.txt +0 -0
  92. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-04.txt +0 -0
  93. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-05.txt +0 -0
  94. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-06.txt +0 -0
  95. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-07.txt +0 -0
  96. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/p2b0-terms-08.txt +0 -0
  97. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_01.hermit.owx +0 -0
  98. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_01.ofn +0 -0
  99. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_01.owx +0 -0
  100. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_02.hermit.owx +0 -0
  101. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_02.ofn +0 -0
  102. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_02.owx +0 -0
  103. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_03.hermit.owx +0 -0
  104. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_03.ofn +0 -0
  105. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_03.owx +0 -0
  106. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_04.hermit.owx +0 -0
  107. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_04.ofn +0 -0
  108. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_04.owx +0 -0
  109. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_05.hermit.owx +0 -0
  110. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_05.ofn +0 -0
  111. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_05.owx +0 -0
  112. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_06.hermit.owx +0 -0
  113. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_06.ofn +0 -0
  114. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_06.owx +0 -0
  115. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_07.hermit.owx +0 -0
  116. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_07.ofn +0 -0
  117. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_07.owx +0 -0
  118. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_08.hermit.owx +0 -0
  119. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_08.ofn +0 -0
  120. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/pair_08.owx +0 -0
  121. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/fixtures/phase2b/phase2b-verdicts.log +0 -0
  122. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/label_heuristic_canary.rs +0 -0
  123. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/phase2c_pair_06_canary.rs +0 -0
  124. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/real_ontology_corpus.rs +0 -0
  125. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-reasoner/tests/snapshot_phase0_canary.rs +0 -0
  126. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-saturation/Cargo.toml +0 -0
  127. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-saturation/README.md +0 -0
  128. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-saturation/tests/fixtures/phase2a_functional_role_canary.ofn +0 -0
  129. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-saturation/tests/fixtures/phase2b_compound_existential_canary.ofn +0 -0
  130. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/Cargo.toml +0 -0
  131. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/README.md +0 -0
  132. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/counters.rs +0 -0
  133. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/deps.rs +0 -0
  134. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/graph.rs +0 -0
  135. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/hyper.rs +0 -0
  136. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/lib.rs +0 -0
  137. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/replay.rs +0 -0
  138. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/rules.rs +0 -0
  139. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/saturate.rs +0 -0
  140. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/search.rs +0 -0
  141. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/snapshot.rs +0 -0
  142. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/src/trail.rs +0 -0
  143. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/tests/backprop_risk.rs +0 -0
  144. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/tests/replay_driver.rs +0 -0
  145. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/tests/replay_roundtrip.rs +0 -0
  146. {rustdl-0.3.2 → rustdl-0.3.4}/crates/owl-dl-tableau/tests/snapshot_capture.rs +0 -0
  147. {rustdl-0.3.2 → rustdl-0.3.4}/pyproject.toml +0 -0
  148. {rustdl-0.3.2 → rustdl-0.3.4}/python/rustdl/__init__.py +0 -0
  149. {rustdl-0.3.2 → rustdl-0.3.4}/python/rustdl/data/pizza.owl.gz +0 -0
  150. {rustdl-0.3.2 → rustdl-0.3.4}/python/rustdl/data/sio.owl.gz +0 -0
  151. {rustdl-0.3.2 → rustdl-0.3.4}/python/rustdl/data/sulo.owl.gz +0 -0
  152. {rustdl-0.3.2 → rustdl-0.3.4}/python/rustdl/examples.py +0 -0
@@ -1037,7 +1037,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
1037
1037
 
1038
1038
  [[package]]
1039
1039
  name = "owl-dl-bench"
1040
- version = "0.3.2"
1040
+ version = "0.3.4"
1041
1041
  dependencies = [
1042
1042
  "anyhow",
1043
1043
  "clap",
@@ -1054,7 +1054,7 @@ dependencies = [
1054
1054
 
1055
1055
  [[package]]
1056
1056
  name = "owl-dl-cli"
1057
- version = "0.3.2"
1057
+ version = "0.3.4"
1058
1058
  dependencies = [
1059
1059
  "anyhow",
1060
1060
  "clap",
@@ -1066,7 +1066,7 @@ dependencies = [
1066
1066
 
1067
1067
  [[package]]
1068
1068
  name = "owl-dl-core"
1069
- version = "0.3.2"
1069
+ version = "0.3.4"
1070
1070
  dependencies = [
1071
1071
  "bitvec",
1072
1072
  "bumpalo",
@@ -1080,7 +1080,7 @@ dependencies = [
1080
1080
 
1081
1081
  [[package]]
1082
1082
  name = "owl-dl-datatypes"
1083
- version = "0.3.2"
1083
+ version = "0.3.4"
1084
1084
  dependencies = [
1085
1085
  "owl-dl-core",
1086
1086
  "proptest",
@@ -1090,7 +1090,7 @@ dependencies = [
1090
1090
 
1091
1091
  [[package]]
1092
1092
  name = "owl-dl-py"
1093
- version = "0.3.2"
1093
+ version = "0.3.4"
1094
1094
  dependencies = [
1095
1095
  "horned-owl",
1096
1096
  "owl-dl-core",
@@ -1101,7 +1101,7 @@ dependencies = [
1101
1101
 
1102
1102
  [[package]]
1103
1103
  name = "owl-dl-reasoner"
1104
- version = "0.3.2"
1104
+ version = "0.3.4"
1105
1105
  dependencies = [
1106
1106
  "dashmap",
1107
1107
  "horned-owl",
@@ -1117,7 +1117,7 @@ dependencies = [
1117
1117
 
1118
1118
  [[package]]
1119
1119
  name = "owl-dl-saturation"
1120
- version = "0.3.2"
1120
+ version = "0.3.4"
1121
1121
  dependencies = [
1122
1122
  "dashmap",
1123
1123
  "fixedbitset",
@@ -1133,7 +1133,7 @@ dependencies = [
1133
1133
 
1134
1134
  [[package]]
1135
1135
  name = "owl-dl-tableau"
1136
- version = "0.3.2"
1136
+ version = "0.3.4"
1137
1137
  dependencies = [
1138
1138
  "bitvec",
1139
1139
  "bumpalo",
@@ -2569,7 +2569,7 @@ dependencies = [
2569
2569
 
2570
2570
  [[package]]
2571
2571
  name = "xtask"
2572
- version = "0.3.2"
2572
+ version = "0.3.4"
2573
2573
  dependencies = [
2574
2574
  "anyhow",
2575
2575
  "clap",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/owl-dl-core", "crates/owl-dl-saturation", "crates/owl-dl-tableau", "crates/owl-dl-datatypes", "crates/owl-dl-reasoner", "crates/owl-dl-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.2"
6
+ version = "0.3.4"
7
7
  edition = "2024"
8
8
  rust-version = "1.88"
9
9
  license = "Apache-2.0 OR MIT"
@@ -15,11 +15,11 @@ keywords = ["owl", "ontology", "reasoner", "description-logic", "semantic-web"]
15
15
  categories = ["science", "data-structures"]
16
16
 
17
17
  [workspace.dependencies]
18
- owl-dl-core = { path = "crates/owl-dl-core", version = "0.3.2" }
19
- owl-dl-saturation = { path = "crates/owl-dl-saturation", version = "0.3.2" }
20
- owl-dl-tableau = { path = "crates/owl-dl-tableau", version = "0.3.2" }
21
- owl-dl-datatypes = { path = "crates/owl-dl-datatypes", version = "0.3.2" }
22
- owl-dl-reasoner = { path = "crates/owl-dl-reasoner", version = "0.3.2" }
18
+ owl-dl-core = { path = "crates/owl-dl-core", version = "0.3.4" }
19
+ owl-dl-saturation = { path = "crates/owl-dl-saturation", version = "0.3.4" }
20
+ owl-dl-tableau = { path = "crates/owl-dl-tableau", version = "0.3.4" }
21
+ owl-dl-datatypes = { path = "crates/owl-dl-datatypes", version = "0.3.4" }
22
+ owl-dl-reasoner = { path = "crates/owl-dl-reasoner", version = "0.3.4" }
23
23
 
24
24
  horned-owl = { version = "1.4", default-features = false }
25
25
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rustdl
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: License :: OSI Approved :: Apache Software License
@@ -527,6 +527,11 @@ pub fn convert_ontology<A: ForIRI>(
527
527
  })
528
528
  };
529
529
  out.axioms.extend(derived);
530
+ // Derive `X ⊑ ∃R.C` from `X ⊑ ∃R.(D₁ ⊔ … ⊔ Dₙ)` when the disjuncts
531
+ // share a told-subsumer C (sound under-approximation; feeds the EL
532
+ // saturator a case-split it otherwise drops). Runs on the fully
533
+ // populated IR.
534
+ crate::disjunction_existential::derive_disjunction_existentials(&mut out);
530
535
  out.axioms.sort();
531
536
  Ok(out)
532
537
  }
@@ -0,0 +1,219 @@
1
+ //! Preprocessing pass: derive `X ⊑ ∃R.C` from
2
+ //! `X ⊑ ∃R.(D₁ ⊔ … ⊔ Dₙ)` when all disjuncts share a told-subsumer `C`.
3
+ //!
4
+ //! ## Why
5
+ //!
6
+ //! The consequence-based EL saturator drops existentials whose filler is
7
+ //! a disjunction (`∃R.(D₁ ⊔ … ⊔ Dₙ)` is out of EL). But when every
8
+ //! disjunct shares a common subsumer `C` — i.e. `Dᵢ ⊑ C` for all `i` —
9
+ //! the disjunction is eliminable by cases: `(D₁ ⊔ … ⊔ Dₙ) ⊑ C`, hence
10
+ //! `∃R.(D₁ ⊔ … ⊔ Dₙ) ⊑ ∃R.C`. Feeding the saturator the derived
11
+ //! `X ⊑ ∃R.C` lets it close subsumptions that otherwise need a full
12
+ //! tableau case-split.
13
+ //!
14
+ //! This is a **sound under-approximation**: every emitted axiom is
15
+ //! entailed, and we only use *told* (explicit, transitively-closed)
16
+ //! subsumers of *atomic* disjuncts, so no false positive is possible.
17
+ //! Cases where the common subsumer is only *derived* (not told), or a
18
+ //! disjunct is non-atomic, are left to the tableau/wedge.
19
+ //!
20
+ //! ## Impact
21
+ //!
22
+ //! Closes the SIO corpus MISSES `SIO_010092 ⊑ SIO_001353` and
23
+ //! `SIO_010092 ⊑ SIO_010410`: `SIO_010092` (DNA template) is
24
+ //! `⊑ ∃has-function.(template-for-RNA ⊔ template-for-DNA)`, both
25
+ //! disjuncts `⊑` `SIO_010088` (template-for-molecular-synthesis)
26
+ //! `⊑ realizable-entity`, and `has-function ⊑* has-realizable-property`.
27
+
28
+ use crate::ir::{ClassId, ConceptExpr, ConceptId, ConceptPool, Role};
29
+ use crate::ontology::{Axiom, InternalOntology};
30
+ use crate::told::{ToldTables, build_told_tables};
31
+
32
+ /// Scan `onto` for `SubClassOf(X, ∃R.(union-of-atomics))` (directly, or
33
+ /// as a conjunct of a top-level `And`) and append a derived
34
+ /// `SubClassOf(X, ∃R.C)` for each *minimal* common told-subsumer `C` of
35
+ /// the disjuncts. See the module docs for soundness.
36
+ pub fn derive_disjunction_existentials(onto: &mut InternalOntology) {
37
+ let told = build_told_tables(onto);
38
+ // Phase 1 (immutable borrow): collect (sub, role, common-class).
39
+ let mut triples: Vec<(ConceptId, Role, ClassId)> = Vec::new();
40
+ for ax in &onto.axioms {
41
+ let Axiom::SubClassOf { sub, sup } = ax else {
42
+ continue;
43
+ };
44
+ collect_from_sup(*sub, *sup, &onto.concepts, &told, &mut triples);
45
+ }
46
+ if triples.is_empty() {
47
+ return;
48
+ }
49
+ // Phase 2 (mutable borrow): intern the derived existentials + push.
50
+ for (sub, role, c) in triples {
51
+ let body = onto.concepts.atomic(c);
52
+ let sup = onto.concepts.some(role, body);
53
+ if sub == sup {
54
+ continue;
55
+ }
56
+ onto.axioms.push(Axiom::SubClassOf { sub, sup });
57
+ }
58
+ }
59
+
60
+ /// Handle a single `SubClassOf` super-concept: a direct `∃R.(union)` or
61
+ /// each `∃R.(union)` conjunct of a top-level `And`.
62
+ fn collect_from_sup(
63
+ sub: ConceptId,
64
+ sup: ConceptId,
65
+ pool: &ConceptPool,
66
+ told: &ToldTables,
67
+ out: &mut Vec<(ConceptId, Role, ClassId)>,
68
+ ) {
69
+ match pool.get(sup) {
70
+ ConceptExpr::Some(role, body) => {
71
+ for c in minimal_common_subsumers(*body, pool, told) {
72
+ out.push((sub, *role, c));
73
+ }
74
+ }
75
+ ConceptExpr::And(operands) => {
76
+ for &op in operands {
77
+ if let ConceptExpr::Some(role, body) = pool.get(op) {
78
+ for c in minimal_common_subsumers(*body, pool, told) {
79
+ out.push((sub, *role, c));
80
+ }
81
+ }
82
+ }
83
+ }
84
+ _ => {}
85
+ }
86
+ }
87
+
88
+ /// If `body` is `Or(D₁, …, Dₙ)` with all `Dᵢ` atomic and `n ≥ 2`,
89
+ /// return the *minimal* (most specific) classes `C` such that every
90
+ /// `Dᵢ ⊑ C` is told. Empty otherwise.
91
+ fn minimal_common_subsumers(
92
+ body: ConceptId,
93
+ pool: &ConceptPool,
94
+ told: &ToldTables,
95
+ ) -> Vec<ClassId> {
96
+ let ConceptExpr::Or(disjuncts) = pool.get(body) else {
97
+ return Vec::new();
98
+ };
99
+ let mut atoms: Vec<ClassId> = Vec::with_capacity(disjuncts.len());
100
+ for &d in disjuncts {
101
+ match pool.get(d) {
102
+ ConceptExpr::Atomic(c) => atoms.push(*c),
103
+ // A non-atomic disjunct (nested ∃, And, …) is left to the
104
+ // tableau — keep this pass a sound under-approximation.
105
+ _ => return Vec::new(),
106
+ }
107
+ }
108
+ if atoms.len() < 2 {
109
+ return Vec::new();
110
+ }
111
+ // Intersection of the (reflexive, transitively-closed, sorted)
112
+ // told-super-class sets. Reflexivity is sound here: a disjunct `Dᵢ`
113
+ // lands in the intersection only if it told-subsumes every other
114
+ // disjunct, in which case `Dᵢ ⊒ (D₁ ⊔ … ⊔ Dₙ)` and `∃R.Dᵢ` holds.
115
+ let mut common: Vec<ClassId> = told.super_classes(atoms[0]).to_vec();
116
+ for &a in &atoms[1..] {
117
+ let supers = told.super_classes(a);
118
+ common.retain(|c| supers.binary_search(c).is_ok());
119
+ if common.is_empty() {
120
+ return Vec::new();
121
+ }
122
+ }
123
+ // Keep only minimal elements: drop `C` if some other common `C'` is
124
+ // told-below `C` (the saturator recovers the weaker supers from the
125
+ // minimal ones, so emitting the whole chain is redundant).
126
+ common
127
+ .iter()
128
+ .copied()
129
+ .filter(|&c| {
130
+ !common
131
+ .iter()
132
+ .any(|&other| other != c && told.is_told_sub(other, c))
133
+ })
134
+ .collect()
135
+ }
136
+
137
+ #[cfg(test)]
138
+ mod tests {
139
+ use crate::ir::{ConceptExpr, Role};
140
+ use horned_owl::io::ParserConfiguration;
141
+ use horned_owl::io::ofn::reader::read;
142
+ use horned_owl::model::RcStr;
143
+ use horned_owl::ontology::set::SetOntology;
144
+ use std::io::Cursor;
145
+
146
+ /// The SIO pattern: `X ⊑ ∃R.(D1 ⊔ D2)` with `D1,D2 ⊑ E ⊑ F`. The
147
+ /// pass (run inside `convert_ontology`) must add `X ⊑ ∃R.E` (E is
148
+ /// the minimal common told-subsumer), and must NOT add the weaker
149
+ /// `X ⊑ ∃R.F` (only minimal subsumers). Mirrors `SIO_010092`'s
150
+ /// `∃has-function.(template-RNA ⊔ template-DNA)`.
151
+ #[test]
152
+ fn pass_emits_minimal_common_subsumer_existential() {
153
+ let src = "\
154
+ Prefix(:=<http://t.org/#>)
155
+ Ontology(
156
+ Declaration(Class(:X)) Declaration(Class(:D1)) Declaration(Class(:D2))
157
+ Declaration(Class(:E)) Declaration(Class(:F))
158
+ Declaration(ObjectProperty(:R))
159
+ SubClassOf(:X ObjectSomeValuesFrom(:R ObjectUnionOf(:D1 :D2)))
160
+ SubClassOf(:D1 :E) SubClassOf(:D2 :E) SubClassOf(:E :F)
161
+ )
162
+ ";
163
+ let (set_onto, _): (SetOntology<RcStr>, _) =
164
+ read(&mut Cursor::new(src), ParserConfiguration::default()).expect("parses");
165
+ // convert_ontology runs the pass.
166
+ let onto = crate::convert::convert_ontology(&set_onto).expect("converts");
167
+ let cid = |iri: &str| onto.vocabulary.class_id(iri).expect("declared");
168
+ let x = cid("http://t.org/#X");
169
+ let e = cid("http://t.org/#E");
170
+ let f = cid("http://t.org/#F");
171
+
172
+ let some_class = |target| {
173
+ onto.axioms.iter().any(|ax| {
174
+ if let crate::ontology::Axiom::SubClassOf { sub, sup } = ax {
175
+ matches!(onto.concepts.get(*sub), ConceptExpr::Atomic(c) if *c == x)
176
+ && matches!(onto.concepts.get(*sup),
177
+ ConceptExpr::Some(Role::Named(_), body)
178
+ if matches!(onto.concepts.get(*body), ConceptExpr::Atomic(c) if *c == target))
179
+ } else {
180
+ false
181
+ }
182
+ })
183
+ };
184
+ assert!(
185
+ some_class(e),
186
+ "expected derived X ⊑ ∃R.E (minimal common subsumer)"
187
+ );
188
+ assert!(
189
+ !some_class(f),
190
+ "should NOT emit the non-minimal X ⊑ ∃R.F (E ⊑ F already covers it)"
191
+ );
192
+ }
193
+
194
+ /// No common subsumer ⇒ no derived axiom (and no panic).
195
+ #[test]
196
+ fn pass_no_common_subsumer_emits_nothing() {
197
+ let src = "\
198
+ Prefix(:=<http://t.org/#>)
199
+ Ontology(
200
+ Declaration(Class(:X)) Declaration(Class(:D1)) Declaration(Class(:D2))
201
+ Declaration(ObjectProperty(:R))
202
+ SubClassOf(:X ObjectSomeValuesFrom(:R ObjectUnionOf(:D1 :D2)))
203
+ )
204
+ ";
205
+ let (set_onto, _): (SetOntology<RcStr>, _) =
206
+ read(&mut Cursor::new(src), ParserConfiguration::default()).expect("parses");
207
+ let onto = crate::convert::convert_ontology(&set_onto).expect("converts");
208
+ let x = onto.vocabulary.class_id("http://t.org/#X").expect("X");
209
+ // The only ∃R.* axiom on X is the original union; no atomic-body
210
+ // existential was derived (D1, D2 share no told subsumer).
211
+ let derived = onto.axioms.iter().any(|ax| {
212
+ matches!(ax, crate::ontology::Axiom::SubClassOf { sub, sup }
213
+ if matches!(onto.concepts.get(*sub), ConceptExpr::Atomic(c) if *c == x)
214
+ && matches!(onto.concepts.get(*sup),
215
+ ConceptExpr::Some(_, body) if matches!(onto.concepts.get(*body), ConceptExpr::Atomic(_))))
216
+ });
217
+ assert!(!derived, "no common subsumer ⇒ nothing derived");
218
+ }
219
+ }
@@ -13,6 +13,7 @@ pub mod convert;
13
13
  pub mod convert_back;
14
14
  pub mod data_axioms;
15
15
  pub mod definitions;
16
+ pub mod disjunction_existential;
16
17
  pub mod ir;
17
18
  pub mod locality;
18
19
  pub mod normalize;
@@ -378,6 +378,46 @@ fn sio_closure_matches_konclude() {
378
378
  assert_eq!(fp, 0, "sio has FPs — D1 sound-under-approximation broken");
379
379
  }
380
380
 
381
+ #[test]
382
+ #[ignore = "needs ontologies/real/wine.ofn + konclude-input/wine-classified.owx \
383
+ (W3C wine+food merged, circular imports stripped; HermiT oracle). \
384
+ SHOIN(D): nominal- + disjointness-heavy expressivity stressor. \
385
+ Fetch via scripts/fetch-real-ontologies.sh."]
386
+ fn wine_closure_matches_konclude() {
387
+ let input = Path::new("../../ontologies/real/wine.ofn");
388
+ let truth = Path::new("../../ontologies/real/konclude-input/wine-classified.owx");
389
+ if !input.exists() || !truth.exists() {
390
+ eprintln!("SKIP: missing wine fixture");
391
+ return;
392
+ }
393
+ let (_r, _k, fp, _m) = diff_corpus_ontology("wine", input, truth, 200);
394
+ assert_eq!(
395
+ fp, 0,
396
+ "wine has FPs — soundness regression on nominals/datatypes"
397
+ );
398
+ }
399
+
400
+ #[test]
401
+ #[ignore = "needs ontologies/real/bibtex.ofn + konclude-input/bibtex-classified.owx \
402
+ (ORE-2015 ore_ont_3341, a BibTeX ontology; HermiT oracle). \
403
+ Datatype-heavy + real class hierarchy: 41 DataMinCardinality + 40 \
404
+ DataPropertyDomain + 39 DataPropertyRange, 15 classes, 56 inferred \
405
+ edges — exercises Phase-D classification on real data. Fetch via \
406
+ scripts/fetch-real-ontologies.sh."]
407
+ fn bibtex_closure_matches_konclude() {
408
+ let input = Path::new("../../ontologies/real/bibtex.ofn");
409
+ let truth = Path::new("../../ontologies/real/konclude-input/bibtex-classified.owx");
410
+ if !input.exists() || !truth.exists() {
411
+ eprintln!("SKIP: missing bibtex fixture");
412
+ return;
413
+ }
414
+ let (_r, _k, fp, _m) = diff_corpus_ontology("bibtex", input, truth, 200);
415
+ assert_eq!(
416
+ fp, 0,
417
+ "bibtex has FPs — Phase-D sound-under-approximation broken"
418
+ );
419
+ }
420
+
381
421
  #[test]
382
422
  #[ignore = "needs ontologies/real/ro.ofn + konclude-input/ro-classified.owx; Phase D1 fixture (was UnsupportedAxiom-erroring pre-D1; HermiT oracle generated 2026-06-03)"]
383
423
  fn ro_closure_matches_konclude() {
@@ -53,8 +53,8 @@ use std::collections::{HashMap, HashSet, VecDeque};
53
53
 
54
54
  use fixedbitset::FixedBitSet;
55
55
  use owl_dl_core::{
56
- Axiom, ClassId, ConceptExpr, ConceptId, ConceptPool, InternalOntology, Role, RoleId,
57
- SubRolePath,
56
+ Axiom, ClassId, ConceptExpr, ConceptId, ConceptPool, IndividualId, InternalOntology, Role,
57
+ RoleId, SubRolePath,
58
58
  };
59
59
 
60
60
  /// Compute the subsumer closure over the EL-fragment subset of
@@ -676,6 +676,22 @@ impl WorklistEngine {
676
676
  #[allow(clippy::too_many_lines)]
677
677
  fn process_fact(&mut self, idx: usize) {
678
678
  let fact = self.facts[idx];
679
+ // Nominal/ABox transitive propagation: if `fact` is
680
+ // `X ⊑ ∃R.{a}` (target is a NomKey) and `R` is transitive with
681
+ // `a R⁺ b` in the ABox, derive `X ⊑ ∃R.{b}`. Sound: `X R a`,
682
+ // `a R⁺ b`, `R` transitive ⟹ `X R b`. See `build_abox_nominal_reach`.
683
+ if !self.rules.abox_nominal_reach.is_empty()
684
+ && let Some(reach) = self.rules.abox_nominal_reach.get(&(fact.role, fact.target))
685
+ {
686
+ let derived: Vec<ClassId> = reach.clone();
687
+ for b_key in derived {
688
+ self.push_fact(ExistentialFact {
689
+ sub: fact.sub,
690
+ role: fact.role,
691
+ target: b_key,
692
+ });
693
+ }
694
+ }
679
695
  let role_supers = supers_of(&self.role_super, fact.role);
680
696
  // NOTE: range propagation deliberately omitted.
681
697
  //
@@ -1034,6 +1050,16 @@ struct ElRules {
1034
1050
  /// Pairwise disjoint atomic-class pairs, decomposed from n-ary
1035
1051
  /// `DisjointClasses` axioms. Read as `A ⊓ B ⊑ ⊥`.
1036
1052
  disjoint_pairs: Vec<(ClassId, ClassId)>,
1053
+ /// Nominal-reasoning support (wine region cluster). For a
1054
+ /// **transitive** role `R` and a nominal-key class `NomKey(a)`
1055
+ /// (synthetic stand-in for the singleton `{a}`),
1056
+ /// `abox_nominal_reach[(R, NomKey(a))]` lists `NomKey(b)` for every
1057
+ /// individual `b` reachable from `a` via the transitive closure of
1058
+ /// `R` over the named-individual `ABox`. Lets a fact
1059
+ /// `X ⊑ ∃R.{a}` derive `X ⊑ ∃R.{b}` (sound: `X R a`, `a R⁺ b`,
1060
+ /// `R` transitive ⟹ `X R b`). Empty unless the ontology has both
1061
+ /// nominal existential bodies and transitive-role `ABox` edges.
1062
+ abox_nominal_reach: std::collections::HashMap<(RoleId, ClassId), Vec<ClassId>>,
1037
1063
  /// Atomic classes told directly to be unsatisfiable via
1038
1064
  /// `SubClassOf(Atomic(C), Bot)`. Seeded into the unsat worklist
1039
1065
  /// at `seed` time so the standard `process_unsat` propagation
@@ -1146,6 +1172,12 @@ struct TseitinAllocator {
1146
1172
  /// by `(role, body_class_id)` so repeated occurrences of the same
1147
1173
  /// `∃R.B` shape across different conjunctions share one marker.
1148
1174
  by_existential: HashMap<(RoleId, ClassId), ClassId>,
1175
+ /// Stable synthetic atomic class per individual used as a nominal
1176
+ /// (`{a}`) in an existential body. Treated as an opaque atom
1177
+ /// (no subsumers, no triggers) — a 1:1 structural stand-in so the
1178
+ /// EL fold of `C ≡ D ⊓ ∃R.{a}` fires on the same key the fact
1179
+ /// `X ⊑ ∃R.{a}` produced. Injective, so no two individuals merge.
1180
+ nominal_by_ind: HashMap<IndividualId, ClassId>,
1149
1181
  }
1150
1182
 
1151
1183
  impl TseitinAllocator {
@@ -1154,9 +1186,23 @@ impl TseitinAllocator {
1154
1186
  next_id: u32::try_from(num_original_classes).expect("class count fits in u32"),
1155
1187
  by_body: HashMap::new(),
1156
1188
  by_existential: HashMap::new(),
1189
+ nominal_by_ind: HashMap::new(),
1157
1190
  }
1158
1191
  }
1159
1192
 
1193
+ /// Get-or-allocate the opaque synthetic atomic class standing in for
1194
+ /// the nominal `{ind}`. Sound: matching is by individual identity
1195
+ /// (structural), so `∃R.{a}` folds only against the same `a`.
1196
+ fn introduce_nominal(&mut self, ind: IndividualId) -> ClassId {
1197
+ if let Some(&existing) = self.nominal_by_ind.get(&ind) {
1198
+ return existing;
1199
+ }
1200
+ let synthetic = ClassId::new(self.next_id);
1201
+ self.next_id = self.next_id.checked_add(1).expect("synthetic id overflow");
1202
+ self.nominal_by_ind.insert(ind, synthetic);
1203
+ synthetic
1204
+ }
1205
+
1160
1206
  fn introduce(&mut self, mut body: Vec<ClassId>, rules: &mut ElRules) -> ClassId {
1161
1207
  body.sort();
1162
1208
  body.dedup();
@@ -1362,6 +1408,12 @@ fn collect_el_rules(
1362
1408
  _ => {}
1363
1409
  }
1364
1410
  }
1411
+
1412
+ // Nominal/ABox transitive propagation (wine region cluster).
1413
+ // Allocates NomKeys for ABox individuals, so it must run before
1414
+ // `total_classes` is captured.
1415
+ build_abox_nominal_reach(internal, &mut tseitin, &mut rules);
1416
+
1365
1417
  let total_classes = tseitin.next_id as usize;
1366
1418
 
1367
1419
  // Phase 2a: collect functional-role declarations and precompute
@@ -1638,6 +1690,15 @@ fn atomic_existential_rhs(
1638
1690
  if role.is_inverse() {
1639
1691
  return None;
1640
1692
  }
1693
+ // Nominal body `∃R.{a}`: emit the bare per-individual NomKey, NOT a
1694
+ // range-wrapped synthetic. The wrap (`NomKey ⊓ Range(R)`) would make
1695
+ // the fact target a fresh synthetic, defeating the `abox_nominal_reach`
1696
+ // lookup in `process_fact` (keyed on the bare NomKey). Dropping the
1697
+ // range-typing of the witness is a sound under-approximation — the
1698
+ // nominal fold needs only the NomKey identity.
1699
+ if let ConceptExpr::Nominal(ind) = pool.get(*body) {
1700
+ return Some((role.role_id(), tseitin.introduce_nominal(*ind)));
1701
+ }
1641
1702
  let extras = effective_ranges
1642
1703
  .get(&role.role_id())
1643
1704
  .map_or(&[][..], Vec::as_slice);
@@ -1658,6 +1719,90 @@ fn atomic_or_tseitin_body(
1658
1719
  atomic_or_tseitin_body_with_extras(body, &[], pool, rules, tseitin)
1659
1720
  }
1660
1721
 
1722
+ /// Populate [`ElRules::abox_nominal_reach`]: for each **transitive**
1723
+ /// named role `R`, compute the transitive closure of `R` over named
1724
+ /// individuals (`ObjectPropertyAssertion`s) and map each source's
1725
+ /// `NomKey` to the `NomKey`s of all reachable individuals. Enables the
1726
+ /// sound `X ⊑ ∃R.{a}`, `a R⁺ b` ⟹ `X ⊑ ∃R.{b}` propagation in
1727
+ /// `process_fact`. No-op unless the ontology has transitive roles with
1728
+ /// `ABox` edges.
1729
+ fn build_abox_nominal_reach(
1730
+ internal: &InternalOntology,
1731
+ tseitin: &mut TseitinAllocator,
1732
+ rules: &mut ElRules,
1733
+ ) {
1734
+ use std::collections::BTreeSet;
1735
+ let mut transitive: HashSet<RoleId> = HashSet::new();
1736
+ for ax in &internal.axioms {
1737
+ if let Axiom::TransitiveRole(role) = ax
1738
+ && !role.is_inverse()
1739
+ {
1740
+ transitive.insert(role.role_id());
1741
+ }
1742
+ }
1743
+ if transitive.is_empty() {
1744
+ return;
1745
+ }
1746
+ // Direct R-successor graph over individuals (named, transitive R only).
1747
+ let mut direct: HashMap<RoleId, HashMap<IndividualId, BTreeSet<IndividualId>>> = HashMap::new();
1748
+ for ax in &internal.axioms {
1749
+ if let Axiom::ObjectPropertyAssertion {
1750
+ role,
1751
+ subject,
1752
+ object,
1753
+ } = ax
1754
+ && !role.is_inverse()
1755
+ && transitive.contains(&role.role_id())
1756
+ {
1757
+ direct
1758
+ .entry(role.role_id())
1759
+ .or_default()
1760
+ .entry(*subject)
1761
+ .or_default()
1762
+ .insert(*object);
1763
+ }
1764
+ }
1765
+ for (role, graph) in &direct {
1766
+ // Naive transitive-closure fixpoint (ABoxes here are tiny).
1767
+ let mut closure = graph.clone();
1768
+ let mut changed = true;
1769
+ while changed {
1770
+ changed = false;
1771
+ let sources: Vec<IndividualId> = closure.keys().copied().collect();
1772
+ for a in sources {
1773
+ let mids: Vec<IndividualId> = closure
1774
+ .get(&a)
1775
+ .map(|s| s.iter().copied().collect())
1776
+ .unwrap_or_default();
1777
+ let mut additions: Vec<IndividualId> = Vec::new();
1778
+ for m in mids {
1779
+ if let Some(ms) = graph.get(&m) {
1780
+ additions.extend(ms.iter().copied());
1781
+ }
1782
+ }
1783
+ if let Some(reach) = closure.get_mut(&a) {
1784
+ for t in additions {
1785
+ if t != a && reach.insert(t) {
1786
+ changed = true;
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ }
1792
+ for (a, reach) in &closure {
1793
+ if reach.is_empty() {
1794
+ continue;
1795
+ }
1796
+ let a_key = tseitin.introduce_nominal(*a);
1797
+ let targets: Vec<ClassId> = reach
1798
+ .iter()
1799
+ .map(|&b| tseitin.introduce_nominal(b))
1800
+ .collect();
1801
+ rules.abox_nominal_reach.insert((*role, a_key), targets);
1802
+ }
1803
+ }
1804
+ }
1805
+
1661
1806
  /// Return the list of alternative body class ids for an existential
1662
1807
  /// trigger's body. For `Atomic` / `And` returns one element. For
1663
1808
  /// `Or(C1, ..., Cn)` returns one element per operand (each itself
@@ -1701,6 +1846,13 @@ fn atomic_or_tseitin_body_with_extras(
1701
1846
  ) -> Option<ClassId> {
1702
1847
  let body_atomics: Vec<ClassId> = match pool.get(body) {
1703
1848
  ConceptExpr::Atomic(id) => vec![*id],
1849
+ // Nominal `{a}` body (`∃R.{a}`, i.e. ObjectHasValue): use an
1850
+ // opaque per-individual synthetic class as a structural
1851
+ // stand-in so the EL fold of `C ≡ D ⊓ ∃R.{a}` matches the
1852
+ // `X ⊑ ∃R.{a}` fact. Sound (1:1 individual identity); the
1853
+ // singleton/cardinality semantics of `{a}` are deliberately
1854
+ // not modeled (under-approximation — the tableau handles those).
1855
+ ConceptExpr::Nominal(ind) => vec![tseitin.introduce_nominal(*ind)],
1704
1856
  ConceptExpr::And(operands) => {
1705
1857
  atomic_classes_with_existential_markers(operands, pool, rules, tseitin)?
1706
1858
  }
@@ -2599,6 +2751,47 @@ Ontology(<http://rustdl.test/test>\n\
2599
2751
  assert!(subs.contains(class(&internal, "X"), class(&internal, "W")));
2600
2752
  }
2601
2753
 
2754
+ #[test]
2755
+ fn nominal_transitive_abox_fold_classifies() {
2756
+ // Wine region pattern: AlsatianWine ≡ Wine ⊓ ∃locatedIn.{Alsace};
2757
+ // FrenchWine ≡ Wine ⊓ ∃locatedIn.{French}; locatedIn transitive;
2758
+ // ABox Alsace locatedIn French. By transitivity AlsatianWine's
2759
+ // locatedIn-witness reaches French ⟹ AlsatianWine ⊑ FrenchWine.
2760
+ // Exercises the nominal NomKey fold (B) + transitive-ABox
2761
+ // propagation (A). EL alone drops nominal-filler existentials.
2762
+ let internal = parse_internal(&format!(
2763
+ "{HEADER}\
2764
+ Ontology(<http://rustdl.test/test>\n\
2765
+ Declaration(Class(:Wine))\n\
2766
+ Declaration(Class(:AlsatianWine))\n\
2767
+ Declaration(Class(:FrenchWine))\n\
2768
+ Declaration(NamedIndividual(:Alsace))\n\
2769
+ Declaration(NamedIndividual(:French))\n\
2770
+ Declaration(ObjectProperty(:locatedIn))\n\
2771
+ TransitiveObjectProperty(:locatedIn)\n\
2772
+ ObjectPropertyAssertion(:locatedIn :Alsace :French)\n\
2773
+ EquivalentClasses(:AlsatianWine ObjectIntersectionOf(:Wine ObjectHasValue(:locatedIn :Alsace)))\n\
2774
+ EquivalentClasses(:FrenchWine ObjectIntersectionOf(:Wine ObjectHasValue(:locatedIn :French)))\n\
2775
+ )\n"
2776
+ ));
2777
+ let subs = saturate(&internal);
2778
+ assert!(
2779
+ subs.contains(
2780
+ class(&internal, "AlsatianWine"),
2781
+ class(&internal, "FrenchWine")
2782
+ ),
2783
+ "nominal transitive-ABox fold failed: AlsatianWine ⊑ FrenchWine"
2784
+ );
2785
+ // Soundness: the reverse must NOT hold (French does not locate in Alsace).
2786
+ assert!(
2787
+ !subs.contains(
2788
+ class(&internal, "FrenchWine"),
2789
+ class(&internal, "AlsatianWine")
2790
+ ),
2791
+ "unsound: FrenchWine ⊑ AlsatianWine should not hold"
2792
+ );
2793
+ }
2794
+
2602
2795
  #[test]
2603
2796
  fn out_of_fragment_axioms_dont_panic() {
2604
2797
  let internal = parse_internal(&format!(
File without changes