unicode-logic-kit 0.31.0__py3-none-any.whl

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 (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2704 @@
1
+ """The **standard translation** of ALC into first-order logic.
2
+
3
+ ALC concepts denote unary relations over a domain, and roles denote binary
4
+ relations; the standard translation makes this concrete by rendering a
5
+ concept ``C`` as a first-order formula ``π(C, x)`` with one free variable
6
+ ``x`` — "the individual currently under discussion" — such that ``d`` is in
7
+ the extension of ``C`` iff ``π(C, x)`` holds under ``x ↦ d``. Concretely:
8
+
9
+ - ``Atomic("C")`` ↦ ``C(x)`` (the concept becomes a unary predicate)
10
+ - ``⊤`` / ``⊥`` ↦ ``x = x`` / ``x ≠ x`` (see "Top and Bottom" below)
11
+ - ``¬C`` ↦ ``¬π(C, x)``
12
+ - ``C ⊓ D`` ↦ ``π(C, x) ∧ π(D, x)``
13
+ - ``C ⊔ D`` ↦ ``π(C, x) ∨ π(D, x)``
14
+ - ``∃r.C`` ↦ ``∃y (r(x, y) ∧ π(C, y))``
15
+ - ``∀r.C`` ↦ ``∀y (r(x, y) → π(C, y))``
16
+ - ``≥n r.C`` ↦ ``∃≥n y (r(x, y) ∧ π(C, y))`` (see "Qualified number restrictions")
17
+ - ``≤n r.C`` ↦ ``∃≤n y (r(x, y) ∧ π(C, y))``
18
+
19
+ with ``y`` a FRESH variable at every restriction, so nested restrictions
20
+ (``∃r.(∀s.C)``) never capture an outer variable (see "Variable freshness").
21
+ A role ``r`` becomes its own binary predicate ``r(·, ·)`` with no axioms
22
+ forced on it, which is exactly why the reduction is faithful: ALC roles are
23
+ arbitrary Kripke relations, and an uninterpreted FOL predicate is exactly
24
+ that (see also :func:`concept_to_modal` for the single-role reading through
25
+ propositional modal K instead of FOL).
26
+
27
+ Top and Bottom
28
+ --------------
29
+ The FOL node set has no first-class truth-constant (no ``⊤``/``⊥`` term
30
+ that ``Node.to_z3`` renders as Z3's literal ``True``/``False``); the closest
31
+ thing is the reserved falsum atom idiom used by :mod:`unicode_logic_kit.atp.fitch`
32
+ (``Atom("⊥", ())`` there, but that idiom is *propositional* and only means
33
+ something because the Fitch checker special-cases it — it does not carry to
34
+ an arbitrary Z3/Prover9/TPTP export, where a bare atom is just another
35
+ uninterpreted symbol that could be assigned False in some model). Rendering
36
+ ``⊤`` as an uninterpreted atom (``P(x) ∨ ¬P(x)`` for some fresh ``P``) is
37
+ truth-functionally a tautology but is an ugly, semantically unmotivated
38
+ detour through a predicate the concept never mentions.
39
+
40
+ Instead this module renders ``⊤``/``⊥`` through **equality**, which the FOL
41
+ Atom node already treats natively (``Atom("=", …)`` / ``Atom("≠", …)`` map to
42
+ Z3's built-in ``==``/``!=``, Prover9's ``=``, TPTP's ``=``/``!=`` — see
43
+ ``Atom.to_z3``/``to_prover9``/``to_tptp`` in ``fol/_fol_nodes.py``):
44
+
45
+ - ``⊤`` ↦ ``x = x`` — valid in every model (reflexivity), matching how the
46
+ tableau treats ``⊤``: it never causes a clash and never needs expansion,
47
+ i.e. it constrains nothing and holds of every individual.
48
+ - ``⊥`` ↦ ``x ≠ x`` — unsatisfiable in every model, matching how the
49
+ tableau treats ``⊥``: ``x : ⊥`` is *itself* a clash condition (see
50
+ ``_clash`` in :mod:`unicode_logic_kit.dl.tableau`), i.e. no individual can
51
+ satisfy it.
52
+
53
+ This is the standard textbook rendering, adds no extra symbols to the
54
+ signature, and is a genuine validity/unsatisfiability rather than a
55
+ model-dependent coincidence.
56
+
57
+ Variable freshness
58
+ -------------------
59
+ Each ``∃r.C`` / ``∀r.C`` introduces a bound variable minted by
60
+ :func:`~unicode_logic_kit.fol._identifiers.fresh_variables`: one letter plus
61
+ digits (``x0``, ``x1``, …), which is the shape the kit's VARIABLE terminal
62
+ actually accepts. Until 0.30.0 the name was ``f"{var}_{n}"``, and
63
+ ``∃x_1 (r(x, x_1) ∧ …)`` is text this kit printed and its own
64
+ :func:`unicode_logic_kit.api.parse_any` rejects — the underscore is not part of
65
+ the terminal. The letter is the seed name's first character when that is a
66
+ legal variable letter on its own and ``x`` otherwise, so an ABox individual
67
+ named ``alice`` yields ``x0`` rather than ``alice_1``; the seed name itself is
68
+ excluded, so the free variable can never be captured.
69
+
70
+ A single top-level call (one call to :func:`concept_to_fol`, or one of the two
71
+ independent halves of :func:`subsumption_to_fol`) shares ONE minter across its
72
+ entire recursive walk, so EVERY restriction anywhere in that call's concept
73
+ tree — nested *or* sibling — gets a pairwise-distinct name; in particular no
74
+ two restrictions nested along the same path can ever reuse a name, so
75
+ ``∃r.(∀s.C)`` translates to ``∃x0 (r(x, x0) ∧ ∀x1 (s(x0, x1) → π(C, x1)))``
76
+ with no risk of the inner ``∀`` accidentally binding the outer ``∃``'s
77
+ variable. Two SEPARATE top-level calls each start their own minter back at
78
+ ``x0`` and so may independently mint the same name (e.g.
79
+ ``subsumption_to_fol``'s antecedent and consequent, translated by two
80
+ independent calls to the internal translator, may both use ``x0``) — this is
81
+ not a capture risk because the two calls' quantifier scopes are siblings under
82
+ ``→``, not nested inside one another.
83
+
84
+ Bound variables never share a name with an individual
85
+ ------------------------------------------------------
86
+ ``Variable("x")`` and ``Constant("x")`` are two symbols to Z3 (a variable is the
87
+ symbol ``x!v`` and a constant is named as it is, see ``Z3Env``), and the unicode
88
+ text tells them apart (``x`` and ``'x'``). A target that gives the two one
89
+ namespace would not: it would write ``∀x P(x, 'x')`` with one symbol for both,
90
+ so a quantifier binding ``x`` would capture an individual called ``x`` there.
91
+ It captured it on Z3 as well while Z3 took the pair for one symbol: the image of
92
+ ``∃r.{x} ⊑ A`` printed ``∀x (r(x, x) → A(x))``, and ``api.prove`` over it called
93
+ a consistent knowledge base inconsistent, while the tableau (which has no
94
+ variables to capture) said consistent. The same held for every FIXED prefix
95
+ variable — the GCI's ``x``, the role axioms' ``x``/``y``/``z``, the data axioms'
96
+ ``x``/``v``/``w`` — not only the minted ones. So EVERY bound
97
+ variable of an image avoids EVERY individual of the knowledge base, whichever
98
+ target the image is written for: a prefix
99
+ variable that clashes is renamed (:func:`_binder_names`, to the first free
100
+ ``letter + digits`` — exact, being alpha-equivalence), consistently across all
101
+ the axioms of one :func:`kb_to_fol` call, and the minted ones avoid the renamed
102
+ names too. The one variable that is FREE, :func:`concept_to_fol`'s ``var``, is
103
+ the caller's own and so is refused on a clash instead of renamed.
104
+
105
+ Qualified number restrictions
106
+ ------------------------------
107
+ ``AtLeast``/``AtMost`` (≥n r.C / ≤n r.C — see :mod:`unicode_logic_kit.dl.tableau`'s
108
+ "Qualified number restrictions" section for the tableau side of ALCQ) route through
109
+ the EXISTING :class:`~unicode_logic_kit.fol.nodes.Count` node rather than a new
110
+ counting encoding written here: ``≥n r.C`` becomes ``Count("ge", Number(n), y,
111
+ r(x, y) ∧ π(C, y))`` and ``≤n r.C`` becomes the same with ``"le"``. ``Count`` already
112
+ carries a tested standard "distinct witnesses" first-order expansion
113
+ (:meth:`~unicode_logic_kit.fol.nodes.Count._expand`, bounded at ``n ≤ 500`` — see that
114
+ class's docstring) that :meth:`Count.to_z3` lowers through automatically, so this is
115
+ exactly the independent (Z3-backed) differential oracle
116
+ ``tests/test_dl_alcq.py`` cross-checks the tableau against, the same role
117
+ :func:`rbox_to_fol` plays for the RBox extension above. The counting variable ``y``
118
+ is minted by the SAME ``fresh()`` counter as every other restriction (see "Variable
119
+ freshness"), so a number restriction nested inside — or sibling to — an ordinary
120
+ ∃/∀ still gets a pairwise-distinct name throughout one top-level call.
121
+
122
+ Multi-role concepts and modal K
123
+ --------------------------------
124
+ ALC is exactly multi-modal K: an ``∃r.C``/``∀r.C`` pair over a *single* role
125
+ ``r`` is precisely ``◇C``/``□C`` in ordinary (single-relation) modal K. This
126
+ is what :func:`concept_to_modal` implements, generalising the single-role
127
+ ``_to_modal`` test helper in ``tests/test_dl_alc.py`` from a hardcoded role to
128
+ any single role name. It genuinely cannot go further: propositional modal K
129
+ has exactly ONE accessibility relation, so a concept using two or more
130
+ *distinct* role names has no faithful Box/Diamond rendering — encoding role
131
+ identity into, say, an indexed modality (``Knows(Constant(role), …)``) would
132
+ silently reinterpret the DL role as an epistemic agent, which is a different
133
+ logic with different validities (KT45 vs K). :func:`concept_to_modal`
134
+ therefore raises :class:`NotImplementedError` for multi-role concepts and
135
+ points callers at :func:`concept_to_fol`, which has no such limitation — a
136
+ role is just another predicate name, so FOL scales to any number of roles
137
+ for free.
138
+
139
+ GCIs, TBoxes, ABoxes
140
+ ---------------------
141
+ :func:`subsumption_to_fol` renders a general concept inclusion ``sub ⊑ sup``
142
+ as its universal closure ``∀x (π(sub, x) → π(sup, x))`` — the standard
143
+ reduction, matching :func:`unicode_logic_kit.dl.tableau.subsumes`'s own
144
+ ``sub ⊓ ¬sup`` unsatisfiability reduction (they are interderivable: ``sub ⊑
145
+ sup`` iff ``∀x (π(sub,x) → π(sup,x))`` is valid iff ``π(sub,x) ∧ ¬π(sup,x)``
146
+ is unsatisfiable). :func:`tbox_to_fol` conjoins one such closure per GCI in a
147
+ :class:`unicode_logic_kit.dl.tableau.TBox`; :func:`abox_to_fol` renders a
148
+ :class:`unicode_logic_kit.dl.tableau.ABox`'s concept assertions (``a : C`` ↦
149
+ ``π(C, a)`` with the individual ``a`` as a FOL *constant*, not a variable —
150
+ this matters for the ASCII-only Prover9/TPTP exporters, which uppercase
151
+ ``Variable`` names into their variable syntax but leave ``Constant`` names
152
+ alone) and role assertions (``(a, b) : r`` ↦ ``r(a, b)``), conjoined
153
+ together. An empty TBox/ABox translates to the same equality tautology used
154
+ for ``⊤`` (over a nullary placeholder constant, since there is no ``x`` in
155
+ scope at that point) — vacuously true, matching "no axioms" / "no
156
+ assertions" imposing no constraint.
157
+
158
+ An individual of any name has a printed text
159
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
160
+ The kit decides predicate-versus-term by the first character's ``isupper()``
161
+ (see ``fol/_identifiers.py``'s "WHY THE FIRST CHARACTER DECIDES PREDICATE VS.
162
+ TERM"), so in the first-order grammar no BARE word is a
163
+ :class:`~unicode_logic_kit.fol.nodes.Constant` whose name starts upper-case, and
164
+ none is one whose name is a variable token (``x``, ``x0``). An OWL individual,
165
+ however, is an IRI or a label, where capitals are the norm — in the OEO
166
+ ontology every one of the 98 ``ObjectHasValue`` fillers and all 8
167
+ identity-axiom individuals start with one. The printer therefore writes such a
168
+ constant in single quotes (:func:`~unicode_logic_kit.fol.constant_text`),
169
+ and the quoted name is the constant of exactly that name: ``Person('Alice')``,
170
+ ``'Alice' = 'Bob'`` and ``HasStateOfMatter(x, 'Liquid')`` read back as the
171
+ formulas this module built, an individual named ``x0`` prints ``'x0'`` and is no
172
+ free variable, and an individual whose name has a bare spelling stays bare
173
+ (``Person(alice)``). Until 0.30.0 the bare word was all there was, and the text of
174
+ an upper-case individual did not read back as the same formula, in two different
175
+ ways: in EQUALITY position (``abox_to_fol`` of an ``assert_same``/
176
+ ``assert_distinct``, or a ``Nominal``) it did not parse at all, and in ARGUMENT
177
+ position (a role assertion, or a :class:`~unicode_logic_kit.dl.concepts.HasValue`
178
+ image) it parsed in the third-order dialect, as a different formula, with the
179
+ individual read as a ``PredicateTerm``. A role and a class are PREDICATES and
180
+ have no quoted form, so a role spelled in lower case (``hasChild``) is still a
181
+ name that does not read back, and so is the name of a built-in datatype
182
+ (``xsd:integer``); the route that never goes through text — ``dl.abox_consistent``/
183
+ ``dl.instance_check`` over the tableau, and ``api.prove`` over the ``kb_to_fol``
184
+ NODES rather than their printed form — is unaffected by either. The cases are
185
+ asserted in ``tests/test_printed_text_reads_back.py``.
186
+
187
+ Inverse roles and nominals (I, O)
188
+ ------------------------------------
189
+ Unlike :mod:`unicode_logic_kit.dl.tableau` (which refuses both by name — see
190
+ that module's "Inverse roles and nominals (I, O)" section), the standard
191
+ translation to FOL has no trouble with either, so this module TRANSLATES
192
+ them rather than refusing them:
193
+
194
+ - :class:`~unicode_logic_kit.dl.concepts.InverseRole` (``r⁻``), used as a
195
+ restriction's ``role`` field, swaps the translated role atom's argument
196
+ order: ``∃r⁻.C`` ↦ ``∃y (r(y, x) ∧ π(C, y))`` and ``∀r⁻.C`` ↦
197
+ ``∀y (r(y, x) → π(C, y))`` (and likewise for ``AtLeast``/``AtMost``'s
198
+ ``Count`` matrix) — exactly the FOL reading of "an r-predecessor", the
199
+ standard translation of an inverse role (Baader et al., *DL Handbook*,
200
+ Ch. 2). This is a one-line change confined to the atom's argument order;
201
+ everything else about the restriction (the quantifier, the ``Count``
202
+ encoding for ``AtLeast``/``AtMost``) is untouched.
203
+ - :class:`~unicode_logic_kit.dl.concepts.Nominal` (``{a}``) ↦ ``x = a``, with
204
+ ``a`` rendered as the same kind of term ``concept_to_fol``'s own ``term``
205
+ argument already is (a :class:`~unicode_logic_kit.fol.nodes.Variable` when
206
+ translating a bare concept, a :class:`~unicode_logic_kit.fol.nodes.Constant`
207
+ when translating an ABox assertion via :func:`abox_to_fol` — see "GCIs,
208
+ TBoxes, ABoxes" above for why that distinction matters) — the textbook
209
+ reading of a nominal as "the singleton set named by the individual ``a``"
210
+ (Baader et al., *DL Handbook*, Ch. 2's "individual-generated" theories).
211
+ This is genuinely a NEW predicate-free case, not a reuse of an existing
212
+ branch (unlike ⊤/⊥, which reuse equality — see "Top and Bottom" above —
213
+ a nominal names a SPECIFIC individual, not a tautology/contradiction).
214
+ - :class:`~unicode_logic_kit.dl.concepts.HasValue` (``∃r.{a}``) ↦ the ground
215
+ atom ``r(x, a)``, NOT ``∃y (r(x, y) ∧ y = a)``. The two are the same
216
+ formula: ``y`` occurs only inside that conjunction, so one-point elimination
217
+ of the equality-bounded existential is a FOL validity. The reduced form is
218
+ the one rendered because it mints no variable and puts the individual in the
219
+ argument position OWL's own reading puts it in. The in-house tableau does NOT
220
+ decide a value restriction (it refuses it by name; see "Value restrictions
221
+ (ObjectHasValue)" in :mod:`unicode_logic_kit.dl.tableau`): this image with
222
+ ``api.prove``, and ``dl.external_*``, are what decide it.
223
+
224
+ Both directions of this translation are differentially tested (see
225
+ ``tests/test_dl_alc.py``): concept satisfiability/entailment against the
226
+ tableau where a genuinely independent second route exists (none does here —
227
+ these constructs are precisely what the tableau refuses — so the check runs
228
+ the OTHER direction instead, against a bounded FOL model finder/solver on the
229
+ emitted formula itself, per this kit's translation-faithfulness convention).
230
+
231
+ :func:`concept_to_modal` (single-role propositional K) is a DIFFERENT story:
232
+ propositional K has no converse modality and no nominal/naming construct at
233
+ all (that is HYBRID logic, a strictly different formalism this kit does not
234
+ implement), so it explicitly REFUSES both, by name, rather than attempt an
235
+ unfaithful encoding — see its own docstring.
236
+
237
+ RBoxes
238
+ ------
239
+ :func:`rbox_to_fol` renders a :class:`~unicode_logic_kit.dl.tableau.TBox`'s RBox
240
+ (role inclusions and transitivity declarations — see "Role hierarchies and
241
+ transitive roles (RBox)" in :mod:`unicode_logic_kit.dl.tableau`'s module
242
+ docstring) the same way :func:`tbox_to_fol` renders its concept-level GCIs: a
243
+ role inclusion ``r ⊑ s`` becomes its universal closure ``∀x,y (r(x,y) →
244
+ s(x,y))`` and a transitivity declaration ``Trans(r)`` becomes ``∀x,y,z
245
+ (r(x,y) ∧ r(y,z) → r(x,z))``, all conjoined together. This is the reduction
246
+ the tableau's ∀-rule generalisation (RBox rule H) and ∀+-rule (RBox rule S)
247
+ implement as completion-graph propagation, so ``subsumption_to_fol(C, D)`` being
248
+ entailed by the premises ``tbox_to_fol(tbox, concept_inclusions_only=True)`` and
249
+ ``rbox_to_fol(tbox)`` is the independent, structurally unrelated cross-check this
250
+ module's differential test battery runs through the kit's backends against
251
+ ``tableau.subsumes(C, D, tbox)``.
252
+
253
+ The data layer: a guarded ONE-sorted image
254
+ --------------------------------------------
255
+ OWL 2 has two disjoint domains, the object domain ``Δ_I`` and the data domain
256
+ ``Δ_D`` (see :mod:`unicode_logic_kit.dl.datatypes`). The image of the data layer
257
+ is plain first-order logic with two reserved unary predicates, ``OwlThing`` and
258
+ ``OwlData``, NOT the kit's many-sorted logic: a sorted constant must be
259
+ lower-case and every OEO individual and literal is CamelCase, and many-sorted
260
+ logic here is only guards under another syntax (its ``SortedQuantifier`` is
261
+ relativised away on the way to plain FOL) that supplies non-emptiness and
262
+ sub-sorts but not the one thing two-sortedness needs, disjointness.
263
+
264
+ * ``DataExists``/``DataForAll``/``DataAtLeast``/``DataAtMost`` mint a bound
265
+ variable from the SAME minter as every object restriction and guard it with
266
+ the data range's image ``δ(DR, w)``; ``DataHasValue`` is the ground atom
267
+ ``d(x, t)`` for the literal's term ``t`` and mints nothing.
268
+ * the data axioms are SIDE axioms, like the role box: ``SubDataPropertyOf``
269
+ ``∀x ∀v (d(x, v) → e(x, v))``, ``DataPropertyDomain`` ``∀x ∀v (d(x, v) →
270
+ π(C, x))``, ``DataPropertyRange`` ``∀x ∀v (d(x, v) → δ(DR, v))`` (the
271
+ datatype read as a unary predicate over the one domain — NOT the GCI
272
+ ``⊤ ⊑ ∀d.DR``, whose image would carry an ``x = x`` filler),
273
+ ``DatatypeDefinition`` ``∀v (D(v) ↔ δ(DR, v))``, and so on.
274
+ * what makes the image TWO-sorted is not in any one axiom and is not part of
275
+ the formula: :func:`data_sort_axioms` derives, from the vocabulary the
276
+ knowledge base actually uses, the separation ``∀t ¬(OwlThing(t) ∧
277
+ OwlData(t))``, the typing of every property and individual, the datatype
278
+ lattice and its disjointness, and the distinctness of distinct literals.
279
+ :func:`kb_to_fol` puts them in ``side_axioms``.
280
+ * ONE consequence is easy to miss, and is why the image of a GCI changes: with
281
+ a non-empty data domain in the theory, ``∀x (π(sub, x) → π(sup, x))`` also
282
+ ranges over data values, and a data value satisfies every antecedent that
283
+ says nothing about it (``⊤``, ``¬A``, ``∀r.C``). ``⊤ ⊑ {a}`` would then
284
+ identify a data value with the individual ``a`` — a contradiction with the
285
+ separation, for a knowledge base OWL 2 finds consistent. So in the two-sorted
286
+ image every GCI is relativised to the object domain, ``∀x (OwlThing(x) ∧
287
+ π(sub, x) → π(sup, x))``, and so is ``Reflexive(r)``. :func:`kb_to_fol` does
288
+ it, and builds the GOAL the same way: :meth:`KnowledgeBaseFOL.subsumption_goal`,
289
+ :meth:`~KnowledgeBaseFOL.unsatisfiability_goal` and
290
+ :meth:`~KnowledgeBaseFOL.instance_goal` relativise to whatever
291
+ :attr:`KnowledgeBaseFOL.separation` says, so a caller never has to know. (For
292
+ a hand-built query ``object_sort=True`` on :func:`subsumption_to_fol` is the
293
+ same relativisation.)
294
+ * the side axioms are derived from the vocabulary the knowledge base uses, so a
295
+ QUESTION that names a datatype, a data property, a literal or an object
296
+ property the knowledge base does not is asked of a weaker theory: the datatype
297
+ lattice, the typing and the literal facts for that name are simply not there.
298
+ ``kb_to_fol(tbox, abox, query=[concept, …])`` adds the vocabulary of the
299
+ concepts you are going to ask about to everything derived from the
300
+ vocabulary, and the three goal methods REFUSE, by name, a concept the bundle
301
+ does not cover — the wrong spelling cannot be followed silently.
302
+ * ONE name is ONE predicate, so a name used as both an object property and a
303
+ data property, or as both a class and a datatype, would be conflated into a
304
+ single predicate and change what follows (a functional ``P`` with an object
305
+ successor and a data value came out INCONSISTENT). OWL 2 DL forbids both
306
+ pairs; the image REFUSES them by name (``UnsupportedDatatypeError``, from
307
+ :func:`kb_to_fol`, :func:`data_sort_axioms`, :func:`databox_to_fol` and
308
+ :func:`abox_to_fol`) and asks for one of the two to be renamed. A class, an
309
+ object property and an individual sharing a name (OWL 2 DL punning that IS
310
+ allowed) are accepted, and are THREE symbols in the image: the unary
311
+ predicate ``A(x)`` of the class, the binary predicate ``A(x, y)`` of the
312
+ property and the constant ``A`` of the individual. A route that keys a
313
+ symbol on its kind and arity reads them as three — the TPTP writers do, so
314
+ ``api.prove`` with Vampire or E answers over a punned knowledge base.
315
+ * that refusal is per call, over what the call is given, so boxes rendered one
316
+ at a time and conjoined by hand are not seen together by any of them.
317
+ :func:`check_kb_names` runs the same check over several pieces at once — the
318
+ ``TBox`` and ``ABox`` the images were built from.
319
+
320
+ The two-sorted image is SOUND — every OWL 2 interpretation expands to a model of
321
+ it, so it entails nothing the knowledge base does not — and deliberately not
322
+ COMPLETE:
323
+ the lattice states the OWL 2 datatype map's subtype and disjointness facts,
324
+ literals are distinct within a family whose lexical-to-value map is injective,
325
+ and an ordering facet is an UNINTERPRETED comparison atom for ``api.prove`` (use
326
+ :mod:`unicode_logic_kit.atp.z3_arith` for arithmetic over the data ranges alone).
327
+ So ``proved`` transfers to OWL 2 and ``refuted`` does NOT, for a knowledge base
328
+ with a data layer: :attr:`KnowledgeBaseFOL.refutation_is_decisive` is ``False``
329
+ exactly then, and its docstring says what each prover status lets a caller
330
+ conclude. The other two separations are NOT sound for a data layer: without the
331
+ restriction of every inclusion to the object domain, ``⊤ ⊑ {a}`` also ranges
332
+ over data values, the premises are stronger than OWL 2's, and an OWL-consistent
333
+ knowledge base can have an inconsistent image. The goal methods of the bundle
334
+ refuse them for that reason.
335
+
336
+ Knowledge bases: the TBox/RBox split is a trap
337
+ -----------------------------------------------
338
+ :class:`~unicode_logic_kit.dl.tableau.TBox` holds BOTH the concept inclusions and the
339
+ RBox, but the FOL image of each lives in its own function, and
340
+ :func:`tbox_to_fol` renders ONLY the concept inclusions. A question proved against its
341
+ output alone is asked of a WEAKER theory — the GCIs hold, but the roles are unrelated
342
+ and none is transitive — so it can come back REFUTED for a subsumption the tableau
343
+ accepts (a false counterexample). The spelling that used to run, with the instance
344
+ that was recorded as a regression test::
345
+
346
+ t = TBox().add_role_inclusion("hasChild", "hasDescendant")
347
+ dl.subsumes(∃hasChild.⊤, ∃hasDescendant.⊤, t) # True
348
+ api.prove(subsumption_to_fol(∃hasChild.⊤, ∃hasDescendant.⊤),
349
+ [tbox_to_fol(t)]) # REFUTED: the role
350
+ # box was dropped
351
+
352
+ so :func:`tbox_to_fol` now RAISES :class:`RoleBoxOmittedError` for a TBox carrying a
353
+ role box, unless the caller writes ``concept_inclusions_only=True`` — the only way
354
+ to say "yes, I know the role box is not in this formula". The supported entry point
355
+ for a whole knowledge base is :func:`kb_to_fol`, which returns the knowledge base
356
+ and the role-box image SEPARATELY (a :class:`KnowledgeBaseFOL`), following the kit's
357
+ side-axiom convention (:class:`~unicode_logic_kit.comorphism.TranslationResult`): the
358
+ side axioms are never conjoined into the main formula, the caller passes them as
359
+ ``premises`` (see ``Comorphism.side_axioms`` for why folding them in is wrong).
360
+
361
+ The other renderers have no analogous silent drop, because none of them is handed a
362
+ TBox: :func:`concept_to_fol`, :func:`subsumption_to_fol` and :func:`abox_to_fol`
363
+ render exactly what they are given, which makes each of them an answer about the
364
+ EMPTY knowledge base (no GCIs, no role box). That is the right reading for a bare
365
+ concept or an ABox-only question, and the wrong one for any question *relative to*
366
+ a knowledge base — there the TBox image and the role-box image must be premises
367
+ too, which is what :class:`KnowledgeBaseFOL` bundles.
368
+ """
369
+
370
+ import re
371
+ from dataclasses import dataclass, field, fields, is_dataclass
372
+ from typing import FrozenSet, Iterable, List, Optional, Sequence, Set, Tuple
373
+
374
+ from ..fol.nodes import (
375
+ Node, Variable, Constant, Number, Atom,
376
+ Not as _FNot, And as _FAnd, Or as _FOr, Implies, Iff, Quantifier,
377
+ Box, Diamond, Count,
378
+ )
379
+ from ..fol._identifiers import fresh_variables, fresh_variable_like, variable_pattern
380
+ from ..fol._msfl_nodes import key_text
381
+ from .concepts import (
382
+ Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
383
+ InverseRole, Nominal, HasValue, DataExists, DataForAll, DataHasValue,
384
+ DataAtLeast, DataAtMost, DATA_CONCEPTS,
385
+ )
386
+ from .datatypes import (
387
+ OWL_DATA, OWL_THING, DataRange, Literal, UnsupportedDatatypeError,
388
+ canonical_datatype_name, datarange_literals, datarange_datatypes,
389
+ datarange_to_fol, datatype_ancestors, datatype_family,
390
+ EXACT_NUMBER_DATATYPES,
391
+ )
392
+ from .tableau import (
393
+ TBox, ABox, _AXIOM_KINDS, _abox_individual_names, _reject_abox_roles,
394
+ _reject_concept_role, _validate_data_box, _validate_role_box,
395
+ )
396
+
397
+ __all__ = [
398
+ "concept_to_fol", "subsumption_to_fol", "tbox_to_fol", "abox_to_fol",
399
+ "rbox_to_fol", "databox_to_fol", "data_sort_axioms", "check_kb_names",
400
+ "kb_to_fol", "KnowledgeBaseFOL", "SideAxiom", "RoleBoxOmittedError",
401
+ "concept_to_modal",
402
+ ]
403
+
404
+
405
+ # --------------------------------------------------------------------------- #
406
+ # Standard translation to FOL.
407
+ # --------------------------------------------------------------------------- #
408
+
409
+ def _individual_names(concept) -> set:
410
+ """Every individual name ``_translate`` will render as a ``Constant``.
411
+
412
+ Two concept kinds carry one: a :class:`~unicode_logic_kit.dl.concepts.Nominal`
413
+ (``{a}`` becomes ``term = a``) and a
414
+ :class:`~unicode_logic_kit.dl.concepts.HasValue` (``∃r.{a}`` becomes
415
+ ``r(term, a)``). Roles and atomic concepts become predicates, which live in
416
+ their own namespace. The RECURSION is generic over the dataclass fields so
417
+ a new concept type with a nested concept is reached without being named
418
+ here — but the ``individual`` FIELD is not, and a kind missed here fails
419
+ SILENTLY: the avoid set handed to :func:`_fresh_var_factory` would be
420
+ incomplete, so a minted bound variable could share a name with the
421
+ individual and CAPTURE it in a target that writes a variable and a constant
422
+ of one name as one symbol (``Variable("x0")`` is the unicode text ``x0`` and
423
+ ``Constant("x0")`` is ``'x0'``, and they are two symbols to Z3, but a target
424
+ with one namespace for both would read them as one: see "Bound variables
425
+ never share a name with an individual").
426
+ ``tests/test_dl_has_value.py`` has the regression for exactly that, with a
427
+ HasValue individual named ``"x0"``.
428
+ """
429
+ names = set()
430
+ stack = [concept]
431
+ while stack:
432
+ node = stack.pop()
433
+ if isinstance(node, (Nominal, HasValue)):
434
+ names.add(node.individual)
435
+ if is_dataclass(node):
436
+ for field in fields(node):
437
+ value = getattr(node, field.name)
438
+ if isinstance(value, Concept):
439
+ stack.append(value)
440
+ return names
441
+
442
+
443
+ def _fresh_var_factory(base: str, avoid=()):
444
+ """Return a callable producing variable names distinct from ``base``, from
445
+ ``avoid``, and from every name it has already produced (see "Variable
446
+ freshness" above).
447
+
448
+ The names come from :func:`~unicode_logic_kit.fol._identifiers.fresh_variables`
449
+ so that they are names the kit's own parser reads back; ``base`` only picks
450
+ the letter (and is itself excluded), because it may be an ABox individual's
451
+ name and so no legal variable at all. ``avoid`` is the individual names the
452
+ translation will render as constants (:func:`_individual_names`): a bound
453
+ variable that happened to share a name with one of them would CAPTURE it in
454
+ a target that writes ``Variable("a0")`` and ``Constant("a0")`` as one symbol
455
+ (the unicode text and Z3 keep the two apart, a target with one namespace for
456
+ both would not), so the nominal would stop denoting its own individual.
457
+ """
458
+ letter = base[:1] if re.fullmatch(variable_pattern(), base[:1] or " ") else "x"
459
+ used = {base} | set(avoid)
460
+
461
+ def fresh() -> str:
462
+ name = fresh_variables(1, letter=letter, avoid=used)[0]
463
+ used.add(name)
464
+ return name
465
+
466
+ return fresh
467
+
468
+
469
+ def _kb_individual_names(tbox: Optional[TBox], abox: Optional[ABox]) -> Set[str]:
470
+ """Every individual name of the whole knowledge base ``(tbox, abox)`` — the
471
+ set no BOUND variable of its image may share a name with.
472
+
473
+ It is :attr:`_Vocabulary.individuals`, the one walk that already reaches
474
+ every place a name can occur: the assertion positions of the ABox AND the
475
+ fillers of every nominal and value restriction in a concept inclusion, a
476
+ domain or range axiom, a data property domain or an ABox class assertion.
477
+ (:attr:`KnowledgeBaseFOL.individuals` deliberately lists only the ABox's
478
+ own, but a filler is a ``Constant`` of the image all the same.) Reusing it
479
+ keeps ONE definition of "an individual of this knowledge base" instead of a
480
+ second hand-written walk that could drift from it.
481
+ """
482
+ return set(_collect_vocabulary(tbox, abox).individuals)
483
+
484
+
485
+ def _binder_names(preferred: Sequence[str], avoid: Iterable[str]) -> Tuple[str, ...]:
486
+ """The bound-variable names to use for the prefix variables ``preferred``,
487
+ given the individual names ``avoid`` the image renders as ``Constant``\\ s.
488
+
489
+ A name that clashes with an individual is RENAMED, never refused: a bound
490
+ variable called ``x`` and a constant called ``x`` are two symbols to Z3 and
491
+ two texts in the unicode syntax (``x`` and ``'x'``), but one symbol in a
492
+ target with a single namespace for both, and when Z3 took them for one,
493
+ ``∃r.{x} ⊑ A`` printed ``∀x (r(x, x) → A(x))`` —
494
+ the quantifier captured the individual — and the knowledge base
495
+ ``∃r.{x} ⊑ ⊥`` with ``r(a, a)``, ``a ≠ x`` (which only forbids an r-edge
496
+ INTO the individual ``x``) was reported inconsistent. Renaming a bound
497
+ variable is exact (alpha-equivalence), so a name that does not clash is
498
+ returned untouched and every image that never named an individual
499
+ ``x``/``y``/``z`` is byte-for-byte what it was.
500
+
501
+ The replacement is :func:`~unicode_logic_kit.fol._identifiers.fresh_variable_like`
502
+ — the first free ``letter + digits`` — and avoids every individual, every
503
+ OTHER preferred name and every replacement already made, so two prefix
504
+ variables never collapse onto one. A caller that goes on to mint further
505
+ variables must hand THESE names to :func:`_fresh_var_factory`, not the
506
+ preferred ones: ``x`` renamed to ``x1`` leaves ``x`` itself free for the
507
+ minter, which would otherwise be allowed to reuse it.
508
+ """
509
+ avoid = set(avoid)
510
+ taken = set(preferred)
511
+ chosen: List[str] = []
512
+ for name in preferred:
513
+ if name in avoid:
514
+ name = fresh_variable_like(name, avoid | taken)
515
+ taken.add(name)
516
+ chosen.append(name)
517
+ return tuple(chosen)
518
+
519
+
520
+ def _role_atom(role, x: Node, y: Node) -> Node:
521
+ """Render the role atom for an ``x —role→ y`` edge: ``role(x, y)``, or — when
522
+ ``role`` is an :class:`~unicode_logic_kit.dl.concepts.InverseRole` — ``role(y,
523
+ x)`` (see the module docstring's "Inverse roles and nominals (I, O)"
524
+ section: the standard translation of ``r⁻`` swaps the atom's argument order,
525
+ nothing else).
526
+ """
527
+ if isinstance(role, InverseRole):
528
+ return Atom(role.role, (y, x))
529
+ return Atom(role, (x, y))
530
+
531
+
532
+ def _t_top(concept, term, fresh) -> Node:
533
+ return Atom("=", (term, term))
534
+
535
+
536
+ def _t_bottom(concept, term, fresh) -> Node:
537
+ return Atom("≠", (term, term))
538
+
539
+
540
+ def _t_atomic(concept, term, fresh) -> Node:
541
+ return Atom(concept.name, (term,))
542
+
543
+
544
+ def _t_nominal(concept, term, fresh) -> Node:
545
+ return Atom("=", (term, Constant(concept.individual)))
546
+
547
+
548
+ def _t_has_value(concept, term, fresh) -> Node:
549
+ """``π(∃r.{a}, t)`` as the ground atom ``r(t, a)`` — see
550
+ :class:`~unicode_logic_kit.dl.concepts.HasValue` for the one-point
551
+ elimination that makes this the same formula as ``∃y (r(t, y) ∧ y = a)``.
552
+
553
+ ``_role_atom``, not ``Atom`` by hand, so an
554
+ :class:`~unicode_logic_kit.dl.concepts.InverseRole`-valued role gets the
555
+ swapped argument order ``r(a, t)`` for free; ``Constant``, not
556
+ ``Variable``, exactly as :func:`abox_to_fol` renders an ABox individual,
557
+ which is what the ASCII Prover9/TPTP exporters need to tell a name from a
558
+ bound variable. No variable is minted, so ``fresh`` is unused.
559
+ """
560
+ return _role_atom(concept.role, term, Constant(concept.individual))
561
+
562
+
563
+ def _t_not(concept, term, fresh) -> Node:
564
+ return _FNot(_translate(concept.concept, term, fresh))
565
+
566
+
567
+ def _t_and(concept, term, fresh) -> Node:
568
+ return _FAnd(_translate(concept.left, term, fresh),
569
+ _translate(concept.right, term, fresh))
570
+
571
+
572
+ def _t_or(concept, term, fresh) -> Node:
573
+ return _FOr(_translate(concept.left, term, fresh),
574
+ _translate(concept.right, term, fresh))
575
+
576
+
577
+ def _t_exists(concept, term, fresh) -> Node:
578
+ w = Variable(fresh())
579
+ return Quantifier("∃", w, _FAnd(
580
+ _role_atom(concept.role, term, w), _translate(concept.concept, w, fresh)))
581
+
582
+
583
+ def _t_forall(concept, term, fresh) -> Node:
584
+ w = Variable(fresh())
585
+ return Quantifier("∀", w, Implies(
586
+ _role_atom(concept.role, term, w), _translate(concept.concept, w, fresh)))
587
+
588
+
589
+ def _t_count(concept, term, fresh) -> Node:
590
+ w = Variable(fresh())
591
+ matrix = _FAnd(_role_atom(concept.role, term, w),
592
+ _translate(concept.concept, w, fresh))
593
+ op = "ge" if isinstance(concept, AtLeast) else "le"
594
+ return Count(op, Number(concept.n), w, matrix)
595
+
596
+
597
+ def _t_data_exists(concept, term, fresh) -> Node:
598
+ """``π(∃d.DR, t)`` = ``∃w (d(t, w) ∧ δ(DR, w))``: the exact mirror of
599
+ :func:`_t_exists` with a data atom, the bound variable minted from the SAME
600
+ factory so it is pairwise distinct from every object restriction's."""
601
+ w = Variable(fresh())
602
+ return Quantifier("∃", w, _FAnd(
603
+ Atom(concept.prop, (term, w)), datarange_to_fol(concept.datarange, w)))
604
+
605
+
606
+ def _t_data_forall(concept, term, fresh) -> Node:
607
+ """``π(∀d.DR, t)`` = ``∀w (d(t, w) → δ(DR, w))``."""
608
+ w = Variable(fresh())
609
+ return Quantifier("∀", w, Implies(
610
+ Atom(concept.prop, (term, w)), datarange_to_fol(concept.datarange, w)))
611
+
612
+
613
+ def _t_data_has_value(concept, term, fresh) -> Node:
614
+ """``π(DataHasValue(d lt), t)`` = the ground atom ``d(t, t_lt)``. A literal
615
+ denotes ONE fixed data value (OWL 2 §2.3.4: ``{ x | (x, lt^LT) ∈ d^DP }``),
616
+ so there is no quantifier, no bound variable and no datatype guard — the
617
+ literal already pins its datatype. ``fresh`` is unused."""
618
+ return Atom(concept.prop, (term, concept.value.to_term()))
619
+
620
+
621
+ def _t_data_count(concept, term, fresh) -> Node:
622
+ """``≥n d.DR`` / ``≤n d.DR`` through the SAME ``Count`` node the object
623
+ cardinalities use, so ``Count._expand`` and ``Count.to_z3`` carry it."""
624
+ w = Variable(fresh())
625
+ matrix = _FAnd(Atom(concept.prop, (term, w)), datarange_to_fol(concept.datarange, w))
626
+ op = "ge" if isinstance(concept, DataAtLeast) else "le"
627
+ return Count(op, Number(concept.n), w, matrix)
628
+
629
+
630
+ # One entry per concrete concept class. A chain of `isinstance` branches was the
631
+ # single worst merge point in this file -- every new class expression kind added
632
+ # one branch in the middle of one function -- so the dispatch is a table a new
633
+ # kind appends ONE line to, keyed by exact type (dl.concepts' concept classes
634
+ # are all concrete frozen dataclasses with no subclassing, so exact-type lookup
635
+ # is total: an unlisted type is a genuine programming error, reported as such).
636
+ _TRANSLATORS = {
637
+ Top: _t_top,
638
+ Bottom: _t_bottom,
639
+ Atomic: _t_atomic,
640
+ Nominal: _t_nominal,
641
+ HasValue: _t_has_value,
642
+ Not: _t_not,
643
+ And: _t_and,
644
+ Or: _t_or,
645
+ Exists: _t_exists,
646
+ ForAll: _t_forall,
647
+ AtLeast: _t_count,
648
+ AtMost: _t_count,
649
+ DataExists: _t_data_exists,
650
+ DataForAll: _t_data_forall,
651
+ DataHasValue: _t_data_has_value,
652
+ DataAtLeast: _t_data_count,
653
+ DataAtMost: _t_data_count,
654
+ }
655
+
656
+
657
+ def _translate(concept: Concept, term: Node, fresh) -> Node:
658
+ """Render ``concept`` as a FOL formula with ``term`` (a Variable or
659
+ Constant) standing for the individual under discussion; ``fresh`` mints
660
+ the bound variable for each nested restriction (see :func:`_fresh_var_factory`).
661
+ """
662
+ handler = _TRANSLATORS.get(type(concept))
663
+ if handler is None:
664
+ raise TypeError(f"concept_to_fol: unsupported concept {type(concept).__name__}")
665
+ # An OWL 2 built-in property name (or ``=``/``≠``) as THIS node's role is
666
+ # refused by name, not rendered as the ordinary predicate of that name: the
667
+ # universal/empty property is not one, and the image of ``∃owl:bottom…C``
668
+ # as an uninterpreted atom is satisfiable where the restriction is not.
669
+ # Here, in the one dispatch every concept passes through, so no handler has
670
+ # to remember it.
671
+ _reject_concept_role(concept, where="dl.translate")
672
+ return handler(concept, term, fresh)
673
+
674
+
675
+ def concept_to_fol(concept: Concept, var: str = "x") -> Node:
676
+ """Render ``concept`` as a FOL formula ``π(concept, var)`` with one free
677
+ variable ``var`` (see the module docstring for the translation rules).
678
+
679
+ ``concept`` is satisfiable (:func:`unicode_logic_kit.dl.tableau.concept_satisfiable`)
680
+ iff ``∃var π(concept, var)`` is FOL-satisfiable — wrap the result in a
681
+ ``Quantifier("∃", Variable(var), …)`` to test that directly (e.g. via
682
+ :func:`unicode_logic_kit.is_satisfiable`).
683
+
684
+ This is satisfiability w.r.t. the EMPTY knowledge base: nothing about any
685
+ TBox or RBox is in the formula. For ``concept_satisfiable(concept, tbox)``
686
+ pass ``kb_to_fol(tbox).tbox_premises`` as the premises of the query (see
687
+ :func:`kb_to_fol`); the role box in particular is NOT implied by anything
688
+ here.
689
+
690
+ A concept with a DATA restriction (``DataExists`` and its four siblings) is
691
+ NOT answered by this formula closed under ``∃``: the formula says nothing
692
+ about the data domain, so a data value satisfies the existential and
693
+ ``∃HasV.xsd:integer ⊓ ∀HasV.xsd:string`` would come out satisfiable, where
694
+ OWL 2 makes it unsatisfiable (the two datatypes are disjoint). Build the
695
+ knowledge base with ``kb_to_fol(tbox, abox, query=[concept])`` and ask
696
+ ``kb.unsatisfiability_goal(concept)``, which carries the sort and datatype
697
+ axioms the question needs. The registry edge ``alc → fol`` refuses such a
698
+ concept for exactly that reason; this function renders it, because the
699
+ knowledge-base images are built from it.
700
+
701
+ Raises:
702
+ ValueError: ``var`` is the name of an individual the concept itself
703
+ names (a nominal or value restriction). ``var`` is the one FREE
704
+ variable of the result — the caller quantifies over it — so it
705
+ cannot be renamed behind the caller's back (every other bound
706
+ variable of an image is renamed to avoid a clash, see
707
+ :func:`_binder_names`); and a target that writes ``Variable("x")``
708
+ and ``Constant("x")`` as one symbol (Z3 and the unicode text do
709
+ not) would let ``∃x`` over ``π(∃r.{x}, x)`` bind
710
+ the individual. Pass another ``var``.
711
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
712
+ a restriction's role is an OWL 2 built-in property
713
+ name or ``=``/``≠`` (see :func:`~unicode_logic_kit.dl.tableau.reserved_role`):
714
+ an uninterpreted predicate of that name would be a different
715
+ restriction.
716
+ """
717
+ _check_concepts_punning((concept,), "dl.concept_to_fol")
718
+ names = _individual_names(concept)
719
+ if var in names:
720
+ raise ValueError(
721
+ f"dl.concept_to_fol: the free variable {var!r} has the same name as "
722
+ f"an individual this concept names (a nominal or a value "
723
+ f"restriction). The unicode text ({var} against {var!r}) and Z3 "
724
+ f"keep a variable and a constant of one name apart, but a target "
725
+ f"that writes both as one symbol would let whatever quantifies "
726
+ f"{var!r} bind the individual, and the image is kept free of that "
727
+ f"clash for every target. Pass var= another name, "
728
+ f"e.g. var={fresh_variable_like(var, names | {var})!r}.")
729
+ return _translate(concept, Variable(var), _fresh_var_factory(var, names))
730
+
731
+
732
+ def subsumption_to_fol(sub: Concept, sup: Concept, var: str = "x", *,
733
+ object_sort: bool = False) -> Node:
734
+ """Render the GCI ``sub ⊑ sup`` as its universal closure
735
+ ``∀var (π(sub, var) → π(sup, var))``.
736
+
737
+ ``object_sort=True`` relativises the closure to the OBJECT domain,
738
+ ``∀var (OwlThing(var) ∧ π(sub, var) → π(sup, var))`` — the form a GCI takes
739
+ in the two-sorted image of a knowledge base that has a data layer (see
740
+ "The data layer" in the module docstring: unrelativised, the quantifier
741
+ also ranges over data values, which satisfy every antecedent that says
742
+ nothing about them). It is what a GOAL must look like to be the question
743
+ :func:`kb_to_fol` ``(…, separation="two-sorted")`` poses. The default is
744
+ the plain closure, byte-identical to every release before the data layer.
745
+
746
+ A goal asked of a knowledge base is better built by the knowledge base:
747
+ :meth:`KnowledgeBaseFOL.subsumption_goal` picks the relativisation that
748
+ fits ``kb.separation`` and refuses a concept whose vocabulary the bundle's
749
+ side axioms do not cover. On a TWO-sorted bundle the plain default spelling
750
+ answers ``refuted`` for a pure object-level subsumption that holds
751
+ (``A ⊑ B``, ``B ⊑ C`` entail ``A ⊑ C``, and the unrelativised goal asks
752
+ about data values too) — silently, which is why the methods exist.
753
+
754
+ ``sub ⊑ sup`` holds (:func:`unicode_logic_kit.dl.tableau.subsumes`) iff this
755
+ formula is FOL-valid — check via :func:`unicode_logic_kit.is_valid`.
756
+
757
+ "Valid" is subsumption w.r.t. the EMPTY knowledge base. Subsumption relative
758
+ to a TBox (``subsumes(sub, sup, tbox)``) is NOT ``is_valid`` of this formula
759
+ and NOT ``Implies(tbox_to_fol(tbox), …)`` either — the latter drops the role
760
+ box. Use ``api.prove(subsumption_to_fol(sub, sup), kb_to_fol(tbox).tbox_premises)``
761
+ (see :func:`kb_to_fol`).
762
+
763
+ ``var`` names the variable the closure binds, and it is a PREFERENCE: when
764
+ ``sub`` or ``sup`` names an individual (a nominal, a value restriction) of
765
+ that very name, the bound variable is renamed to the first free ``letter +
766
+ digits`` (``∃r.{x} ⊑ A`` prints ``∀x0 (r(x0, 'x') → A(x0))``, not the
767
+ ``∀x (r(x, 'x') → A(x))`` that a target with one namespace for variables
768
+ and constants would read as a capture). The quantifier binds the variable, so
769
+ the rename is exact — the contrast with :func:`concept_to_fol`, whose
770
+ ``var`` is FREE and is refused instead.
771
+
772
+ Raises:
773
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
774
+ a restriction's role is an OWL 2 built-in property
775
+ name or ``=``/``≠``.
776
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
777
+ ``sub`` and ``sup`` together use one name as
778
+ two kinds OWL 2 DL keeps apart — an object property and a data
779
+ property, or a class and a datatype (``∃P.B ⊓ ∃P.xsd:integer``): the
780
+ image has one predicate per name. The refusal is per call, over
781
+ what the call is given; whether a name clashes with the knowledge
782
+ base the goal is asked of is checked by the bundle's own goal
783
+ methods (:meth:`KnowledgeBaseFOL.subsumption_goal`).
784
+ """
785
+ _check_concepts_punning((sub, sup), "dl.subsumption_to_fol")
786
+ return _subsumption_image(sub, sup, var, object_sort, ())
787
+
788
+
789
+ def _subsumption_image(sub: Concept, sup: Concept, var: str, object_sort: bool,
790
+ avoid: Iterable[str]) -> Node:
791
+ """:func:`subsumption_to_fol`, with ``avoid`` — the individual names of the
792
+ WHOLE knowledge base the GCI is part of — added to the two concepts' own.
793
+
794
+ The prefix variable is bound by the closure, so a ``var`` that clashes with
795
+ an individual is renamed (:func:`_binder_names`) rather than left to capture
796
+ it, and the variables the two translations mint avoid the RENAMED name."""
797
+ avoid = set(avoid) | _individual_names(sub) | _individual_names(sup)
798
+ (var,) = _binder_names((var,), avoid)
799
+ v = Variable(var)
800
+ antecedent = _translate(sub, v, _fresh_var_factory(var, avoid))
801
+ consequent = _translate(sup, v, _fresh_var_factory(var, avoid))
802
+ if object_sort:
803
+ antecedent = _FAnd(Atom(OWL_THING, (v,)), antecedent)
804
+ return Quantifier("∀", v, Implies(antecedent, consequent))
805
+
806
+
807
+ # The constant the empty-knowledge-base tautology is built from. ``c_`` plus a
808
+ # word is the kit's CONSTANT terminal (fol._identifiers.constant_pattern), so
809
+ # the text this prints reads back through api.parse_any as the SAME formula.
810
+ # Until 0.30.0 this was Constant("_"), which printed `_ = _` — text the kit's
811
+ # own parser rejects ("Unexpected character '_'"), so an RBox-only or empty
812
+ # knowledge base violated the "everything printed reads back" property that
813
+ # tests/test_printed_text_reads_back.py exists to hold (measured on the OEO
814
+ # fragment: 150 of 3636 accepted axioms printed an unreadable image for exactly
815
+ # this reason, every one of them an RBox-only knowledge base whose `formula` is
816
+ # this tautology). `c = c` is valid whatever `c` denotes, so a knowledge base
817
+ # that happens to name an individual `c_tautology` is still rendered correctly
818
+ # — the same argument concept_to_modal's reserved `_MODAL_TRUE_ATOM` makes.
819
+ # (A constant of any other name has a text too, since the printer quotes what
820
+ # no bare word spells: `Constant("_")` would print `'_' = '_'`, which reads
821
+ # back. The bare `c_tautology` keeps the text of 0.30.0 for every image that
822
+ # holds the tautology.)
823
+ _TAUTOLOGY_CONSTANT = Constant("c_tautology")
824
+
825
+
826
+ def _tautology() -> Node:
827
+ """A closed, genuinely valid formula — the FOL image of "no constraint"
828
+ (an empty TBox or ABox). See "GCIs, TBoxes, ABoxes" in the module docstring.
829
+ """
830
+ return Atom("=", (_TAUTOLOGY_CONSTANT, _TAUTOLOGY_CONSTANT))
831
+
832
+
833
+ def _conjoin(parts: List[Node]) -> Node:
834
+ """Fold a (possibly empty) list of formulas into a single conjunction,
835
+ BALANCED: depth ``ceil(log2 n)`` rather than ``n``.
836
+
837
+ A left fold — what this did until 0.30.0 — makes the conjunction's depth
838
+ equal the number of axioms, and the kit's node operations are recursive, so
839
+ a real ontology's image could be built but not used. Measured on this tree
840
+ with the default ``sys.getrecursionlimit() == 1000``: a TBox of 495 concept
841
+ inclusions printed, 496 raised ``RecursionError`` from the shared printer,
842
+ and at 1000 conjuncts EVERY operation failed — ``to_unicode_str``,
843
+ ``to_dict``, ``to_tptp``, ``to_prover9``, ``to_z3``, and ``__eq__`` /
844
+ ``__hash__``, the last two generated by ``@dataclass`` and so beyond any
845
+ printer fix. The OEO fragment (3091 concept inclusions) was already over
846
+ that line. Only reducing the TREE's depth fixes all seven at once; raising
847
+ the recursion limit would trade a clean exception for a segfault.
848
+
849
+ A conjunction is associative, so this is the SAME formula up to
850
+ associativity: identical models, and ``_formula_alpha_equal`` unaffected.
851
+ What DOES change is printed parenthesisation for three or more conjuncts
852
+ (``And(And(P, Q), R)`` prints ``P ∧ Q ∧ R``; the balanced
853
+ ``And(And(P, Q), And(R, S))`` prints ``P ∧ Q ∧ (R ∧ S)``) — identical for
854
+ 0, 1 and 2 conjuncts, which is every pinned string in the suite.
855
+ """
856
+ if not parts:
857
+ return _tautology()
858
+ level = list(parts)
859
+ while len(level) > 1:
860
+ level = [_FAnd(level[i], level[i + 1]) if i + 1 < len(level) else level[i]
861
+ for i in range(0, len(level), 2)]
862
+ return level[0]
863
+
864
+
865
+ def _side_axiom_census(tbox: TBox) -> str:
866
+ """``"2 SubObjectPropertyOf axiom(s), 1 TransitiveObjectProperty axiom(s)"``
867
+ — the side axioms ``tbox`` carries, named by their OWL keyword.
868
+
869
+ Derived from :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`' ``part ==
870
+ "side"`` rows, so a side-axiom kind added later names itself in
871
+ :class:`RoleBoxOmittedError`'s message without anyone editing the message.
872
+ """
873
+ parts = []
874
+ for row in _AXIOM_KINDS:
875
+ if row.holder != "tbox" or row.part != "side":
876
+ continue
877
+ count = len(getattr(tbox, row.field))
878
+ if count:
879
+ parts.append(f"{count} {row.kind} axiom(s)")
880
+ return ", ".join(parts)
881
+
882
+
883
+ def _carries_data_box(tbox: TBox) -> bool:
884
+ """True iff ``tbox`` stores an axiom of a ``layer == "data"`` side kind (a
885
+ data-box axiom) -- the ones :func:`databox_to_fol`, not :func:`rbox_to_fol`,
886
+ renders. Derived from :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`, like
887
+ :func:`_side_axiom_census`, so a data-box kind added later is named too."""
888
+ return any(row.holder == "tbox" and row.part == "side" and row.layer == "data"
889
+ and getattr(tbox, row.field) for row in _AXIOM_KINDS)
890
+
891
+
892
+ class RoleBoxOmittedError(ValueError):
893
+ """Raised by :func:`tbox_to_fol` when the TBox carries a role box and the
894
+ caller did not say that rendering only the concept inclusions is intended.
895
+
896
+ See "Knowledge bases: the TBox/RBox split is a trap" in the module
897
+ docstring: the concept-inclusion image alone is not the TBox's meaning, and
898
+ using it as the knowledge base can produce false counterexamples.
899
+ """
900
+
901
+
902
+ def tbox_to_fol(tbox: TBox, var: str = "x", *, concept_inclusions_only: bool = False,
903
+ object_sort: bool = False) -> Node:
904
+ """Render every GCI/equivalence in ``tbox`` as its universal closure
905
+ (:func:`subsumption_to_fol`) and conjoin them.
906
+
907
+ An empty TBox renders as a tautology (no axioms ⇒ no constraint). The
908
+ TBox is entailment-consistent with :func:`unicode_logic_kit.dl.tableau.TBox`:
909
+ equivalences are stored as the pair of inclusions they abbreviate
910
+ (``TBox.add_equivalence``), so they fall out of ``tbox.inclusions`` with
911
+ no special case needed here.
912
+
913
+ **Only the concept inclusions are rendered.** A :class:`TBox` also holds a
914
+ role box (role inclusions, transitivity and every other role-box axiom — the
915
+ ``part == "side"`` rows of ``dl.tableau._AXIOM_KINDS``) and a data box, and
916
+ their image is NOT in the result; a formula built from this function alone
917
+ therefore describes a weaker theory than ``tbox`` and can be refuted where
918
+ the tableau's ``subsumes(…, tbox)`` succeeds (a false counterexample). To
919
+ make that impossible to do by accident, a TBox that carries any such side
920
+ axiom raises :class:`RoleBoxOmittedError` — use :func:`kb_to_fol`
921
+ (the knowledge base plus the role box as separate premises) or
922
+ :func:`rbox_to_fol` for the role-box image (:func:`databox_to_fol` for the
923
+ data-box image, which the message names when the TBox carries one). Pass
924
+ ``concept_inclusions_only=True`` to say, at the call site, that the
925
+ concept-inclusion image alone is what you want (e.g. to render or inspect the
926
+ GCIs, or when the role box is supplied some other way).
927
+
928
+ The condition is DERIVED from :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`
929
+ via :meth:`~unicode_logic_kit.dl.tableau.TBox.has_side_axioms`, not written out
930
+ over the fields that happened to exist when it was added: a TBox field that
931
+ escaped a hand-written ``or`` here would render as a tautology and raise
932
+ nothing, which is a silent loss rather than an error.
933
+
934
+ ``object_sort=True`` relativises every GCI to the object domain — see
935
+ :func:`subsumption_to_fol`.
936
+
937
+ A class and an object property of one name are two symbols in the result
938
+ (``A(x)`` and ``A(x, y)``), as in every image of this module — see
939
+ :func:`kb_to_fol`.
940
+
941
+ Raises:
942
+ RoleBoxOmittedError: ``tbox`` carries a side axiom — a role-box axiom or
943
+ a data-box axiom — and ``concept_inclusions_only`` is false.
944
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
945
+ the TBox uses one name as two kinds OWL 2 DL
946
+ keeps apart (an object property and a data property, a class and a
947
+ datatype) — see :func:`kb_to_fol`.
948
+ """
949
+ if not concept_inclusions_only and tbox.has_side_axioms():
950
+ # rbox_to_fol renders the ROLE box only (a tautology for a TBox whose
951
+ # side axioms are all data-box ones); the data box has its own renderer,
952
+ # which the message names whenever the TBox carries a data-box axiom.
953
+ images = ("rbox_to_fol(tbox) for the role-box image"
954
+ + (" and databox_to_fol(tbox) for the data-box image"
955
+ if _carries_data_box(tbox) else ""))
956
+ raise RoleBoxOmittedError(
957
+ f"tbox_to_fol: this TBox carries side axioms ({_side_axiom_census(tbox)}) "
958
+ "that tbox_to_fol does not render, so its output alone is a WEAKER theory "
959
+ "than the TBox and can be refuted where dl.subsumes(…, tbox) succeeds. Use "
960
+ "kb_to_fol(tbox, abox) (the knowledge base and the side axioms, to be "
961
+ f"passed as premises) or, for one box alone, {images}; "
962
+ "pass concept_inclusions_only=True if the concept-inclusion image alone "
963
+ "is really what you want.")
964
+ vocabulary = _collect_vocabulary(tbox, None)
965
+ _check_name_punning(vocabulary, "dl.tbox_to_fol")
966
+ return _tbox_image(tbox, var, object_sort, set(vocabulary.individuals))
967
+
968
+
969
+ def _tbox_image(tbox: TBox, var: str, object_sort: bool,
970
+ avoid: Iterable[str]) -> Node:
971
+ """The conjunction of every GCI of ``tbox``, each bound variable avoiding
972
+ ``avoid`` — the individual names of the whole knowledge base — so that one
973
+ :func:`kb_to_fol` call binds the same prefix variable in every conjunct."""
974
+ return _conjoin([_subsumption_image(sub, sup, var, object_sort, avoid)
975
+ for sub, sup in tbox.inclusions])
976
+
977
+
978
+ @dataclass(frozen=True)
979
+ class SideAxiom:
980
+ """One side axiom of a knowledge base's FOL image: a premise that is NEVER
981
+ a conjunct of :attr:`KnowledgeBaseFOL.formula` (see :func:`kb_to_fol`).
982
+
983
+ Fields:
984
+
985
+ * ``kind`` — the OWL 2 keyword this axiom came from, e.g.
986
+ ``"SubObjectPropertyOf"`` or ``"TransitiveObjectProperty"``. The OWL name
987
+ rather than an internal one, so a caller can census an ontology's image
988
+ per axiom kind (:meth:`KnowledgeBaseFOL.axioms_of_kind`) without a second
989
+ translation table. It is the ``kind`` column of
990
+ :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`.
991
+ * ``group`` — which route owns the premise: ``"rbox"`` for a role-box
992
+ axiom, ``"data"`` for the image of a data-box axiom, ``"sort"`` for the
993
+ object/data separation and the typing it needs, ``"datatype"`` for the
994
+ datatype lattice and the literals. (:attr:`KnowledgeBaseFOL.rbox_axioms`
995
+ and :attr:`~KnowledgeBaseFOL.data_axioms` are the two cases the kit itself
996
+ asks about.) A ``group`` rather than a type hierarchy because the
997
+ caller's question is a fork, not a taxonomy. The ``kind`` of a ``"sort"``
998
+ or ``"datatype"`` axiom is a descriptive name, not an OWL keyword — there
999
+ is no OWL axiom it came from.
1000
+ * ``formula`` — the axiom itself.
1001
+ """
1002
+
1003
+ kind: str
1004
+ group: str
1005
+ formula: Node
1006
+
1007
+
1008
+ def _forall(variables: List[Variable], body: Node) -> Node:
1009
+ """``∀v1 … ∀vn body`` — the universal closure, outermost variable first."""
1010
+ for variable in reversed(variables):
1011
+ body = Quantifier("∀", variable, body)
1012
+ return body
1013
+
1014
+
1015
+ def _chain_variables(length: int, x: str, y: str, z: str,
1016
+ avoid: Iterable[str] = ()) -> List[Variable]:
1017
+ """``length`` variable names for a property chain's quantifier prefix.
1018
+
1019
+ The first three are ``x``, ``y``, ``z`` — the same three
1020
+ :func:`_rbox_axioms` already uses for transitivity, which IS the chain
1021
+ ``r ∘ r ⊑ r``, so the two images stay visually comparable. Beyond three the
1022
+ kit has no fourth canonical name, so the rest come from
1023
+ :func:`_fresh_var_factory` (``x0``, ``x1``, …), which yields the
1024
+ one-letter-plus-digits shape the kit's VARIABLE terminal accepts — never a
1025
+ hand-rolled ``f"{base}_{n}"``. ``avoid`` is the individual names of the
1026
+ knowledge base: a chain axiom names none, so it cannot capture one, but the
1027
+ invariant "no bound variable of an image shares a name with an individual"
1028
+ is kept without exception rather than only where it is needed.
1029
+ """
1030
+ names = [x, y, z][:length]
1031
+ if length > 3:
1032
+ fresh = _fresh_var_factory(x, avoid={y, z} | set(avoid))
1033
+ while len(names) < length:
1034
+ names.append(fresh())
1035
+ return [Variable(name) for name in names]
1036
+
1037
+
1038
+ def _chain_axiom(chain, super_role, x: str, y: str, z: str,
1039
+ avoid: Iterable[str] = ()) -> Node:
1040
+ """The FOL image of ``P1 ∘ … ∘ Pn ⊑ Q``: for ``n = 2``,
1041
+ ``∀x ∀y ∀z (P1(x, y) ∧ P2(y, z) → Q(x, z))``.
1042
+
1043
+ OWL 2 direct semantics: the axiom holds iff for every ``y0 … yn`` with
1044
+ ``<y_{i-1}, y_i> ∈ Pi^I`` we have ``<y0, yn> ∈ Q^I`` — so the body is the
1045
+ conjunction of the ``n`` step atoms (folded by :func:`_conjoin`, like every
1046
+ other conjunction this module builds) and the head links the first variable
1047
+ to the last.
1048
+ """
1049
+ variables = _chain_variables(len(chain) + 1, x, y, z, avoid)
1050
+ body = _conjoin([_role_atom(role, variables[i], variables[i + 1])
1051
+ for i, role in enumerate(chain)])
1052
+ return _forall(variables, Implies(body, _role_atom(super_role, variables[0],
1053
+ variables[-1])))
1054
+
1055
+
1056
+ def _rbox_axioms(tbox: TBox, x: str = "x", y: str = "y",
1057
+ z: str = "z", *, object_sort: bool = False,
1058
+ avoid: Optional[Iterable[str]] = None) -> List[SideAxiom]:
1059
+ """The role box of ``tbox`` as a list of :class:`SideAxiom` — one per role
1060
+ inclusion, then one per transitive role, then one per further role-box axiom
1061
+ in the order :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS` lists them.
1062
+ Every set-valued field is sorted, so the order is deterministic despite the
1063
+ fields being unordered sets.
1064
+
1065
+ The ONE place the role box becomes FOL: :func:`rbox_to_fol` conjoins what
1066
+ this returns and :func:`kb_to_fol` carries it as
1067
+ :attr:`KnowledgeBaseFOL.side_axioms`, so a new role-box axiom kind is
1068
+ rendered for both by appending one entry here (and, as every kind must,
1069
+ one row of :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`).
1070
+
1071
+ Every stored entry is re-validated first — the SECOND line of defence
1072
+ behind :meth:`~unicode_logic_kit.dl.tableau.TBox.add_role_inclusion` and
1073
+ friends, and the SAME validation
1074
+ (:func:`~unicode_logic_kit.dl.tableau._validate_role_box`) the tableau's
1075
+ shared guard runs, so the two routes cannot disagree about whether a ``TBox``
1076
+ assembled by hand, or mutated in place, is a question at all: it must fail
1077
+ LOUDLY rather than print an atom like ``('r', 's')(x, y)`` that looks like
1078
+ an axiom and is not.
1079
+
1080
+ The three prefix variables are the PREFERENCES ``x``, ``y``, ``z``: each one
1081
+ that is also the name of an individual of the knowledge base (``avoid`` —
1082
+ by default the individuals the TBox's own class expressions name;
1083
+ :func:`kb_to_fol` passes the whole knowledge base's) is renamed, so a domain
1084
+ axiom over ``∃r.{x}`` does not bind the individual ``x``
1085
+ (:func:`_binder_names`).
1086
+ """
1087
+ where = "dl.rbox_to_fol"
1088
+ _validate_role_box(tbox, where=where)
1089
+ avoid = (_kb_individual_names(tbox, None) if avoid is None else set(avoid))
1090
+ x, y, z = _binder_names((x, y, z), avoid)
1091
+ parts: List[SideAxiom] = []
1092
+ for sub_role, super_role in tbox.role_inclusions:
1093
+ vx, vy = Variable(x), Variable(y)
1094
+ # _role_atom, not Atom(...) by hand: it is the function that already
1095
+ # knows an InverseRole swaps the atom's argument order, and is what
1096
+ # every concept restriction in this module uses.
1097
+ parts.append(SideAxiom("SubObjectPropertyOf", "rbox",
1098
+ Quantifier("∀", vx, Quantifier("∀", vy,
1099
+ Implies(_role_atom(sub_role, vx, vy),
1100
+ _role_atom(super_role, vx, vy))))))
1101
+ for role in sorted(tbox.transitive_roles):
1102
+ vx, vy, vz = Variable(x), Variable(y), Variable(z)
1103
+ parts.append(SideAxiom("TransitiveObjectProperty", "rbox",
1104
+ Quantifier("∀", vx, Quantifier("∀", vy, Quantifier("∀", vz,
1105
+ Implies(_FAnd(Atom(role, (vx, vy)), Atom(role, (vy, vz))),
1106
+ Atom(role, (vx, vz))))))))
1107
+ for left, right in tbox.disjoint_role_pairs:
1108
+ vx, vy = Variable(x), Variable(y)
1109
+ parts.append(SideAxiom("DisjointObjectProperties", "rbox",
1110
+ Quantifier("∀", vx, Quantifier("∀", vy,
1111
+ _FNot(_FAnd(Atom(left, (vx, vy)), Atom(right, (vx, vy))))))))
1112
+ for role in sorted(tbox.asymmetric_roles):
1113
+ vx, vy = Variable(x), Variable(y)
1114
+ parts.append(SideAxiom("AsymmetricObjectProperty", "rbox",
1115
+ Quantifier("∀", vx, Quantifier("∀", vy,
1116
+ Implies(Atom(role, (vx, vy)), _FNot(Atom(role, (vy, vx))))))))
1117
+ for role in sorted(tbox.irreflexive_roles):
1118
+ vx = Variable(x)
1119
+ parts.append(SideAxiom("IrreflexiveObjectProperty", "rbox",
1120
+ Quantifier("∀", vx, _FNot(Atom(role, (vx, vx))))))
1121
+ for role in sorted(tbox.functional_roles):
1122
+ vx, vy, vz = Variable(x), Variable(y), Variable(z)
1123
+ parts.append(SideAxiom("FunctionalObjectProperty", "rbox",
1124
+ Quantifier("∀", vx, Quantifier("∀", vy, Quantifier("∀", vz,
1125
+ Implies(_FAnd(Atom(role, (vx, vy)), Atom(role, (vx, vz))),
1126
+ Atom("=", (vy, vz))))))))
1127
+ for p, q in tbox.inverse_role_pairs:
1128
+ vx, vy = Variable(x), Variable(y)
1129
+ parts.append(SideAxiom("InverseObjectProperties", "rbox",
1130
+ Quantifier("∀", vx, Quantifier("∀", vy,
1131
+ Iff(Atom(p, (vx, vy)), Atom(q, (vy, vx)))))))
1132
+ for role in sorted(tbox.symmetric_roles):
1133
+ vx, vy = Variable(x), Variable(y)
1134
+ parts.append(SideAxiom("SymmetricObjectProperty", "rbox",
1135
+ Quantifier("∀", vx, Quantifier("∀", vy,
1136
+ Implies(Atom(role, (vx, vy)), Atom(role, (vy, vx)))))))
1137
+ for role in sorted(tbox.reflexive_roles):
1138
+ vx = Variable(x)
1139
+ # Reflexive(r) is "every INDIVIDUAL is r-related to itself". The one
1140
+ # role-box axiom with an unguarded universal over a single variable, so
1141
+ # in the two-sorted image it needs the object-domain guard (a data
1142
+ # value is not r-related to itself, and the typing of r forbids it).
1143
+ reflexive: Node = Atom(role, (vx, vx))
1144
+ if object_sort:
1145
+ reflexive = Implies(Atom(OWL_THING, (vx,)), reflexive)
1146
+ parts.append(SideAxiom("ReflexiveObjectProperty", "rbox",
1147
+ Quantifier("∀", vx, reflexive)))
1148
+ for role in sorted(tbox.inverse_functional_roles):
1149
+ vx, vy, vz = Variable(x), Variable(y), Variable(z)
1150
+ parts.append(SideAxiom("InverseFunctionalObjectProperty", "rbox",
1151
+ Quantifier("∀", vx, Quantifier("∀", vy, Quantifier("∀", vz,
1152
+ Implies(_FAnd(Atom(role, (vy, vx)), Atom(role, (vz, vx))),
1153
+ Atom("=", (vy, vz))))))))
1154
+ for chain, super_role in tbox.role_chains:
1155
+ parts.append(SideAxiom("ObjectPropertyChain", "rbox",
1156
+ _chain_axiom(chain, super_role, x, y, z, avoid)))
1157
+ # The two role-box kinds whose axiom carries a CLASS EXPRESSION, so the
1158
+ # filler goes through the full _translate (12 of the 110 OEO domain axioms
1159
+ # and 10 of the 108 range axioms have a non-atomic one) at the FIRST
1160
+ # variable for a domain axiom and the SECOND for a range axiom -- that
1161
+ # choice of variable is the whole difference between them.
1162
+ for kind, field_name, position in (("ObjectPropertyDomain", "role_domains", 0),
1163
+ ("ObjectPropertyRange", "role_ranges", 1)):
1164
+ for role, concept in getattr(tbox, field_name):
1165
+ vx, vy = Variable(x), Variable(y)
1166
+ # The avoid set must carry the prefix variables AND every
1167
+ # individual of the knowledge base: a minted x0 that collided with
1168
+ # one of them would capture it (see _fresh_var_factory). The prefix
1169
+ # variables themselves were already renamed away from the
1170
+ # individuals above, which is what stops the FIXED letters x and y
1171
+ # -- not only the minted ones -- from binding a filler's individual.
1172
+ fresh = _fresh_var_factory(
1173
+ x, avoid={x, y, z} | avoid | _individual_names(concept))
1174
+ parts.append(SideAxiom(kind, "rbox",
1175
+ Quantifier("∀", vx, Quantifier("∀", vy,
1176
+ Implies(Atom(role, (vx, vy)),
1177
+ _translate(concept, (vx, vy)[position], fresh))))))
1178
+ return parts
1179
+
1180
+
1181
+ def rbox_to_fol(tbox: TBox, x: str = "x", y: str = "y", z: str = "z") -> Node:
1182
+ """Render every role-box axiom in ``tbox`` as its FOL image (see "RBoxes" in
1183
+ the module docstring) and conjoin them. Each image is the OWL 2
1184
+ direct-semantics sentence for that axiom:
1185
+
1186
+ - ``r ⊑ s`` ↦ ``∀x ∀y (r(x, y) → s(x, y))``
1187
+ - ``r ⊑ s⁻`` ↦ ``∀x ∀y (r(x, y) → s(y, x))``
1188
+ - ``Trans(r)`` ↦ ``∀x ∀y ∀z (r(x, y) ∧ r(y, z) → r(x, z))``
1189
+ - ``Disj(p, q)`` ↦ ``∀x ∀y ¬(p(x, y) ∧ q(x, y))``
1190
+ - ``Asym(r)`` ↦ ``∀x ∀y (r(x, y) → ¬r(y, x))``
1191
+ - ``Irr(r)`` ↦ ``∀x ¬r(x, x)`` (ONE variable)
1192
+ - ``Func(r)`` ↦ ``∀x ∀y ∀z (r(x, y) ∧ r(x, z) → y = z)``
1193
+ - ``Inv(p, q)`` ↦ ``∀x ∀y (p(x, y) ↔ q(y, x))`` (ONE ↔: the
1194
+ axiom is an EQUALITY of relations, not two inclusions)
1195
+ - ``Sym(r)`` ↦ ``∀x ∀y (r(x, y) → r(y, x))``
1196
+ - ``Refl(r)`` ↦ ``∀x r(x, x)``
1197
+ - ``InvFunc(r)`` ↦ ``∀x ∀y ∀z (r(y, x) ∧ r(z, x) → y = z)``
1198
+ (the argument order is the whole content of the axiom)
1199
+ - ``p1 ∘ p2 ⊑ q`` ↦ ``∀x ∀y ∀z (p1(x, y) ∧ p2(y, z) → q(x, z))``
1200
+ - ``Dom(r, C)`` ↦ ``∀x ∀y (r(x, y) → π(C, x))``
1201
+ - ``Rng(r, C)`` ↦ ``∀x ∀y (r(x, y) → π(C, y))`` (the filler
1202
+ at the SECOND variable — the only difference from the domain axiom)
1203
+
1204
+ An ``EquivalentObjectProperties`` axiom has no entry of its own because
1205
+ :meth:`~unicode_logic_kit.dl.tableau.TBox.add_equivalent_roles` stores it as
1206
+ the role inclusions it abbreviates, so it renders as those.
1207
+
1208
+ This is the ROLE box only: the data box's images are
1209
+ :func:`databox_to_fol`'s, so a TBox carrying data axioms alone renders as
1210
+ the tautology here (the role box IS empty). :func:`kb_to_fol` renders both.
1211
+
1212
+ Set-valued fields are sorted before rendering so the output (and hence any
1213
+ string comparison of it) is deterministic despite them being unordered
1214
+ ``set``s. A role box with no axioms at all — including every plain ALC
1215
+ ``TBox`` predating this function — renders as a tautology, matching
1216
+ :func:`tbox_to_fol`'s own empty-TBox convention.
1217
+
1218
+ This is a SIDE axiom of every question asked relative to ``tbox``: pass it
1219
+ (or, better, the per-axiom tuple :attr:`KnowledgeBaseFOL.axioms`) as a
1220
+ premise of ``api.prove`` — :func:`kb_to_fol` does that bookkeeping.
1221
+
1222
+ ``x``, ``y`` and ``z`` are the PREFERRED names of the bound variables: one
1223
+ that is also the name of an individual a domain or range filler names
1224
+ (``∃r.{x}``) is renamed to the first free ``letter + digits`` instead of
1225
+ binding that individual (see :func:`_binder_names`).
1226
+
1227
+ The images of the boxes are built one call at a time, so a name used as two
1228
+ kinds of thing ACROSS boxes (an object property here, a data property in the
1229
+ data box) cannot be seen by any one of them: the refusal of such a clash is
1230
+ per call, over what the call is given. :func:`kb_to_fol` is the call that
1231
+ sees everything, and refuses it; :func:`check_kb_names` runs the same check
1232
+ over the boxes of images rendered separately. A class, an object property and
1233
+ an individual of one name are three symbols here (see :func:`kb_to_fol`).
1234
+
1235
+ Raises:
1236
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
1237
+ a role-box entry does not hold a usable role —
1238
+ see :class:`~unicode_logic_kit.dl.tableau.RoleExpressionError`. The
1239
+ builders refuse the same values, so this fires only for a ``TBox``
1240
+ assembled or mutated by hand — and it is the SAME validation the
1241
+ tableau's shared guard runs, so both routes refuse it.
1242
+ """
1243
+ return _conjoin([a.formula for a in _rbox_axioms(tbox, x, y, z)])
1244
+
1245
+
1246
+ def _databox_axioms(tbox: TBox, x: str = "x", v: str = "v",
1247
+ w: str = "w", *,
1248
+ avoid: Optional[Iterable[str]] = None) -> List[SideAxiom]:
1249
+ """The data box of ``tbox`` as :class:`SideAxiom` s of group ``"data"`` — in
1250
+ the order :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS` lists the kinds.
1251
+ Every OWL 2 direct-semantics sentence below is derived in
1252
+ :func:`databox_to_fol`'s docstring. Property names are re-validated here, the
1253
+ second line of defence behind the builders, by the SAME validation
1254
+ (:func:`~unicode_logic_kit.dl.tableau._validate_data_box`) the tableau's
1255
+ shared guard runs, and the prefix variables are renamed away from the
1256
+ knowledge base's individuals exactly as :func:`_rbox_axioms` does — a data
1257
+ property domain over ``∃r.{v}`` must not bind the individual ``v``.
1258
+ """
1259
+ where = "dl.databox_to_fol"
1260
+ _validate_data_box(tbox, where=where)
1261
+ avoid = (_kb_individual_names(tbox, None) if avoid is None else set(avoid))
1262
+ x, v, w = _binder_names((x, v, w), avoid)
1263
+ parts: List[SideAxiom] = []
1264
+
1265
+ def closed(variables, body) -> Node:
1266
+ return _forall([Variable(name) for name in variables], body)
1267
+
1268
+ for sub_prop, super_prop in tbox.data_property_inclusions:
1269
+ vx, vv = Variable(x), Variable(v)
1270
+ parts.append(SideAxiom("SubDataPropertyOf", "data", closed((x, v), Implies(
1271
+ Atom(sub_prop, (vx, vv)), Atom(super_prop, (vx, vv))))))
1272
+ for left, right in tbox.disjoint_data_property_pairs:
1273
+ vx, vv = Variable(x), Variable(v)
1274
+ parts.append(SideAxiom("DisjointDataProperties", "data", closed((x, v), _FNot(
1275
+ _FAnd(Atom(left, (vx, vv)), Atom(right, (vx, vv)))))))
1276
+ for prop in sorted(tbox.functional_data_properties):
1277
+ vx, vv, vw = Variable(x), Variable(v), Variable(w)
1278
+ parts.append(SideAxiom("FunctionalDataProperty", "data", closed((x, v, w), Implies(
1279
+ _FAnd(Atom(prop, (vx, vv)), Atom(prop, (vx, vw))), Atom("=", (vv, vw))))))
1280
+ for prop, concept in tbox.data_property_domains:
1281
+ vx, vv = Variable(x), Variable(v)
1282
+ # The filler is a full class expression (a union in two OEO axioms), so
1283
+ # it goes through _translate at the FIRST variable; the avoid set
1284
+ # carries the prefix variables (already renamed away from every
1285
+ # individual) and every individual of the knowledge base.
1286
+ fresh = _fresh_var_factory(
1287
+ x, avoid={x, v, w} | avoid | _individual_names(concept))
1288
+ parts.append(SideAxiom("DataPropertyDomain", "data", closed((x, v), Implies(
1289
+ Atom(prop, (vx, vv)), _translate(concept, vx, fresh)))))
1290
+ for prop, datarange in tbox.data_property_ranges:
1291
+ vx, vv = Variable(x), Variable(v)
1292
+ parts.append(SideAxiom("DataPropertyRange", "data", closed((x, v), Implies(
1293
+ Atom(prop, (vx, vv)), datarange_to_fol(datarange, vv)))))
1294
+ for name, datarange in tbox.datatype_definitions:
1295
+ vv = Variable(v)
1296
+ parts.append(SideAxiom("DatatypeDefinition", "data", closed((v,), Iff(
1297
+ Atom(name, (vv,)), datarange_to_fol(datarange, vv)))))
1298
+ return parts
1299
+
1300
+
1301
+ def databox_to_fol(tbox: TBox, x: str = "x", v: str = "v", w: str = "w") -> Node:
1302
+ """Render every data-box axiom in ``tbox`` as its FOL image and conjoin them
1303
+ (see "The data layer" in the module docstring). Each image is the OWL 2
1304
+ direct-semantics sentence for that axiom, the datatype read as a unary
1305
+ predicate over the one domain:
1306
+
1307
+ - ``SubDataPropertyOf(d e)`` ↦ ``∀x ∀v (d(x, v) → e(x, v))``
1308
+ - ``DisjointDataProperties(d e)`` ↦ ``∀x ∀v ¬(d(x, v) ∧ e(x, v))``
1309
+ - ``FunctionalDataProperty(d)`` ↦ ``∀x ∀v ∀w (d(x, v) ∧ d(x, w) → v = w)``
1310
+ - ``DataPropertyDomain(d C)`` ↦ ``∀x ∀v (d(x, v) → π(C, x))``
1311
+ - ``DataPropertyRange(d DR)`` ↦ ``∀x ∀v (d(x, v) → δ(DR, v))``
1312
+ - ``DatatypeDefinition(D DR)`` ↦ ``∀v (D(v) ↔ δ(DR, v))`` — a DEFINITION,
1313
+ so a biconditional and not a pair of inclusions read one way.
1314
+
1315
+ ``EquivalentDataProperties`` has no entry of its own: it is stored as the
1316
+ inclusions it abbreviates. A data box with nothing in it is a tautology,
1317
+ like :func:`rbox_to_fol`'s empty role box. This is a SIDE axiom of every
1318
+ question asked relative to ``tbox`` — :func:`kb_to_fol` does the
1319
+ bookkeeping, and adds the two-sorted axioms of :func:`data_sort_axioms`.
1320
+
1321
+ The refusal of a name used as two kinds of thing is PER CALL: this function
1322
+ sees the vocabulary of ``tbox``, not of the ABox or of a role box rendered
1323
+ separately, so a clash across boxes built one call at a time is invisible to
1324
+ it. :func:`kb_to_fol` is the call that sees everything, and
1325
+ :func:`check_kb_names` runs the same check over boxes rendered separately.
1326
+
1327
+ Raises:
1328
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
1329
+ a data-box entry does not hold a usable property
1330
+ name (only reachable for a ``TBox`` assembled by hand).
1331
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
1332
+ a literal in a data range has no term, or a
1333
+ name of ``tbox`` is used both as an object property and a data
1334
+ property or both as a class and a datatype (see :func:`kb_to_fol`).
1335
+ """
1336
+ where = "dl.databox_to_fol"
1337
+ _validate_data_box(tbox, where=where)
1338
+ # The whole TBox's vocabulary decides whether a predicate of this image is
1339
+ # also another kind's (``P`` an object role in a GCI, a data property here).
1340
+ vocabulary = _collect_vocabulary(tbox, None)
1341
+ _check_name_punning(vocabulary, where)
1342
+ return _conjoin([a.formula for a in _databox_axioms(
1343
+ tbox, x, v, w, avoid=vocabulary.individuals)])
1344
+
1345
+
1346
+ def abox_to_fol(abox: ABox) -> Node:
1347
+ """Render every assertion in ``abox`` as a FOL formula and conjoin them.
1348
+
1349
+ A concept assertion ``a : C`` becomes ``π(C, a)`` with the individual
1350
+ ``a`` translated as a :class:`~unicode_logic_kit.fol.nodes.Constant` (not a
1351
+ Variable — see "GCIs, TBoxes, ABoxes" above for why this matters). A role
1352
+ assertion ``(a, b) : r`` becomes ``r(a, b)``. A distinctness assertion
1353
+ ``a ≠ b`` (see :meth:`~unicode_logic_kit.dl.tableau.ABox.assert_distinct` and
1354
+ "Qualified number restrictions" in :mod:`unicode_logic_kit.dl.tableau`'s module
1355
+ docstring — this reasoner has no unique name assumption, so this is the ONLY
1356
+ thing that ever forces two individuals apart) becomes the FOL atom ``a ≠ b``
1357
+ directly — no translation needed, ``Atom`` already renders it natively for
1358
+ every backend. A same-individual assertion ``a = b``
1359
+ (:meth:`~unicode_logic_kit.dl.tableau.ABox.assert_same`) is its exact mirror,
1360
+ and a negative role assertion
1361
+ (:meth:`~unicode_logic_kit.dl.tableau.ABox.assert_negative_role`) becomes the
1362
+ ground literal ``¬r(a, b)``. A data property assertion
1363
+ (:meth:`~unicode_logic_kit.dl.tableau.ABox.assert_data`) is the ground atom
1364
+ ``d(a, t)`` for the literal's term ``t`` (``HasNumber(alice, 400)``), and a
1365
+ negative one its negation. An ABox with no assertions at all renders as a
1366
+ tautology.
1367
+
1368
+ The conjuncts come in assertion-kind order — concept assertions, role
1369
+ assertions, distinctness, sameness, negative role assertions, data
1370
+ assertions, negative data assertions — each kind in the order it was
1371
+ asserted, folded by :func:`_conjoin`.
1372
+
1373
+ The refusal of a name used as two kinds of thing is PER CALL, over the ABox
1374
+ alone: a name that is a role here and a data property in a TBox rendered
1375
+ separately is invisible to this function. :func:`kb_to_fol` is the call that
1376
+ sees everything, and :func:`check_kb_names` runs the same check over boxes
1377
+ rendered separately.
1378
+
1379
+ The result is the ABox ALONE. Whether the assertions are consistent, or
1380
+ entail ``a : C``, *relative to a TBox* (the usual question — GCIs apply to
1381
+ every named individual, and a role box changes which edges count) is not
1382
+ answered by this formula: pass ``kb_to_fol(tbox, abox).premises`` as the
1383
+ premises of the query instead (see :func:`kb_to_fol`). An ABox-only question
1384
+ — "do these bare assertions clash?" — is exactly what this renders.
1385
+
1386
+ ``abox_to_fol(ABox().assert_concept(a, C))`` is also the way to spell the
1387
+ GOAL ``π(C, a)`` of an instance-checking query with the individual as a
1388
+ constant.
1389
+
1390
+ Raises:
1391
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
1392
+ an assertion's property is an OWL 2 built-in name
1393
+ (``owl:topObjectProperty`` …), ``=``/``≠``, an inverse role or no
1394
+ name at all — see
1395
+ :func:`~unicode_logic_kit.dl.tableau._reject_abox_roles`. Printed as
1396
+ the uninterpreted predicate of that name it would be a different
1397
+ assertion (the EMPTY property relates no pair, so ``(a, b) :
1398
+ owl:bottomObjectProperty`` is inconsistent, ``(a, b) : p`` is not).
1399
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
1400
+ one name is both the property of a role
1401
+ assertion and the property of a data assertion, or both a class
1402
+ of a concept assertion and a datatype of a literal (see
1403
+ :func:`kb_to_fol`).
1404
+ """
1405
+ vocabulary = _collect_vocabulary(None, abox)
1406
+ _check_name_punning(vocabulary, "dl.abox_to_fol")
1407
+ return _abox_image(abox, vocabulary.individuals)
1408
+
1409
+
1410
+ def _abox_image(abox: ABox, avoid: Iterable[str]) -> Node:
1411
+ """:func:`abox_to_fol` with ``avoid`` — the individual names of the whole
1412
+ knowledge base — added to the names the minted variables must avoid."""
1413
+ _reject_abox_roles(abox, where="dl.abox_to_fol")
1414
+ avoid = set(avoid)
1415
+ parts: List[Node] = []
1416
+ for individual, concept in abox.concept_assertions:
1417
+ term = Constant(individual)
1418
+ parts.append(_translate(
1419
+ concept, term,
1420
+ _fresh_var_factory(individual, avoid | _individual_names(concept))))
1421
+ for a, b, role in abox.role_assertions:
1422
+ parts.append(Atom(role, (Constant(a), Constant(b))))
1423
+ for a, b in abox.distinct_assertions:
1424
+ parts.append(Atom("≠", (Constant(a), Constant(b))))
1425
+ # SameIndividual(a b) holds iff a^I = b^I: the exact mirror of the
1426
+ # distinctness atom above, and ``=`` is native in every backend (Z3 ==,
1427
+ # Prover9 =, TPTP =) exactly as ``≠`` is.
1428
+ for a, b in abox.same_assertions:
1429
+ parts.append(Atom("=", (Constant(a), Constant(b))))
1430
+ # NegativeObjectPropertyAssertion(r a b) holds iff <a^I, b^I> ∉ r^I: the
1431
+ # negated GROUND literal, not a concept assertion about a nominal.
1432
+ for a, b, role in abox.negative_role_assertions:
1433
+ parts.append(_FNot(Atom(role, (Constant(a), Constant(b)))))
1434
+ # DataPropertyAssertion(d a lt) holds iff <a^I, lt^LT> ∈ d^DP: the ground
1435
+ # atom, with the individual as a Constant (as for a role assertion) and the
1436
+ # literal as the TERM of its data value. Nothing is quantified, so no
1437
+ # variable is minted. The negative form is the negated ground literal.
1438
+ for individual, prop, value in abox.data_assertions:
1439
+ parts.append(Atom(prop, (Constant(individual), value.to_term())))
1440
+ for individual, prop, value in abox.negative_data_assertions:
1441
+ parts.append(_FNot(Atom(prop, (Constant(individual), value.to_term()))))
1442
+ return _conjoin(parts)
1443
+
1444
+
1445
+ # --------------------------------------------------------------------------- #
1446
+ # A whole knowledge base: the TBox, the ABox and the role box, kept apart.
1447
+ # --------------------------------------------------------------------------- #
1448
+
1449
+ @dataclass(frozen=True)
1450
+ class KnowledgeBaseFOL:
1451
+ """The FOL image of a knowledge base ``(tbox, abox)`` — see :func:`kb_to_fol`.
1452
+
1453
+ Fields:
1454
+
1455
+ * ``formula`` — the knowledge base itself: the conjunction of the concept-
1456
+ inclusion image and the ABox image (a half with nothing in it is left out,
1457
+ so a TBox-only knowledge base has ``formula == tbox``; with nothing at all
1458
+ it is the usual equality tautology).
1459
+ * ``side_axioms`` — the SIDE axioms as :class:`SideAxiom` values, each
1460
+ carrying the OWL ``kind`` it came from and its ``group``: the role-box
1461
+ image (one per role inclusion, then one per further role-box axiom in the
1462
+ order of ``dl.tableau._AXIOM_KINDS``, set-valued fields sorted), the
1463
+ data-box image and, for a knowledge base with a data layer, the sort
1464
+ axioms; empty when the TBox has none of them. They are premises,
1465
+ NEVER conjoined into ``formula``. ``axioms`` is the bare-formula view of
1466
+ this ONE field, not a second list that could drift out of step with it.
1467
+ * ``formulas`` — the per-axiom list behind ``formula``: the concept-
1468
+ inclusion image and the ABox image, each present only when it has
1469
+ content, so ``formula == _conjoin(formulas)``. A caller with an
1470
+ ontology-sized knowledge base can pass ``(*kb.formulas, *kb.axioms)``
1471
+ as premises instead of the single conjunction; the two are
1472
+ entailment-equivalent (a conjunction of premises and a list of premises
1473
+ have the same models).
1474
+ * ``tbox`` / ``abox`` — the two halves of ``formula`` on their own
1475
+ (:func:`tbox_to_fol` with ``concept_inclusions_only=True``, and
1476
+ :func:`abox_to_fol`), each a tautology when empty. A question that is about
1477
+ the terminology alone (subsumption, concept satisfiability) needs ``tbox``
1478
+ but not ``abox``.
1479
+ * ``individuals`` — the sorted names of the individuals the ABox mentions
1480
+ (concept assertions, role-assertion endpoints, identity, distinctness and
1481
+ negative role assertions — the ``individual_positions`` column of
1482
+ :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`, so an assertion kind
1483
+ added later is counted without touching this scan); each is a FOL
1484
+ :class:`~unicode_logic_kit.fol.nodes.Constant` of that name in
1485
+ ``abox``/``formula``, which is what a goal about an individual has to use.
1486
+ An individual named in an ABox assertion is an individual of the ABox
1487
+ wherever in the assertion it stands: ``a : ∃r.{b}`` (a value restriction
1488
+ or a nominal, at any depth of the asserted class expression) lists ``b``
1489
+ as well as ``a``. The in-house tableau refuses those concepts, so what
1490
+ this changes is this field and the sweeps of the external routes.
1491
+
1492
+ The ABox is the SCOPE, not merely where the scan happens to look: a
1493
+ caller's individual can also reach the TBox half, as the filler of a
1494
+ nominal (``C ⊑ {a}`` → ``∀x (C(x) → x = a)``) or of a value restriction
1495
+ (``C ⊑ ∃r.{a}`` → ``∀x (C(x) → r(x, a))``), and such a name is a
1496
+ ``Constant`` in ``formula`` that this field does NOT report. That is
1497
+ deliberate: ``individuals`` is the list of things the knowledge base
1498
+ makes ASSERTIONS about, which is what ``instance_retrieval`` and
1499
+ ``realize_all`` enumerate, and the two routes agree only because all
1500
+ three read the same scan. A caller that needs every name in term
1501
+ position should walk the image — ``node.walk()`` yields terms too.
1502
+
1503
+ * ``separation`` — which theory of the data layer ``side_axioms`` carries:
1504
+ ``"two-sorted"`` (the object/data separation, the typing of every
1505
+ property and individual, the datatype lattice, literal distinctness — and
1506
+ every GCI of ``formula``/``tbox`` relativised to the object domain),
1507
+ ``"data-lattice"`` (the datatype facts only, no object/data separation,
1508
+ GCIs as written) or ``"none"`` (the data-box images alone). ``"none"`` for
1509
+ every knowledge base without a data layer, whatever was asked for, since
1510
+ nothing is then added. A GOAL asked of a ``"two-sorted"`` knowledge base
1511
+ must be relativised too — :meth:`subsumption_goal`,
1512
+ :meth:`unsatisfiability_goal` and :meth:`instance_goal` build it so.
1513
+ * ``data_layer`` — whether the knowledge base, together with the ``query=``
1514
+ concepts it was built for, has a data layer at all: a data property, a
1515
+ datatype or a literal occurs in it. :attr:`refutation_is_decisive` is its
1516
+ negation.
1517
+
1518
+ ``premises`` and ``tbox_premises`` are the two premise lists the common
1519
+ questions need, and the three ``*_goal`` methods build the matching goal.
1520
+ How to use them with :func:`unicode_logic_kit.api.prove` is in
1521
+ :func:`kb_to_fol`.
1522
+ """
1523
+
1524
+ formula: Node
1525
+ side_axioms: Tuple[SideAxiom, ...]
1526
+ tbox: Node
1527
+ abox: Node
1528
+ individuals: Tuple[str, ...] = ()
1529
+ formulas: Tuple[Node, ...] = ()
1530
+ separation: str = "none"
1531
+ data_layer: bool = False
1532
+ # What the side axioms were derived from, read by the three goal methods to
1533
+ # tell a concept they cover from one they do not. Internal: not part of the
1534
+ # logical content, so it neither prints nor takes part in ==.
1535
+ _coverage: Optional["_Coverage"] = field(default=None, repr=False, compare=False)
1536
+
1537
+ @property
1538
+ def axioms(self) -> Tuple[Node, ...]:
1539
+ """``side_axioms``' formulas alone, in the same order — the premise list
1540
+ to hand a prover.
1541
+
1542
+ DERIVED, not stored: one source of truth means no package can append to
1543
+ one list and forget the other.
1544
+ """
1545
+ return tuple(a.formula for a in self.side_axioms)
1546
+
1547
+ @property
1548
+ def rbox_axioms(self) -> Tuple[Node, ...]:
1549
+ """The ``group == "rbox"`` side axioms' formulas — the role-box image."""
1550
+ return tuple(a.formula for a in self.side_axioms if a.group == "rbox")
1551
+
1552
+ @property
1553
+ def data_axioms(self) -> Tuple[Node, ...]:
1554
+ """The side axioms' formulas belonging to the data layer (``group`` in
1555
+ ``"data"`` — the images of the data-box axioms —, ``"sort"`` — the
1556
+ object/data separation and the typing — and ``"datatype"`` — the
1557
+ datatype lattice, literal typing and literal distinctness).
1558
+
1559
+ Empty for a knowledge base without a data layer. A caller can ask "which
1560
+ of these premises belong to the data layer?" of any bundle, without
1561
+ knowing which entry point built it.
1562
+ """
1563
+ return tuple(a.formula for a in self.side_axioms
1564
+ if a.group in ("data", "sort", "datatype"))
1565
+
1566
+ def axioms_of_kind(self, kind: str) -> Tuple[Node, ...]:
1567
+ """The side axioms' formulas whose OWL ``kind`` is ``kind``, in order.
1568
+
1569
+ ``kind`` is an OWL 2 keyword as :data:`SideAxiom.kind` spells it, e.g.
1570
+ ``"TransitiveObjectProperty"``. An unknown kind gives ``()`` rather
1571
+ than raising: the question "did this knowledge base's image carry any
1572
+ axiom of kind K?" has a correct answer for every K, and "none" is it.
1573
+ """
1574
+ return tuple(a.formula for a in self.side_axioms if a.kind == kind)
1575
+
1576
+ @property
1577
+ def premises(self) -> Tuple[Node, ...]:
1578
+ """``(formula, *axioms)`` — "the whole knowledge base holds": the premises
1579
+ of a question relative to the TBox AND the ABox (instance checking).
1580
+
1581
+ For an ontology-sized knowledge base ``(*formulas, *axioms)`` says the
1582
+ same thing one axiom at a time; see ``formulas``."""
1583
+ return (self.formula, *self.axioms)
1584
+
1585
+ @property
1586
+ def tbox_premises(self) -> Tuple[Node, ...]:
1587
+ """``(tbox, *axioms)`` — "the terminology holds", ABox left out: the
1588
+ premises of a question about concepts relative to the TBox
1589
+ (subsumption, concept satisfiability)."""
1590
+ return (self.tbox, *self.axioms)
1591
+
1592
+ @property
1593
+ def refutation_is_decisive(self) -> bool:
1594
+ """Whether a ``"refuted"`` verdict of :func:`unicode_logic_kit.api.prove`
1595
+ over this bundle is an answer about OWL 2, and not only about the image:
1596
+ ``True`` exactly when the knowledge base (with the ``query=`` concepts it
1597
+ was built for) has NO data layer.
1598
+
1599
+ What a caller may conclude from each status, for a goal built by
1600
+ :meth:`subsumption_goal`, :meth:`unsatisfiability_goal` or
1601
+ :meth:`instance_goal` and the premises the method's docstring names:
1602
+
1603
+ * ``"proved"`` — YES, for every goal these methods hand out. Under the
1604
+ two-sorted separation the image is SOUND for the data layer: every
1605
+ OWL 2 interpretation of the knowledge base expands to a model of the
1606
+ image, so what the image entails the knowledge base entails. That
1607
+ is NOT so for a data-layer bundle built with
1608
+ ``separation="data-lattice"`` or ``"none"``: its inclusions also range
1609
+ over data values, which makes its premises stronger than OWL 2's, and
1610
+ the methods refuse such a bundle by name instead of building a goal.
1611
+ * ``"refuted"`` — NO when this property is ``True`` (the image of an
1612
+ object-only knowledge base is faithful: a countermodel of the image
1613
+ is one of the knowledge base). When it is ``False`` it means only
1614
+ that the IMAGE has a countermodel, and the image is deliberately not
1615
+ COMPLETE, so the countermodel may be one OWL 2 excludes. Three things
1616
+ it does not state: an ordering facet is an uninterpreted comparison
1617
+ (a range ``xsd:integer[>= 10]`` with ``HasN(a, 5)`` is OWL-inconsistent
1618
+ and ``refuted`` here; ``HasN(a, 15)`` entails ``a : ∃HasN.xsd:integer[>= 3]``
1619
+ in OWL and does not here); a literal is typed only by the datatype it
1620
+ was written with (``d(a, "1.0"^^xsd:decimal)`` entails
1621
+ ``a : ∃d.xsd:integer`` in OWL and does not here, and
1622
+ ``d(a, "5"^^xsd:int)`` entails ``a : ∃d.xsd:nonNegativeInteger``); and
1623
+ the size of a value space is not stated. Decide facet arithmetic over
1624
+ the data ranges alone with ``atp.z3_arith.is_valid_arith``.
1625
+ * ``"unknown"`` — no answer within the budget, in either direction.
1626
+ """
1627
+ return not self.data_layer
1628
+
1629
+ def _covers(self, method: str, *concepts: Concept) -> "_Coverage":
1630
+ """The coverage of this bundle, after checking that it covers
1631
+ ``concepts``; raises by name otherwise (see :func:`_check_goal_concepts`)."""
1632
+ coverage = self._coverage
1633
+ if coverage is None:
1634
+ raise ValueError(
1635
+ f"KnowledgeBaseFOL.{method}: this bundle was not built by "
1636
+ f"dl.kb_to_fol, so it does not know which vocabulary its side "
1637
+ f"axioms cover and cannot say whether a goal is the question "
1638
+ f"it asks. Build it with dl.kb_to_fol(tbox, abox, query=[...]).")
1639
+ if self.data_layer and self.separation != "two-sorted":
1640
+ raise UnsupportedDatatypeError(
1641
+ f"KnowledgeBaseFOL.{method}: this bundle has a data layer and was "
1642
+ f"built with separation={self.separation!r}. Its inclusions are then "
1643
+ f"not restricted to the object domain, so its premises are STRONGER "
1644
+ f"than OWL 2's — '⊤ ⊑ {{a}}' also ranges over data values there — "
1645
+ f"and an OWL-consistent knowledge base can have an inconsistent "
1646
+ f"image, from which every goal is proved. A proof over this bundle "
1647
+ f"does not transfer to OWL 2. Build it with "
1648
+ f"separation='two-sorted' (the default).")
1649
+ _check_goal_concepts(coverage, self.separation, self.data_layer,
1650
+ concepts, where=f"KnowledgeBaseFOL.{method}")
1651
+ return coverage
1652
+
1653
+ def subsumption_goal(self, sub: Concept, sup: Concept) -> Node:
1654
+ """The goal ``sub ⊑ sup``, relativised the way this bundle needs it
1655
+ (to the object domain when ``separation == "two-sorted"``, as written
1656
+ otherwise): ``api.prove(kb.subsumption_goal(sub, sup),
1657
+ kb.tbox_premises)`` is ``"proved"`` ⇒ ``sub ⊑ sup`` follows from the
1658
+ terminology (what ``dl.subsumes(sub, sup, tbox)`` asks, over the
1659
+ constructs the tableau does not refuse). With ``kb.premises`` instead
1660
+ the ABox is a premise as well. ``"refuted"`` is the answer NO only
1661
+ when :attr:`refutation_is_decisive`.
1662
+
1663
+ Raises:
1664
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
1665
+ a concept names a data property, a
1666
+ datatype, a literal (or, on a two-sorted bundle, an object
1667
+ property) the bundle's side axioms do not cover, so the
1668
+ question would be asked of a weaker theory than the one OWL 2
1669
+ describes — pass ``query=[sub, sup]`` to :func:`kb_to_fol`; or
1670
+ a name is used as two kinds of thing at once.
1671
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
1672
+ a restriction's role is an OWL 2 built-in
1673
+ property name.
1674
+ """
1675
+ coverage = self._covers("subsumption_goal", sub, sup)
1676
+ return _subsumption_image(sub, sup, coverage.var,
1677
+ self.separation == "two-sorted", coverage.avoid)
1678
+
1679
+ def unsatisfiability_goal(self, concept: Concept) -> Node:
1680
+ """The goal "no object is in ``concept``" — on a two-sorted bundle
1681
+ ``¬∃x (OwlThing(x) ∧ π(concept, x))``, otherwise ``¬∃x π(concept, x)``:
1682
+ ``api.prove(kb.unsatisfiability_goal(concept), kb.tbox_premises)`` is
1683
+ ``"proved"`` ⇒ the concept is UNSATISFIABLE with respect to the
1684
+ terminology (``dl.concept_satisfiable(concept, tbox)`` is ``False``). A
1685
+ ``"refuted"`` answers "satisfiable" only when
1686
+ :attr:`refutation_is_decisive`.
1687
+
1688
+ The ``OwlThing`` conjunct is what makes this the OWL 2 question: a data
1689
+ value satisfies every concept that says nothing about it, so without it
1690
+ ``∃HasV.xsd:integer ⊓ ∀HasV.xsd:string`` would be "satisfiable" by a
1691
+ data value, where OWL 2 makes it unsatisfiable (the datatypes are
1692
+ disjoint).
1693
+
1694
+ Raises:
1695
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
1696
+ as :meth:`subsumption_goal`, with ``query=[concept]``.
1697
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
1698
+ as :meth:`subsumption_goal`, with ``query=[concept]``.
1699
+ """
1700
+ coverage = self._covers("unsatisfiability_goal", concept)
1701
+ avoid = set(coverage.avoid) | _individual_names(concept)
1702
+ (var,) = _binder_names((coverage.var,), avoid)
1703
+ x = Variable(var)
1704
+ body = _translate(concept, x, _fresh_var_factory(var, avoid))
1705
+ if self.separation == "two-sorted":
1706
+ body = _FAnd(Atom(OWL_THING, (x,)), body)
1707
+ return _FNot(Quantifier("∃", x, body))
1708
+
1709
+ def instance_goal(self, individual: str, concept: Concept) -> Node:
1710
+ """The goal ``individual : concept`` — ``π(concept, individual)`` with
1711
+ the individual as a constant: ``api.prove(kb.instance_goal(a, C),
1712
+ kb.premises)`` is ``"proved"`` ⇒ the knowledge base entails ``a : C``
1713
+ (``dl.instance_check(abox, a, C, tbox)``). An individual the bundle
1714
+ does not type (it occurs in no axiom) is asked about as an OBJECT on a
1715
+ two-sorted bundle, ``OwlThing(a) → π(C, a)``: every OWL 2 individual is
1716
+ in the object domain.
1717
+
1718
+ Raises:
1719
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
1720
+ as :meth:`subsumption_goal`, with ``query=[concept]``.
1721
+ ~unicode_logic_kit.dl.tableau.RoleExpressionError:
1722
+ as :meth:`subsumption_goal`, with ``query=[concept]``.
1723
+ TypeError: ``individual`` is not a non-empty name.
1724
+ """
1725
+ if not isinstance(individual, str) or not individual:
1726
+ raise TypeError(
1727
+ f"KnowledgeBaseFOL.instance_goal: the individual is a non-empty "
1728
+ f"name (str), got {type(individual).__name__} {individual!r}.")
1729
+ coverage = self._covers("instance_goal", concept)
1730
+ term = Constant(individual)
1731
+ avoid = set(coverage.avoid) | _individual_names(concept) | {individual}
1732
+ goal = _translate(concept, term, _fresh_var_factory(individual, avoid))
1733
+ if self.separation == "two-sorted" and individual not in coverage.individuals:
1734
+ return Implies(Atom(OWL_THING, (term,)), goal)
1735
+ return goal
1736
+
1737
+ def to_dict(self) -> dict:
1738
+ """JSON-compatible form: every formula through its own ``to_dict``.
1739
+
1740
+ The five keys are the FORMULAS — ``side_axioms``' ``kind``/``group``
1741
+ labels are bookkeeping for the caller, not part of the logical content,
1742
+ and ``formulas`` is a regrouping of ``formula``; a reader of this dict
1743
+ loses nothing it could use to ask a prover a question.
1744
+ """
1745
+ return {
1746
+ "formula": self.formula.to_dict(),
1747
+ "axioms": [a.to_dict() for a in self.axioms],
1748
+ "tbox": self.tbox.to_dict(),
1749
+ "abox": self.abox.to_dict(),
1750
+ "individuals": list(self.individuals),
1751
+ }
1752
+
1753
+
1754
+ def _abox_individuals(abox: ABox) -> Tuple[str, ...]:
1755
+ """The individual names ``abox`` mentions, sorted — and NO anonymous
1756
+ fallback individual, unlike the tableau's own (private) ``_individuals``:
1757
+ there the placeholder exists so an empty ABox still has a node to run on,
1758
+ here it would be a constant no assertion ever named.
1759
+
1760
+ The scan itself is :func:`~unicode_logic_kit.dl.tableau._abox_individual_names`,
1761
+ driven by :data:`~unicode_logic_kit.dl.tableau._AXIOM_KINDS`'
1762
+ ``individual_positions`` column: an assertion kind added later would
1763
+ otherwise have to be remembered in two independent field-by-field scans,
1764
+ and the one that forgot it would report ``individuals == ()`` for a
1765
+ knowledge base that names individuals.
1766
+ """
1767
+ return tuple(sorted(_abox_individual_names(abox)))
1768
+
1769
+
1770
+ # --------------------------------------------------------------------------- #
1771
+ # The two-sorted discipline: the vocabulary a knowledge base uses, and the
1772
+ # side axioms derived from it.
1773
+ # --------------------------------------------------------------------------- #
1774
+
1775
+ _SEPARATIONS = ("two-sorted", "data-lattice", "none")
1776
+
1777
+
1778
+ def _check_separation(separation: str, where: str = "dl.kb_to_fol") -> None:
1779
+ if separation not in _SEPARATIONS:
1780
+ raise UnsupportedDatatypeError(
1781
+ f"{where}: separation={separation!r} is not one of "
1782
+ f"{', '.join(map(repr, _SEPARATIONS))}. 'two-sorted' is the full "
1783
+ f"discipline (the default), 'data-lattice' only the datatype facts, "
1784
+ f"'none' the data-box images alone.")
1785
+
1786
+
1787
+ @dataclass
1788
+ class _Vocabulary:
1789
+ """The names a knowledge base uses, by kind — what the sort axioms are
1790
+ derived FROM, so that they are scoped to the knowledge base's own vocabulary
1791
+ and not to every name in the OWL 2 datatype map."""
1792
+
1793
+ classes: Set[str] = field(default_factory=set)
1794
+ object_roles: Set[str] = field(default_factory=set)
1795
+ data_properties: Set[str] = field(default_factory=set)
1796
+ individuals: Set[str] = field(default_factory=set)
1797
+ datatypes: Set[str] = field(default_factory=set)
1798
+ literals: List[Literal] = field(default_factory=list)
1799
+
1800
+ def has_data(self) -> bool:
1801
+ """True iff the knowledge base has a data layer at all: a data property,
1802
+ a datatype or a literal occurs in it. Every data axiom and every data
1803
+ restriction names at least a data property, so this is the whole test."""
1804
+ return bool(self.data_properties or self.datatypes or self.literals)
1805
+
1806
+ def add_role(self, role) -> None:
1807
+ self.object_roles.add(role.role if isinstance(role, InverseRole) else role)
1808
+
1809
+ def add_literal(self, literal: Literal) -> None:
1810
+ self.literals.append(literal)
1811
+ self.datatypes.add(literal.datatype)
1812
+
1813
+ def add_datarange(self, datarange: DataRange) -> None:
1814
+ self.datatypes.update(datarange_datatypes(datarange))
1815
+ for literal in datarange_literals(datarange):
1816
+ self.add_literal(literal)
1817
+
1818
+ def add_concept(self, concept: Concept) -> None:
1819
+ if isinstance(concept, Atomic):
1820
+ self.classes.add(concept.name)
1821
+ elif isinstance(concept, Nominal):
1822
+ self.individuals.add(concept.individual)
1823
+ elif isinstance(concept, HasValue):
1824
+ self.add_role(concept.role)
1825
+ self.individuals.add(concept.individual)
1826
+ elif isinstance(concept, Not):
1827
+ self.add_concept(concept.concept)
1828
+ elif isinstance(concept, (And, Or)):
1829
+ self.add_concept(concept.left)
1830
+ self.add_concept(concept.right)
1831
+ elif isinstance(concept, (Exists, ForAll, AtLeast, AtMost)):
1832
+ self.add_role(concept.role)
1833
+ self.add_concept(concept.concept)
1834
+ elif isinstance(concept, (DataExists, DataForAll, DataAtLeast, DataAtMost)):
1835
+ self.data_properties.add(concept.prop)
1836
+ self.add_datarange(concept.datarange)
1837
+ elif isinstance(concept, DataHasValue):
1838
+ self.data_properties.add(concept.prop)
1839
+ self.add_literal(concept.value)
1840
+ # Top / Bottom mention nothing.
1841
+
1842
+
1843
+ def _collect_vocabulary(tbox: Optional[TBox], abox: Optional[ABox], *,
1844
+ where: str = "dl.translate",
1845
+ query: Iterable[Concept] = ()) -> _Vocabulary:
1846
+ """Walk every axiom of ``(tbox, abox)`` once and collect its vocabulary,
1847
+ then the vocabulary of the ``query`` concepts: the concepts a caller is going
1848
+ to ASK about join the knowledge base's vocabulary, because everything derived
1849
+ from the vocabulary (the separation regime, the sort and datatype axioms, the
1850
+ punning check, the avoid set of the bound variables) must cover the question
1851
+ as well as the knowledge base. See :func:`kb_to_fol`.
1852
+
1853
+ Hand-listed over the fields, like every consumer of the TBox/ABox shape, and
1854
+ for that reason pinned by ``tests/test_dl_data.py``: a field the walk does
1855
+ not read would silently shrink the sort axioms.
1856
+
1857
+ Every stored value is VALIDATED first (``dl.tableau._validate_role_box`` and
1858
+ friends — the same checks the tableau's shared guard runs), because this walk
1859
+ is the first thing every image does with a ``TBox`` and a hand-built one with
1860
+ a malformed entry (a 3-tuple where a pair is stored) would otherwise die here,
1861
+ with the bare ``ValueError`` of an unpacking, before any second line of
1862
+ defence could name it.
1863
+ """
1864
+ if tbox is not None:
1865
+ _validate_role_box(tbox, where=where)
1866
+ _validate_data_box(tbox, where=where)
1867
+ _reject_abox_roles(abox, where=where)
1868
+ v = _Vocabulary()
1869
+ if tbox is not None:
1870
+ for sub, sup in tbox.inclusions:
1871
+ v.add_concept(sub)
1872
+ v.add_concept(sup)
1873
+ for sub_role, super_role in tbox.role_inclusions:
1874
+ v.add_role(sub_role)
1875
+ v.add_role(super_role)
1876
+ for names in (tbox.transitive_roles, tbox.symmetric_roles,
1877
+ tbox.asymmetric_roles, tbox.reflexive_roles,
1878
+ tbox.irreflexive_roles, tbox.functional_roles,
1879
+ tbox.inverse_functional_roles):
1880
+ v.object_roles.update(names)
1881
+ for pair in tbox.inverse_role_pairs + tbox.disjoint_role_pairs:
1882
+ v.object_roles.update(pair)
1883
+ for chain, super_role in tbox.role_chains:
1884
+ v.object_roles.update(chain)
1885
+ v.object_roles.add(super_role)
1886
+ for role, concept in tbox.role_domains + tbox.role_ranges:
1887
+ v.object_roles.add(role)
1888
+ v.add_concept(concept)
1889
+ for pair in tbox.data_property_inclusions + tbox.disjoint_data_property_pairs:
1890
+ v.data_properties.update(pair)
1891
+ v.data_properties.update(tbox.functional_data_properties)
1892
+ for prop, concept in tbox.data_property_domains:
1893
+ v.data_properties.add(prop)
1894
+ v.add_concept(concept)
1895
+ for prop, datarange in tbox.data_property_ranges:
1896
+ v.data_properties.add(prop)
1897
+ v.add_datarange(datarange)
1898
+ for name, datarange in tbox.datatype_definitions:
1899
+ v.datatypes.add(canonical_datatype_name(name))
1900
+ v.add_datarange(datarange)
1901
+ if abox is not None:
1902
+ for individual, concept in abox.concept_assertions:
1903
+ v.individuals.add(individual)
1904
+ v.add_concept(concept)
1905
+ for a, b, role in abox.role_assertions + abox.negative_role_assertions:
1906
+ v.individuals.update((a, b))
1907
+ v.object_roles.add(role)
1908
+ for a, b in abox.distinct_assertions + abox.same_assertions:
1909
+ v.individuals.update((a, b))
1910
+ for individual, prop, literal in abox.data_assertions + abox.negative_data_assertions:
1911
+ v.individuals.add(individual)
1912
+ v.data_properties.add(prop)
1913
+ v.add_literal(literal)
1914
+ for concept in query:
1915
+ v.add_concept(concept)
1916
+ return v
1917
+
1918
+
1919
+ def _check_reserved_names(vocabulary: _Vocabulary, separation: str,
1920
+ where: str = "dl.kb_to_fol") -> None:
1921
+ """Refuse a knowledge base that uses a name the data image reserves.
1922
+
1923
+ ``OwlData`` is reserved whenever there is a data layer (``rdfs:Literal`` and
1924
+ every complement are rendered with it); ``OwlThing`` only when the
1925
+ two-sorted axioms are asked for. A class that happened to be called
1926
+ ``OwlData`` would otherwise be constrained by the sort axioms as if it were
1927
+ the data domain — a silent change to the ontology's own class.
1928
+ """
1929
+ reserved = (OWL_DATA, OWL_THING) if separation == "two-sorted" else (OWL_DATA,)
1930
+ for name in reserved:
1931
+ for kind, names in (("class", vocabulary.classes),
1932
+ ("object role", vocabulary.object_roles),
1933
+ ("data property", vocabulary.data_properties),
1934
+ ("individual", vocabulary.individuals),
1935
+ ("datatype", vocabulary.datatypes)):
1936
+ if name in names:
1937
+ guard = ("the data domain" if name == OWL_DATA else "the object domain")
1938
+ raise UnsupportedDatatypeError(
1939
+ f"{where}: {name!r} is this module's reserved guard "
1940
+ f"predicate for {guard} of the OWL 2 two-sorted image (see "
1941
+ f"data_sort_axioms), but this knowledge base uses {name!r} as "
1942
+ f"a {kind} name, so the sort axioms would constrain the "
1943
+ f"ontology's own {kind}. Rename it"
1944
+ + (", or pass separation='data-lattice' (which reserves only "
1945
+ "'OwlData') or 'none'." if name == OWL_THING else "."))
1946
+
1947
+
1948
+ def _check_concepts_punning(concepts: Sequence[Concept], where: str) -> None:
1949
+ """:func:`_check_name_punning` over the vocabulary of ``concepts`` alone: the
1950
+ refusal of a pun INSIDE what a renderer is given (``∃P.B ⊓ ∃P.xsd:integer``),
1951
+ for the entry points that never see a knowledge base."""
1952
+ vocabulary = _Vocabulary()
1953
+ for concept in concepts:
1954
+ vocabulary.add_concept(concept)
1955
+ _check_name_punning(vocabulary, where, "the concepts it is given")
1956
+
1957
+
1958
+ def _check_name_punning(vocabulary: _Vocabulary, where: str,
1959
+ scope: str = "this knowledge base") -> None:
1960
+ """Refuse a knowledge base that uses ONE name for two kinds OWL 2 DL keeps
1961
+ apart: an object property and a data property, or a class and a datatype.
1962
+
1963
+ OWL 2 DL (Structural Specification, section 5.8.1) forbids both pairs. The
1964
+ stored axioms keep the kinds apart (the TBox has separate fields for roles
1965
+ and data properties), but the first-order image has ONE predicate per name:
1966
+ ``P(x, y)`` of an object property and ``P(x, v)`` of a data property are the
1967
+ same predicate, so the conflation changes what follows from the knowledge
1968
+ base (a functional ``P`` with an object successor and a data value became
1969
+ INCONSISTENT, which the two-sorted reading says it is not). Printing it as
1970
+ one predicate would be an approximation; so it is refused, by name, before
1971
+ any image is built. A class, an object property and an individual that share
1972
+ a name (punning OWL 2 DL does allow) are not touched.
1973
+
1974
+ A class is compared with the datatypes by its CANONICAL name, so a class
1975
+ spelled with the full ``http://www.w3.org/2001/XMLSchema#integer`` IRI is
1976
+ the datatype ``xsd:integer`` too.
1977
+ """
1978
+ pairs = (
1979
+ ("an object property", vocabulary.object_roles,
1980
+ "a data property", vocabulary.data_properties,
1981
+ lambda name: name),
1982
+ ("a class", vocabulary.classes,
1983
+ "a datatype", vocabulary.datatypes,
1984
+ canonical_datatype_name),
1985
+ )
1986
+ for first_kind, first, second_kind, second, canonical in pairs:
1987
+ if not first or not second:
1988
+ continue
1989
+ clash = sorted(name for name in first if canonical(name) in second)
1990
+ if clash:
1991
+ name = clash[0]
1992
+ raise UnsupportedDatatypeError(
1993
+ f"{where}: the name {name!r} is used as both {first_kind} and "
1994
+ f"{second_kind} in {scope}. OWL 2 DL forbids that "
1995
+ f"(the two are disjoint kinds), and the first-order image has "
1996
+ f"ONE predicate per name, so it would read {name!r} as both at "
1997
+ f"once and change what follows. Rename one of them"
1998
+ + (f" (all clashing names: {', '.join(map(repr, clash))})."
1999
+ if len(clash) > 1 else "."))
2000
+
2001
+
2002
+ def _literal_family(literal: Literal) -> Optional[str]:
2003
+ """The group within which two distinct literal TERMS are known to be
2004
+ distinct VALUES, or ``None`` when this kit cannot say.
2005
+
2006
+ Exact numbers are compared by value (``Number``), the strings by their
2007
+ (whitespace-processed) text — ``xsd:string``, language-tagged strings, and
2008
+ ``xsd:normalizedString``/``xsd:token``, whose term IS the ``xsd:string`` term
2009
+ of the processed text (:meth:`~unicode_logic_kit.dl.datatypes.Literal.to_term`)
2010
+ —, ``xsd:anyURI`` by its collapsed text, ``xsd:boolean`` by its canonical
2011
+ ``true``/``false``. Other datatypes (``xsd:dateTime``, ``xsd:hexBinary``, …)
2012
+ have lexical forms that are NOT in one-to-one correspondence with values
2013
+ (``"0A"`` and ``"0a"`` are one hexBinary value), so claiming two different
2014
+ spellings denote different values would be unsound.
2015
+ """
2016
+ if literal.datatype in EXACT_NUMBER_DATATYPES:
2017
+ return "number"
2018
+ if literal.datatype in ("xsd:string", "rdf:PlainLiteral", "xsd:normalizedString",
2019
+ "xsd:token"):
2020
+ return "string"
2021
+ if literal.datatype == "xsd:anyURI":
2022
+ return "anyURI"
2023
+ if literal.datatype == "xsd:boolean":
2024
+ return "boolean"
2025
+ return None
2026
+
2027
+
2028
+ @dataclass(frozen=True)
2029
+ class _Coverage:
2030
+ """What the side axioms of one :func:`kb_to_fol` bundle were derived from:
2031
+ the vocabulary of the knowledge base AND of the ``query=`` concepts, and the
2032
+ ``var`` the bundle's GCIs bind. The three goal methods of
2033
+ :class:`KnowledgeBaseFOL` read it to tell a concept the bundle covers from one
2034
+ it does not. ``individuals`` is every individual the image types and so
2035
+ every one a bound variable must avoid.
2036
+ """
2037
+
2038
+ var: str
2039
+ vocabulary: _Vocabulary
2040
+ individuals: FrozenSet[str]
2041
+ literals: FrozenSet[Tuple[Node, str]] # (term, written datatype) per literal
2042
+
2043
+ @property
2044
+ def avoid(self) -> FrozenSet[str]:
2045
+ return self.individuals
2046
+
2047
+
2048
+ def _reject_data_concept(concept: Concept, where: str) -> None:
2049
+ """Refuse, by name, a concept with a data restriction for the registry edge
2050
+ ``alc → fol``, which declares ``faithful`` and carries no side axioms.
2051
+
2052
+ That is true of a concept without a data layer. With one the image is a
2053
+ guarded ONE-sorted theory whose sorts, typing and datatype lattice are side
2054
+ axioms of a KNOWLEDGE BASE (:func:`data_sort_axioms`), so the bare image is a
2055
+ weaker theory: ``∃HasV.xsd:integer ⊓ ∀HasV.xsd:string`` closed existentially is
2056
+ satisfiable by a data value, where OWL 2 makes it unsatisfiable. A concept
2057
+ without one is untouched.
2058
+ """
2059
+ vocabulary = _Vocabulary()
2060
+ vocabulary.add_concept(concept)
2061
+ if not vocabulary.has_data():
2062
+ return
2063
+ names = ([f"the data property {name!r}" for name in sorted(vocabulary.data_properties)]
2064
+ + [f"the datatype {name!r}" for name in sorted(vocabulary.datatypes)])
2065
+ raise UnsupportedDatatypeError(
2066
+ f"{where}: the concept {concept.to_unicode()} has a data restriction "
2067
+ f"({', '.join(names)}). Its first-order image is ONE-sorted over "
2068
+ f"OwlThing and OwlData, and what makes it two-sorted — the separation "
2069
+ f"of the two domains, the typing of the property, the datatype lattice "
2070
+ f"and its disjointness, the literal facts — are side axioms of a "
2071
+ f"KNOWLEDGE BASE, not of one concept. This edge declares 'faithful' "
2072
+ f"and carries none, so it would answer for a weaker theory: "
2073
+ f"∃HasV.xsd:integer ⊓ ∀HasV.xsd:string would be satisfiable (a data "
2074
+ f"value satisfies it) where OWL 2 makes it unsatisfiable. Ask the "
2075
+ f"knowledge base instead: kb = dl.kb_to_fol(tbox, abox, "
2076
+ f"query=[concept]), then api.prove(kb.unsatisfiability_goal(concept), "
2077
+ f"kb.tbox_premises) (kb.subsumption_goal and kb.instance_goal are "
2078
+ f"the other two questions).")
2079
+
2080
+
2081
+ def _literal_key(literal: Literal) -> Tuple[Node, str]:
2082
+ """``(term, datatype)`` of a literal — one ``LiteralTyping`` fact of the
2083
+ image. Raises :class:`UnsupportedDatatypeError` for a literal with no term."""
2084
+ return (literal.to_term(), literal.datatype)
2085
+
2086
+
2087
+ def _query_concepts(query, where: str) -> Tuple[Concept, ...]:
2088
+ """``query=`` as a tuple of concepts, or a :class:`TypeError` naming the
2089
+ mistake: a bare concept (``query=C``) or a string would otherwise iterate
2090
+ into nonsense, and a non-concept is no question."""
2091
+ if isinstance(query, (Concept, str)):
2092
+ raise TypeError(
2093
+ f"{where}: query= is an iterable of the concepts you are going to "
2094
+ f"ask about — write query=[concept], not query={query!r}.")
2095
+ concepts = () if query is None else tuple(query) # None: no query
2096
+ for concept in concepts:
2097
+ if not isinstance(concept, Concept):
2098
+ raise TypeError(
2099
+ f"{where}: query= takes concepts (dl.Atomic, dl.DataExists, …), "
2100
+ f"got {type(concept).__name__} {concept!r}.")
2101
+ # Rendered once, only to refuse by name what no goal could be built from:
2102
+ # an OWL 2 built-in property name as a role, a literal with no term. Better
2103
+ # here than as a typing axiom for a name that is no property at all.
2104
+ for concept in concepts:
2105
+ names = _individual_names(concept)
2106
+ _translate(concept, Variable("x"), _fresh_var_factory("x", names | {"x"}))
2107
+ return concepts
2108
+
2109
+
2110
+ def _check_goal_concepts(coverage: _Coverage, separation: str, data_layer: bool,
2111
+ concepts: Sequence[Concept], where: str) -> None:
2112
+ """Refuse, by name, a goal over ``concepts`` that the bundle behind
2113
+ ``coverage`` is not the right theory for.
2114
+
2115
+ Two things make the answer wrong, and both are silent in the prover: a name
2116
+ used as two kinds of thing at once (the punning check, run over the bundle's
2117
+ vocabulary together with the goal's), and a name whose side axioms are not
2118
+ there. What a two-sorted bundle derives its axioms from is the object
2119
+ properties (typing), data properties (typing), datatypes (guard, lattice,
2120
+ disjointness) and literals (typing, distinctness); a data-lattice bundle
2121
+ only the last two; a bundle with no data layer none of them, so ANY data
2122
+ vocabulary in the goal is uncovered. ``separation="none"`` over a data layer
2123
+ says the caller supplies the discipline, so nothing is checked.
2124
+ """
2125
+ goal = _Vocabulary()
2126
+ for concept in concepts:
2127
+ goal.add_concept(concept)
2128
+ have = coverage.vocabulary
2129
+ _check_name_punning(_Vocabulary(
2130
+ classes=have.classes | goal.classes,
2131
+ object_roles=have.object_roles | goal.object_roles,
2132
+ data_properties=have.data_properties | goal.data_properties,
2133
+ datatypes=have.datatypes | goal.datatypes), where,
2134
+ "this knowledge base together with the goal")
2135
+ if data_layer:
2136
+ _check_reserved_names(goal, separation, where)
2137
+
2138
+ roles = data_layer and separation == "two-sorted"
2139
+ properties = (not data_layer) or separation == "two-sorted"
2140
+ datatypes = (not data_layer) or separation in ("two-sorted", "data-lattice")
2141
+ missing: List[str] = []
2142
+ if roles:
2143
+ missing += [f"the object property {name!r}"
2144
+ for name in sorted(goal.object_roles - have.object_roles)]
2145
+ if properties:
2146
+ missing += [f"the data property {name!r}"
2147
+ for name in sorted(goal.data_properties - have.data_properties)]
2148
+ if datatypes:
2149
+ wanted = {canonical_datatype_name(name) for name in goal.datatypes}
2150
+ known = {canonical_datatype_name(name) for name in have.datatypes}
2151
+ missing += [f"the datatype {name!r}"
2152
+ for name in sorted(wanted - known - {"rdfs:Literal"})]
2153
+ seen = {}
2154
+ for literal in goal.literals:
2155
+ key = _literal_key(literal)
2156
+ if key not in coverage.literals:
2157
+ seen[literal.to_unicode()] = literal
2158
+ missing += [f"the literal {text}" for text in sorted(seen)]
2159
+ if not missing:
2160
+ return
2161
+ why = ("this knowledge base has no data layer, so its image carries no sort, "
2162
+ "typing or datatype axioms at all" if not data_layer else
2163
+ "the sort, typing, datatype-lattice and literal axioms of this "
2164
+ "knowledge base's image are derived from the names IT uses")
2165
+ raise UnsupportedDatatypeError(
2166
+ f"{where}: the goal names {', '.join(missing)}, which the knowledge "
2167
+ f"base this bundle was built from does not cover ({why}). A goal over "
2168
+ f"them is asked of a weaker theory than the one OWL 2 describes and "
2169
+ f"comes back 'refuted' for entailments OWL 2 makes — silently. Hand the "
2170
+ f"concepts you are going to ask about to the knowledge base when you "
2171
+ f"build it, dl.kb_to_fol(tbox, abox, query=[...]), and ask the new "
2172
+ f"bundle.")
2173
+
2174
+
2175
+ def _sort_axioms(vocabulary: _Vocabulary, separation: str) -> List[SideAxiom]:
2176
+ """The derived side axioms of a knowledge base that has a data layer. See
2177
+ :func:`data_sort_axioms` for what each one says and why it is there.
2178
+
2179
+ The four prefix variables avoid the knowledge base's individuals like every
2180
+ other binder of the image (:func:`_binder_names`). None of these axioms
2181
+ mentions an individual UNDER a quantifier, so none can capture one — it is
2182
+ kept as an invariant of the whole image, not a case analysis."""
2183
+ parts: List[SideAxiom] = []
2184
+ x, y, t, v = (Variable(name) for name in _binder_names(
2185
+ ("x", "y", "t", "v"), vocabulary.individuals))
2186
+ thing, data = (lambda term: Atom(OWL_THING, (term,))), (lambda term: Atom(OWL_DATA, (term,)))
2187
+
2188
+ def forall(variables, body) -> Node:
2189
+ return _forall(list(variables), body)
2190
+
2191
+ if separation == "two-sorted":
2192
+ parts.append(SideAxiom("DomainSeparation", "sort",
2193
+ forall([t], _FNot(_FAnd(thing(t), data(t))))))
2194
+ parts.append(SideAxiom("DomainNonEmptiness", "sort",
2195
+ Quantifier("∃", t, thing(t))))
2196
+ parts.append(SideAxiom("DomainNonEmptiness", "sort",
2197
+ Quantifier("∃", t, data(t))))
2198
+ for role in sorted(vocabulary.object_roles):
2199
+ parts.append(SideAxiom("ObjectPropertyTyping", "sort", forall([x, y], Implies(
2200
+ Atom(role, (x, y)), _FAnd(thing(x), thing(y))))))
2201
+ for prop in sorted(vocabulary.data_properties):
2202
+ parts.append(SideAxiom("DataPropertyTyping", "sort", forall([x, v], Implies(
2203
+ Atom(prop, (x, v)), _FAnd(thing(x), data(v))))))
2204
+ for individual in sorted(vocabulary.individuals):
2205
+ parts.append(SideAxiom("IndividualTyping", "sort", thing(Constant(individual))))
2206
+
2207
+ datatypes = sorted(canonical_datatype_name(name) for name in vocabulary.datatypes
2208
+ if canonical_datatype_name(name) != "rdfs:Literal")
2209
+ used = set(datatypes)
2210
+ for name in datatypes:
2211
+ parts.append(SideAxiom("DatatypeGuard", "datatype", forall([v], Implies(
2212
+ Atom(name, (v,)), data(v)))))
2213
+ for name in datatypes:
2214
+ for ancestor in sorted(datatype_ancestors(name) & used):
2215
+ parts.append(SideAxiom("DatatypeSubsumption", "datatype", forall([v], Implies(
2216
+ Atom(name, (v,)), Atom(ancestor, (v,))))))
2217
+ for i, left in enumerate(datatypes):
2218
+ for right in datatypes[i + 1:]:
2219
+ family_left, family_right = datatype_family(left), datatype_family(right)
2220
+ if family_left is not None and family_right is not None \
2221
+ and family_left != family_right:
2222
+ parts.append(SideAxiom("DatatypeDisjointness", "datatype", forall([v], _FNot(
2223
+ _FAnd(Atom(left, (v,)), Atom(right, (v,)))))))
2224
+
2225
+ # Literals: one TERM per data value, however many spellings denote it.
2226
+ terms: dict = {}
2227
+ for literal in vocabulary.literals:
2228
+ term = literal.to_term()
2229
+ entry = terms.setdefault(term, {"datatypes": set(), "family": _literal_family(literal)})
2230
+ entry["datatypes"].add(literal.datatype)
2231
+ # An order, not a text a reader sees: the literals are sorted by the text of
2232
+ # 0.30.0 (every constant by its bare name), so the side axioms keep the order
2233
+ # they had whatever the names hold (an escape in a quoted name changes how
2234
+ # two names compare).
2235
+ ordered = sorted(terms, key=key_text)
2236
+ for term in ordered:
2237
+ parts.append(SideAxiom("LiteralTyping", "datatype", data(term)))
2238
+ for name in sorted(terms[term]["datatypes"]):
2239
+ if name != "rdfs:Literal":
2240
+ parts.append(SideAxiom("LiteralTyping", "datatype", Atom(name, (term,))))
2241
+ for family in ("number", "string", "anyURI", "boolean"):
2242
+ members = [term for term in ordered if terms[term]["family"] == family]
2243
+ for i, left in enumerate(members):
2244
+ for right in members[i + 1:]:
2245
+ parts.append(SideAxiom("LiteralDistinctness", "datatype",
2246
+ Atom("≠", (left, right))))
2247
+ return parts
2248
+
2249
+
2250
+ def data_sort_axioms(tbox: Optional[TBox] = None, abox: Optional[ABox] = None, *,
2251
+ separation: str = "two-sorted",
2252
+ query: Iterable[Concept] = ()) -> Tuple[SideAxiom, ...]:
2253
+ """The side axioms that turn the data layer's one-sorted image into the
2254
+ two-sorted theory OWL 2 describes, derived from the vocabulary ``(tbox,
2255
+ abox)`` actually uses — the same axioms :func:`kb_to_fol` puts in
2256
+ ``side_axioms``, for a caller who builds a question by hand (a bare
2257
+ :func:`concept_to_fol` query, say) and needs them as premises.
2258
+
2259
+ Empty for a knowledge base without a data layer, and for
2260
+ ``separation="none"``. ``query`` is :func:`kb_to_fol`'s: the concepts you are
2261
+ going to ask about, whose vocabulary joins the knowledge base's — a
2262
+ knowledge base without a data layer asked about a data restriction HAS one,
2263
+ for this purpose.
2264
+
2265
+ ``separation="two-sorted"`` (``group "sort"``, then ``"datatype"``):
2266
+
2267
+ * ``DomainSeparation`` ``∀t ¬(OwlThing(t) ∧ OwlData(t))`` — OWL 2's object
2268
+ and data domains are disjoint;
2269
+ * ``DomainNonEmptiness`` ``∃t OwlThing(t)``, ``∃t OwlData(t)``;
2270
+ * ``ObjectPropertyTyping`` ``∀x ∀y (r(x, y) → OwlThing(x) ∧ OwlThing(y))``
2271
+ per object role, ``DataPropertyTyping`` ``∀x ∀v (d(x, v) → OwlThing(x) ∧
2272
+ OwlData(v))`` per data property, ``IndividualTyping`` ``OwlThing(a)`` per
2273
+ individual — one per NAME the knowledge base uses, not per class name:
2274
+ with every GCI relativised to the object domain a class atom needs no
2275
+ typing of its own.
2276
+
2277
+ Both ``"two-sorted"`` and ``"data-lattice"`` (``group "datatype"``), over the
2278
+ datatypes the knowledge base MENTIONS:
2279
+
2280
+ * ``DatatypeGuard`` ``∀v (D(v) → OwlData(v))`` — a datatype is a subset of
2281
+ the data domain (``rdfs:Literal`` IS the data domain and is rendered as
2282
+ ``OwlData`` itself, so it has none);
2283
+ * ``DatatypeSubsumption`` ``∀v (D(v) → E(v))`` for ``D`` below ``E`` in the
2284
+ OWL 2 datatype map, both mentioned (``xsd:integer`` ⊑ ``xsd:decimal``);
2285
+ * ``DatatypeDisjointness`` ``∀v ¬(D(v) ∧ E(v))`` for two mentioned built-in
2286
+ datatypes of different families (``xsd:integer``/``xsd:string``): this is
2287
+ what makes ``DataPropertyRange(d xsd:integer)`` together with
2288
+ ``DataPropertyRange(d xsd:string)`` force ``d`` empty;
2289
+ * ``LiteralTyping`` ``OwlData(t)`` and ``D(t)`` for each literal's term ``t``
2290
+ and datatype ``D``;
2291
+ * ``LiteralDistinctness`` ``t ≠ u`` for two distinct terms within the exact
2292
+ numbers, within the strings (``xsd:string``, language-tagged strings and
2293
+ the whitespace-processed ``xsd:normalizedString``/``xsd:token``), within
2294
+ the ``xsd:anyURI`` values, within the booleans — pairwise, so QUADRATIC in
2295
+ the number of distinct literals of one family (1000 distinct integers are
2296
+ 499,500 axioms). Across families the typing and the disjointness already
2297
+ separate them; for the datatypes whose lexical forms are not in
2298
+ one-to-one correspondence with values (``xsd:dateTime``,
2299
+ ``xsd:hexBinary``) nothing is claimed.
2300
+
2301
+ What it is not: complete. Facet comparisons are uninterpreted atoms for
2302
+ ``api.prove`` (use :mod:`unicode_logic_kit.atp.z3_arith` for arithmetic over
2303
+ data ranges alone), and the cardinality of a value space (``xsd:boolean``
2304
+ has exactly two values) is not stated.
2305
+
2306
+ Raises:
2307
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
2308
+ ``separation`` is not a known name, the
2309
+ knowledge base uses ``OwlThing``/``OwlData`` as its own name, a
2310
+ literal has no term, or one name is used both as an object property
2311
+ and a data property or both as a class and a datatype (OWL 2 DL
2312
+ forbids both; the image has one predicate per name — rename one).
2313
+ """
2314
+ _check_separation(separation)
2315
+ queried = _query_concepts(query, "dl.data_sort_axioms")
2316
+ vocabulary = _collect_vocabulary(tbox, abox, query=queried)
2317
+ _check_name_punning(vocabulary, "dl.data_sort_axioms")
2318
+ if separation == "none" or not vocabulary.has_data():
2319
+ return ()
2320
+ _check_reserved_names(vocabulary, separation)
2321
+ return tuple(_sort_axioms(vocabulary, separation))
2322
+
2323
+
2324
+ def _union_vocabularies(vocabularies: Iterable[_Vocabulary]) -> _Vocabulary:
2325
+ """One :class:`_Vocabulary` holding every name of each of ``vocabularies``."""
2326
+ union = _Vocabulary()
2327
+ for vocabulary in vocabularies:
2328
+ union.classes |= vocabulary.classes
2329
+ union.object_roles |= vocabulary.object_roles
2330
+ union.data_properties |= vocabulary.data_properties
2331
+ union.individuals |= vocabulary.individuals
2332
+ union.datatypes |= vocabulary.datatypes
2333
+ union.literals.extend(vocabulary.literals)
2334
+ return union
2335
+
2336
+
2337
+ def check_kb_names(*parts, separation: str = "two-sorted",
2338
+ query: Iterable[Concept] = ()) -> None:
2339
+ """Run, over several pieces of one knowledge base at once, the check on names
2340
+ that :func:`kb_to_fol` runs over the whole of it.
2341
+
2342
+ Every function that renders one box of a knowledge base —
2343
+ :func:`tbox_to_fol`, :func:`rbox_to_fol`, :func:`databox_to_fol`,
2344
+ :func:`abox_to_fol` — refuses a name used as two kinds OWL 2 DL keeps apart
2345
+ (an object property and a data property, a class and a datatype, or a name
2346
+ the data image reserves) only over what it is given. A caller who renders the
2347
+ boxes one call at a time, from the same ontology or from several, and
2348
+ conjoins the images by hand, therefore has no call that sees the clash:
2349
+ ``P`` as the object property of an ABox role assertion and as the data
2350
+ property of a TBox's ``DataPropertyRange`` is ONE predicate ``P`` in the
2351
+ combined image, and what follows from it changes. This function takes the
2352
+ pieces and answers what :func:`kb_to_fol` would answer about their union.
2353
+
2354
+ ``parts`` are the :class:`~unicode_logic_kit.dl.tableau.TBox`,
2355
+ :class:`~unicode_logic_kit.dl.tableau.ABox` or
2356
+ :class:`KnowledgeBaseFOL` objects the images were built from, in any number
2357
+ and any mixture (a bundle contributes the vocabulary it was built over,
2358
+ ``query=`` concepts included). A formula is not accepted: the image of a box
2359
+ is an ordinary first-order formula, in which ``P(x, y)`` of an object
2360
+ property and ``P(x, v)`` of a data property are the same atom, so which kind
2361
+ a predicate was is not recorded in it, and any guess from its shape would be
2362
+ an approximation. ``separation`` and ``query`` are :func:`kb_to_fol`'s.
2363
+
2364
+ A class, an object property and an individual that share a name (punning
2365
+ that OWL 2 DL does allow) are accepted: in the image they are three symbols,
2366
+ the unary predicate ``A(x)``, the binary predicate ``A(x, y)`` and the
2367
+ constant ``A``.
2368
+
2369
+ Returns ``None``; the function exists for what it refuses.
2370
+
2371
+ Raises:
2372
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
2373
+ ``separation`` is not a known name, a name of the union is used as
2374
+ both an object property and a data property or as both a class and a
2375
+ datatype, or ``OwlThing``/``OwlData`` is used as one of its own
2376
+ names — the refusals :func:`kb_to_fol` makes, here over the union of
2377
+ the pieces.
2378
+ TypeError: a piece is not a ``TBox``, ``ABox`` or ``KnowledgeBaseFOL``
2379
+ (a formula is named in the message), or ``query`` is not an
2380
+ iterable of concepts.
2381
+ ValueError: a ``KnowledgeBaseFOL`` piece was not built by
2382
+ :func:`kb_to_fol`, so it does not record its vocabulary.
2383
+ """
2384
+ where = "dl.check_kb_names"
2385
+ _check_separation(separation, where)
2386
+ queried = _query_concepts(query, where)
2387
+ vocabularies = []
2388
+ for part in parts:
2389
+ if isinstance(part, TBox):
2390
+ vocabularies.append(_collect_vocabulary(part, None, where=where))
2391
+ elif isinstance(part, ABox):
2392
+ vocabularies.append(_collect_vocabulary(None, part, where=where))
2393
+ elif isinstance(part, KnowledgeBaseFOL):
2394
+ if part._coverage is None:
2395
+ raise ValueError(
2396
+ f"{where}: this KnowledgeBaseFOL was not built by "
2397
+ f"dl.kb_to_fol, so it does not record the vocabulary it was "
2398
+ f"built over. Pass the TBox and ABox it was built from.")
2399
+ vocabularies.append(part._coverage.vocabulary)
2400
+ elif isinstance(part, Node):
2401
+ raise TypeError(
2402
+ f"{where}: got a formula ({type(part).__name__}), but the image "
2403
+ f"of a box does not record which kind each of its predicates is "
2404
+ f"(P(x, y) of an object property and P(x, v) of a data property "
2405
+ f"are the same atom), so the names cannot be checked from it. "
2406
+ f"Pass the TBox / ABox the image was built from.")
2407
+ else:
2408
+ raise TypeError(
2409
+ f"{where}: takes TBox, ABox or KnowledgeBaseFOL objects, got "
2410
+ f"{type(part).__name__} {part!r}.")
2411
+ vocabulary = _union_vocabularies(
2412
+ vocabularies + [_collect_vocabulary(None, None, where=where, query=queried)])
2413
+ _check_name_punning(vocabulary, where, "the pieces it is given, taken together")
2414
+ if vocabulary.has_data():
2415
+ _check_reserved_names(vocabulary, separation, where)
2416
+
2417
+
2418
+ def kb_to_fol(tbox: Optional[TBox] = None, abox: Optional[ABox] = None,
2419
+ var: str = "x", *, separation: str = "two-sorted",
2420
+ query: Iterable[Concept] = ()) -> KnowledgeBaseFOL:
2421
+ """Render the knowledge base ``(tbox, abox)`` as FOL — the supported way to ask
2422
+ a question *relative to* a TBox that has a role box.
2423
+
2424
+ The concept inclusions (:func:`tbox_to_fol`) and the ABox
2425
+ (:func:`abox_to_fol`) become :attr:`~KnowledgeBaseFOL.formula`; the role box
2426
+ (:func:`rbox_to_fol`'s content, one formula per axiom) becomes the SEPARATE
2427
+ :attr:`~KnowledgeBaseFOL.side_axioms`, each a :class:`SideAxiom` carrying
2428
+ the OWL keyword it came from, so a census of the image per axiom kind
2429
+ (:meth:`~KnowledgeBaseFOL.axioms_of_kind`) is a question with an answer;
2430
+ :attr:`~KnowledgeBaseFOL.axioms` is the bare-formula view of that one
2431
+ field. Following the kit's side-axiom convention
2432
+ (:class:`~unicode_logic_kit.comorphism.TranslationResult`), the axioms are never
2433
+ conjoined into the formula: the caller passes them as premises, which is what
2434
+ ``premises`` / ``tbox_premises`` spell. ``tbox`` may be ``None`` (an ABox-only
2435
+ knowledge base) and ``abox`` may be ``None`` (a TBox-only one), the same
2436
+ defaults as the tableau API; ``var`` is :func:`tbox_to_fol`'s.
2437
+
2438
+ ``query`` is an iterable of the concepts you are going to ASK about. Their
2439
+ vocabulary — classes, object properties, data properties, datatypes,
2440
+ literals, individuals — joins the knowledge base's for everything derived
2441
+ from the vocabulary: the separation regime (a data restriction in the query
2442
+ makes the image two-sorted even when the TBox and ABox have no data layer),
2443
+ the sort, typing, datatype-lattice, disjointness and literal axioms, the
2444
+ punning check and the variables a binder must avoid. Without ``query`` the
2445
+ bundle is what it always was. It matters because those axioms are scoped to
2446
+ the names the knowledge base mentions, so a goal naming ``xsd:decimal`` over
2447
+ a knowledge base that only mentions ``xsd:integer`` would be asked of a
2448
+ theory without ``integer ⊑ decimal`` — and a concept whose satisfiability
2449
+ turns on the datatype lattice would come out satisfiable. The goal methods
2450
+ below REFUSE a concept the bundle does not cover, by name, and say to pass
2451
+ ``query=``.
2452
+
2453
+ Each of the standard questions, with the tableau function it must agree with
2454
+ (``kb = kb_to_fol(tbox, abox, query=[…the concepts below…])``; ``prove`` is
2455
+ :func:`unicode_logic_kit.api.prove`, whose verdict ``.status`` is ``"proved"``,
2456
+ ``"refuted"`` or ``"unknown"``)::
2457
+
2458
+ # (a) subsumption relative to the TBox (the ABox plays no part)
2459
+ prove(kb.subsumption_goal(sub, sup), kb.tbox_premises)
2460
+ # proved <=> dl.subsumes(sub, sup, tbox) refuted <=> it does not
2461
+
2462
+ # (b) concept satisfiability relative to the TBox
2463
+ prove(kb.unsatisfiability_goal(C), kb.tbox_premises)
2464
+ # proved <=> dl.concept_satisfiable(C, tbox) is False
2465
+
2466
+ # (c) instance checking: does the knowledge base entail a : C ?
2467
+ prove(kb.instance_goal("a", C), kb.premises)
2468
+ # proved <=> dl.instance_check(abox, "a", C, tbox)
2469
+
2470
+ # (d) consistency of the knowledge base (Not = unicode_logic_kit.fol.nodes.Not)
2471
+ prove(Not(kb.formula), kb.axioms)
2472
+ # proved <=> INCONSISTENT (dl.abox_consistent(abox, tbox) is False)
2473
+ # refuted <=> consistent (same as is_satisfiable(kb.formula ∧ kb.axioms))
2474
+
2475
+ For a knowledge base without a data layer each goal method builds exactly the
2476
+ spelling the earlier releases documented (``subsumption_to_fol(sub, sup)``;
2477
+ ``¬∃x π(C, x)``, the ``Quantifier("∃", Variable("x"), …)`` around the free
2478
+ ``x``; ``abox_to_fol(ABox().assert_concept("a", C))``, ``π(C, a)`` with the
2479
+ individual as a constant), up to a renamed bound variable where an individual
2480
+ is called ``x``. The point of the methods is the knowledge base WITH a data
2481
+ layer, where the right goal depends on :attr:`KnowledgeBaseFOL.separation`
2482
+ and the wrong one answers silently wrong: they pick it, and
2483
+ :attr:`KnowledgeBaseFOL.refutation_is_decisive` says what a ``"refuted"`` is
2484
+ worth. An inconsistent knowledge base entails everything, in (c) as in the
2485
+ tableau; ``"unknown"`` means the backends gave no answer within budget
2486
+ (first-order entailment with transitivity is only semi-decidable), not that
2487
+ either answer holds.
2488
+
2489
+ Do NOT build these queries from the pieces by hand with
2490
+ ``Implies(tbox_to_fol(tbox), …)``: that drops the role box and can report a
2491
+ false counterexample (see the module docstring); :func:`tbox_to_fol` refuses
2492
+ such a TBox unless told ``concept_inclusions_only=True``.
2493
+
2494
+ **A knowledge base with a data layer** (a data property, a datatype or a
2495
+ literal anywhere in it, the ``query`` concepts included) additionally gets
2496
+ the data box's images and — per ``separation`` — the axioms that make it a
2497
+ two-sorted theory, all in ``side_axioms`` (see "The data layer" in the module
2498
+ docstring and :func:`data_sort_axioms`). What makes the question the
2499
+ prover answers the TWO-SORTED one:
2500
+
2501
+ * the premises: ``kb.premises`` / ``kb.tbox_premises`` / ``kb.axioms`` as
2502
+ above — they already contain the separation, the typing, the lattice and
2503
+ the literal facts; ``kb.formula`` already has every GCI relativised to
2504
+ the object domain;
2505
+ * the goal from the three methods, which relativise it the same way when
2506
+ ``kb.separation == "two-sorted"``; the consistency query
2507
+ ``Not(kb.formula)`` needs nothing extra.
2508
+
2509
+ The two-sorted image of a data layer is SOUND and not complete (see
2510
+ :attr:`KnowledgeBaseFOL.refutation_is_decisive`): ``"proved"`` transfers to
2511
+ OWL 2, ``"refuted"`` does not.
2512
+
2513
+ ``separation`` is ``"two-sorted"`` (the default: the full discipline),
2514
+ ``"data-lattice"`` (only the datatype facts — faithful WITHIN the data
2515
+ domain, no object/data separation, GCIs as written, so the premise count is
2516
+ that of the knowledge base plus its datatypes and literals) or ``"none"``
2517
+ (the data-box images alone, and the caller supplies any sort discipline).
2518
+ Neither of those two is sound for a knowledge base with a data layer:
2519
+ their inclusions are not restricted to the object domain, so they also
2520
+ range over data values and the premises are STRONGER than OWL 2's — the
2521
+ three goal methods refuse such a bundle, and a proof obtained from its
2522
+ premises by hand does not transfer.
2523
+ The effective choice is recorded in :attr:`KnowledgeBaseFOL.separation`.
2524
+ A knowledge base WITHOUT a data layer gets none of this, whatever
2525
+ ``separation`` says: its output is byte-identical to a release before the
2526
+ data layer existed.
2527
+
2528
+ Raises:
2529
+ ~unicode_logic_kit.dl.datatypes.UnsupportedDatatypeError:
2530
+ ``separation`` is not one of the three names;
2531
+ the knowledge base uses ``OwlThing``/``OwlData`` (the reserved
2532
+ guard predicates) as one of its own names; a literal has no term;
2533
+ or one name is used both as an object property and a data property
2534
+ or both as a class and a datatype — OWL 2 DL forbids both, and the
2535
+ image has ONE predicate per name, so it would conflate them and
2536
+ change what follows. Rename one of the two.
2537
+ TypeError: ``query`` is not an iterable of concepts (``query=C`` for one
2538
+ concept is a mistake: write ``query=[C]``).
2539
+
2540
+ A class, an object property and an individual that share a name (OWL 2 DL
2541
+ punning, which is allowed) are not refused and are not conflated: the image
2542
+ writes the class as the unary predicate ``A(x)``, the object property as the
2543
+ binary predicate ``A(x, y)`` and the individual as the constant ``A`` — three
2544
+ symbols. :func:`tbox_to_fol`, :func:`rbox_to_fol`, :func:`abox_to_fol` and
2545
+ :func:`databox_to_fol` write them the same way, so images of one ontology
2546
+ rendered box by box and conjoined are one theory. A route that keys a symbol
2547
+ on its kind and arity (the TPTP writers: Vampire, E) reads three symbols.
2548
+ Names that punning does NOT allow are refused — see :func:`check_kb_names`,
2549
+ which runs that check over boxes rendered separately.
2550
+
2551
+ The agreement with the tableau holds on the fragment it decides — ALCH with
2552
+ transitive roles, and qualified number restrictions on simple roles (the
2553
+ ``Count`` image). The translation itself is wider (inverse roles, nominals)
2554
+ and is then decided by the FOL backends alone.
2555
+ """
2556
+ _check_separation(separation)
2557
+ queried = _query_concepts(query, "dl.kb_to_fol")
2558
+ vocabulary = _collect_vocabulary(tbox, abox, where="dl.kb_to_fol", query=queried)
2559
+ _check_name_punning(vocabulary, "dl.kb_to_fol")
2560
+ effective = separation if vocabulary.has_data() else "none"
2561
+ if vocabulary.has_data():
2562
+ _check_reserved_names(vocabulary, separation)
2563
+ object_sort = effective == "two-sorted"
2564
+ # ONE avoid set for the whole call: every bound variable of every axiom
2565
+ # below — the GCI prefix variable, the role-box and data-box prefix
2566
+ # variables, the sort axioms', the minted ones — avoids every individual of
2567
+ # the knowledge base, so no binder anywhere in the image shares a name with
2568
+ # a constant and the image of one axiom never depends on which others
2569
+ # happen to be in the call (see _binder_names).
2570
+ avoid = vocabulary.individuals
2571
+ tbox_image = (_tbox_image(tbox, var, object_sort, avoid)
2572
+ if tbox is not None else _tautology())
2573
+ abox_image = _abox_image(abox, avoid) if abox is not None else _tautology()
2574
+ has_gcis = tbox is not None and bool(tbox.inclusions)
2575
+ # Derived from the axiom-kind table (ABox.is_empty), not from a hand-written
2576
+ # `or` over the assertion fields: an assertion kind this condition did not
2577
+ # know about would drop the ABox half of `formula` SILENTLY.
2578
+ has_assertions = abox is not None and not abox.is_empty()
2579
+ formulas = tuple(([tbox_image] if has_gcis else []) +
2580
+ ([abox_image] if has_assertions else []))
2581
+ side_axioms: List[SideAxiom] = []
2582
+ if tbox is not None:
2583
+ side_axioms.extend(_rbox_axioms(tbox, object_sort=object_sort, avoid=avoid))
2584
+ side_axioms.extend(_databox_axioms(tbox, avoid=avoid))
2585
+ if effective != "none":
2586
+ side_axioms.extend(_sort_axioms(vocabulary, effective))
2587
+ return KnowledgeBaseFOL(
2588
+ formula=_conjoin(list(formulas)),
2589
+ side_axioms=tuple(side_axioms),
2590
+ tbox=tbox_image,
2591
+ abox=abox_image,
2592
+ individuals=_abox_individuals(abox) if abox is not None else (),
2593
+ formulas=formulas,
2594
+ separation=effective,
2595
+ data_layer=vocabulary.has_data(),
2596
+ _coverage=_Coverage(
2597
+ var=var, vocabulary=vocabulary,
2598
+ individuals=frozenset(vocabulary.individuals),
2599
+ literals=frozenset(_literal_key(literal)
2600
+ for literal in vocabulary.literals)),
2601
+ )
2602
+
2603
+
2604
+ # --------------------------------------------------------------------------- #
2605
+ # Single-role concepts as propositional modal K formulas.
2606
+ # --------------------------------------------------------------------------- #
2607
+
2608
+ _IO_MODAL_MSG = (
2609
+ "concept_to_modal: {what} has no faithful propositional-K rendering — "
2610
+ "plain modal K has no converse modality (inverse roles need TENSE/hybrid "
2611
+ "logic) and no naming/nominal construct at all (that is HYBRID logic, a "
2612
+ "strictly different formalism this kit does not implement); refusing "
2613
+ "rather than silently dropping or misencoding it. Use concept_to_fol "
2614
+ "instead: the standard translation to FOL handles both faithfully (see "
2615
+ "its module docstring's 'Inverse roles and nominals (I, O)' section)."
2616
+ )
2617
+
2618
+
2619
+ def _roles_used(concept: Concept) -> set:
2620
+ """Return the set of distinct role names occurring anywhere in ``concept``."""
2621
+ if isinstance(concept, (Top, Bottom, Atomic)):
2622
+ return set()
2623
+ if isinstance(concept, Nominal):
2624
+ raise NotImplementedError(_IO_MODAL_MSG.format(what=f"the nominal {{{concept.individual}}}"))
2625
+ if isinstance(concept, DATA_CONCEPTS):
2626
+ raise NotImplementedError(
2627
+ f"concept_to_modal: the data restriction {type(concept).__name__} "
2628
+ f"({concept.to_unicode()}) has no propositional-K rendering — modal K "
2629
+ f"has no data domain, only possible worlds. Use concept_to_fol, whose "
2630
+ f"image handles it (see its 'The data layer' section).")
2631
+ if isinstance(concept, Not):
2632
+ return _roles_used(concept.concept)
2633
+ if isinstance(concept, (And, Or)):
2634
+ return _roles_used(concept.left) | _roles_used(concept.right)
2635
+ if isinstance(concept, (Exists, ForAll)):
2636
+ if isinstance(concept.role, InverseRole):
2637
+ raise NotImplementedError(
2638
+ _IO_MODAL_MSG.format(what=f"the inverse role {concept.role.role}⁻"))
2639
+ # A built-in property name is not "the one accessibility relation" of K
2640
+ # (the universal relation is a different modality, the empty one has no
2641
+ # worlds to move to): refused by name, as every other route does.
2642
+ _reject_concept_role(concept, where="dl.concept_to_modal")
2643
+ return {concept.role} | _roles_used(concept.concept)
2644
+ raise TypeError(f"concept_to_modal: unsupported concept {type(concept).__name__}")
2645
+
2646
+
2647
+ # A reserved nullary atom used only to spell out a structural tautology/
2648
+ # contradiction (mirrors the reserved-atom idiom in unicode_logic_kit.atp.fitch's
2649
+ # falsum handling). Since `p ∨ ¬p` is valid — and `p ∧ ¬p` unsatisfiable — for
2650
+ # ANY p, this is correct regardless of what this particular name denotes, even
2651
+ # in the (harmless) case that a concept happens to be literally named "⊤".
2652
+ _MODAL_TRUE_ATOM = Atom("⊤", ())
2653
+
2654
+
2655
+ def _to_modal(concept: Concept) -> Node:
2656
+ """Translate a single-role concept to a propositional modal-K formula."""
2657
+ if isinstance(concept, Atomic):
2658
+ return Atom(concept.name, ())
2659
+ if isinstance(concept, Top):
2660
+ return _FOr(_MODAL_TRUE_ATOM, _FNot(_MODAL_TRUE_ATOM))
2661
+ if isinstance(concept, Bottom):
2662
+ return _FAnd(_MODAL_TRUE_ATOM, _FNot(_MODAL_TRUE_ATOM))
2663
+ if isinstance(concept, Not):
2664
+ return _FNot(_to_modal(concept.concept))
2665
+ if isinstance(concept, And):
2666
+ return _FAnd(_to_modal(concept.left), _to_modal(concept.right))
2667
+ if isinstance(concept, Or):
2668
+ return _FOr(_to_modal(concept.left), _to_modal(concept.right))
2669
+ if isinstance(concept, Exists):
2670
+ return Diamond(_to_modal(concept.concept))
2671
+ if isinstance(concept, ForAll):
2672
+ return Box(_to_modal(concept.concept))
2673
+ raise TypeError(f"concept_to_modal: unsupported concept {type(concept).__name__}")
2674
+
2675
+
2676
+ _MULTI_ROLE_MSG = (
2677
+ "concept_to_modal: concept uses {n} distinct roles {roles}; propositional "
2678
+ "modal K has exactly ONE accessibility relation, so per-role restrictions "
2679
+ "(∃r.C / ∀r.C for different r) cannot be faithfully rendered as Box/Diamond "
2680
+ "— there is no honest way to recover which role a given □/◇ came from. Use "
2681
+ "concept_to_fol instead: it has no such limitation, since a role is just "
2682
+ "another binary FOL predicate r(x, y) and FOL scales to any number of them."
2683
+ )
2684
+
2685
+
2686
+ def concept_to_modal(concept: Concept) -> Node:
2687
+ """Translate a SINGLE-role concept to a propositional modal-K formula.
2688
+
2689
+ ``∃r.C`` ↦ ``◇C``, ``∀r.C`` ↦ ``□C`` (ALC restricted to one role is
2690
+ exactly modal K — see "Multi-role concepts and modal K" in the module
2691
+ docstring). ``concept`` is satisfiable (:func:`unicode_logic_kit.dl.tableau.concept_satisfiable`)
2692
+ iff its image here is satisfiable in K, decidable via
2693
+ :func:`unicode_logic_kit.atp.modal_tableau.is_modal_valid` on the negation
2694
+ (``not is_modal_valid(Not(concept_to_modal(concept)), frame="K")``).
2695
+
2696
+ Raises:
2697
+ NotImplementedError: ``concept`` mentions two or more distinct role
2698
+ names — see :func:`concept_to_fol` for the general (FOL) translation.
2699
+ """
2700
+ roles = _roles_used(concept)
2701
+ if len(roles) > 1:
2702
+ raise NotImplementedError(
2703
+ _MULTI_ROLE_MSG.format(n=len(roles), roles=sorted(roles)))
2704
+ return _to_modal(concept)