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,794 @@
1
+ """A second, independent external OWL 2 DL reasoner route — over Hets/Docker,
2
+ for the ALCHQ + I + O fragment (inverse roles, nominals) — via the ``Fact``
3
+ prover the ``spechub2/hets:latest`` image bundles.
4
+
5
+ This is C43 ("OWL2-DL reasoner fallback via Hets") as actually built, after a
6
+ live Phase-0 capability spike (see "Phase 0 spike findings" below) and after
7
+ re-checking the roadmap item's own claims against the code that shipped
8
+ since it was written (see "Deviations from the roadmap spec" below). The
9
+ short version: :mod:`unicode_logic_kit.dl.owl_reasoner` (added by C43's own
10
+ sibling item, N5) already gives this kit a full ALCHQ+I+O reasoner
11
+ (HermiT via ``owlready2``) and a full-ontology OWL 2 Functional-Style Syntax
12
+ writer (:mod:`unicode_logic_kit.dl.owl_functional`, ALCHQ only) — so the ONLY
13
+ thing left for a Hets route to add is a SECOND, INDEPENDENT oracle: a
14
+ different reasoner (FaCT++, not HermiT), reached a different way (a Docker
15
+ container's REST API, not an in-process JVM binding), so an agreement
16
+ between the two is not just one implementation checking itself twice.
17
+
18
+ Phase 0 spike findings (live, 2026-09-18, against the exact digest
19
+ ``hets/docker.py`` already pins — see that module's own "Container image
20
+ quirks" section, which this module's findings were folded back into)
21
+ ------------------------------------------------------------------------
22
+
23
+ 1. The image DOES parse OWL 2: uploading a ``.omn`` (Manchester) or ``.ofn``
24
+ (Functional-Style) file makes ``GET /dg/<iri>?format=json`` report
25
+ ``"logic": "OWL"`` for the resulting development-graph node, alongside
26
+ CASL nodes.
27
+ 2. ``GET /provers/<iri>?format=json`` for an OWL-typed file lists
28
+ ``Fact, eprover, darwin, darwin-non-fd, Vampire, MathServeBroker, SPASS,
29
+ EProver, Darwin`` — **no** ``Pellet``, **no** ``HermiT``. Only ``Fact``
30
+ (FaCT++, confirmed live inside the image: ``/usr/lib/hets/hets-owl-tools/
31
+ lib/uk.ac.manchester.cs.owl.factplusplus-P5.0-v1.6.3.1.jar`` plus its
32
+ native ``libFaCTPlusPlusJNI.so``, invoked as its own ``java`` subprocess by
33
+ the Hets server, not by this Python process) is a genuine DL reasoner; the
34
+ rest are the FOL provers :mod:`unicode_logic_kit.hets.docker`'s docstring
35
+ already catalogues (some broken there too), reachable for OWL input only
36
+ through a translation chain (``OWL22CASL:...``) that this module does NOT
37
+ use — see point 3.
38
+ 3. **Leaving ``reasoner`` unset (Hets's own default) is broken for OWL
39
+ input**: ``POST /consistency-check`` with no explicit reasoner picks an
40
+ ``OWL22CASL:CASL2SoftFOL``-family translation and then crashes outright —
41
+ live-observed: ``*** Error: SuleCFOL2SoftFOL.transPREDSYMB: unknown pred:
42
+ Qual_pred_name owl_uNothing ...`` — on an ontology as simple as one class
43
+ asserted a subclass of ``owl:Nothing``. This module therefore ALWAYS
44
+ passes ``reasoner="Fact"`` explicitly (see :data:`_REASONER`) and never
45
+ lets Hets choose, unlike :mod:`unicode_logic_kit.atp.hets_backend`'s FOL
46
+ route (where an unset reasoner is fine and used).
47
+ 4. With ``reasoner="Fact"`` explicit, both directions verified live and
48
+ correctly: an ontology whose ABox is genuinely inconsistent (an
49
+ individual in two disjoint classes) answers ``"Inconsistent"`` with FaCT++
50
+ provenance in ``prover_output``; a merely-unsatisfiable-but-unused named
51
+ class (``C ⊑ owl:Nothing`` with no individual ever asserted into ``C``)
52
+ correctly still answers ``"Consistent"`` (an empty class does not make the
53
+ KB inconsistent) — i.e. this is not a reasoner that rubber-stamps
54
+ "Consistent" the way the broken FOL provers rubber-stamp ``"Open"``
55
+ (see :mod:`unicode_logic_kit.hets.docker`'s docstring for that precedent).
56
+ A third result, ``"Timeout"``, is also live-confirmed (forced with
57
+ ``timeLimit: 0``) — treated exactly like an unrecognised result: this
58
+ module raises rather than guessing (see :class:`HetsOwlError`).
59
+ 5. **No AGPL callout is needed.** The roadmap spec's Phase-0 instruction was
60
+ conditional: add one "if a Pellet-backed route is what's actually live".
61
+ It is not — the only live DL reasoner is FaCT++, whose kernel is
62
+ **LGPL-2.1** (confirmed: FaCT++'s own project page,
63
+ http://owl.cs.manchester.ac.uk/tools/fact/, and the FSF Free Software
64
+ Directory both state LGPL for the reasoner kernel), and this kit reaches
65
+ it over the same REST/Docker network boundary
66
+ :mod:`unicode_logic_kit.hets` already draws for the whole subpackage (see
67
+ its module docstring's "Architecture" section) — no different in kind
68
+ from the SPASS/darwin route :mod:`unicode_logic_kit.atp.hets_backend`
69
+ already ships uncommented-on. This is a genuine, recorded deviation from
70
+ the roadmap spec's ``description``/``dependencies`` fields, which framed
71
+ the AGPL callout as likely needed; it turned out not to apply.
72
+ 6. A differential battery against BOTH existing routes — :mod:`unicode_logic_kit.dl.tableau`
73
+ on ALCHQ (satisfiability, subsumption, the RBox, qualified number
74
+ restrictions) and :mod:`unicode_logic_kit.dl.owl_reasoner` on the I/O
75
+ fragment it alone decides — passed 21/21 live during this spike, reusing
76
+ the SAME hand-checked fixtures ``tests/test_owl_reasoner.py`` already
77
+ uses (see ``tests/test_hets_owl.py``, this module's test file, for the
78
+ shipped, pytest-integrated version of that spike).
79
+
80
+ Deviations from the roadmap spec
81
+ ----------------------------------
82
+ * **No ``dl/owl_export.py``.** The spec's Phase 1 asked for one; it does not
83
+ exist because N5 already shipped the equivalent capability twice over —
84
+ :func:`unicode_logic_kit.dl.owl_functional.to_owl_functional` (ALCHQ
85
+ TBox+ABox -> OWL 2 Functional-Style Syntax text) and
86
+ :mod:`unicode_logic_kit.dl.owl_reasoner` (the full ALCHQ+I+O reasoner
87
+ itself). This module does not even reuse ``to_owl_functional`` for its own
88
+ rendering, though — see "Why this module renders its own OWL text" below.
89
+ * **No ``DLHetsBackend`` class / ``atp.protocol.Verdict`` mapping.** The
90
+ spec's Phase 2 sketch mirrored :class:`unicode_logic_kit.atp.hets_backend.HetsBackend`'s
91
+ ``ProverBackend`` shape. This module instead mirrors
92
+ :mod:`unicode_logic_kit.dl.owl_reasoner`'s shape (free functions returning
93
+ plain ``bool``/``Set``/``List``/``Dict``, one per :mod:`unicode_logic_kit.dl.tableau`
94
+ namesake, raising rather than returning an error-carrying envelope) — DL
95
+ reasoning is not part of the ``atp`` FOL-prover registry (:mod:`unicode_logic_kit.dl`
96
+ already draws that line: even ``owl_reasoner`` does not implement
97
+ ``ProverBackend``), and the differential-testing story this task's own
98
+ ``test_oracle`` asks for ("agree exactly with dl.tableau ... and with
99
+ dl.owl_reasoner") is far more direct when all three routes share one
100
+ calling convention.
101
+ * **No general ALCHQ FOL fallback via Hets.** The roadmap's own
102
+ ``existing_coverage`` field already notes this exists in a different
103
+ shape: :func:`unicode_logic_kit.dl.translate.concept_to_fol` feeds ALCHQ
104
+ into the WHOLE ``atp`` backend stable, including
105
+ :class:`unicode_logic_kit.atp.hets_backend.HetsBackend` itself (SPASS via
106
+ CASL). This module adds nothing there; its only reason to exist is the
107
+ I/O fragment neither the in-house tableau nor that FOL route can decide,
108
+ PLUS a second, differently-implemented DL reasoner for the ALCHQ fragment
109
+ as a cross-check.
110
+
111
+ Why this module renders its own OWL text (not ``dl.owl_functional``'s)
112
+ --------------------------------------------------------------------------
113
+ :mod:`unicode_logic_kit.dl.owl_functional` is deliberately ALCHQ-only — its
114
+ ``to_owl_functional``/``to_owl_functional_class_expression`` RAISE
115
+ ``TypeError`` on an :class:`~unicode_logic_kit.dl.concepts.InverseRole`-valued
116
+ role or a :class:`~unicode_logic_kit.dl.concepts.Nominal` (see that module's
117
+ own docstring), because ALCHQ-only round-tripping is its whole contract.
118
+ This module needs the FULL ALCHQ+I+O fragment in TEXT form (Hets's REST API
119
+ only takes uploaded file text, unlike ``owlready2``'s Python object API,
120
+ which is why :mod:`unicode_logic_kit.dl.owl_reasoner` could sidestep this
121
+ exact problem by never going through text at all — see that module's own
122
+ "Translation route" section). So :func:`_render_document` is a second,
123
+ small, ALCHQ+I+O-complete OWL 2 Functional-Style Syntax renderer, written
124
+ fresh rather than extending ``owl_functional.py`` (out of this task's file
125
+ ownership, and extending its ALCHQ-only contract would be the wrong change
126
+ for a module whose entire point is "round-trips exactly ALCHQ, nothing
127
+ more"). It reuses that module's naming-and-precedence-free INSIGHT
128
+ (``Keyword(arg arg ...)`` needs no precedence table — see
129
+ ``owl_functional.py``'s "No ambiguity, no precedence" section) but not its
130
+ code, and it takes a different, simpler approach to names than either
131
+ sibling module:
132
+
133
+ Names: fresh synthetic local names, never round-tripped, exactly like
134
+ ``dl.owl_reasoner``'s ``_Ctx``
135
+ -----------------------------------------------------------------------
136
+ OWL 2 Functional-Style Syntax has no "bare word" name token — every name is
137
+ either a full ``<IRI>`` or a *prefixed* name (``PNAME_LN``, e.g. ``:Doctor``
138
+ or ``ex:Doctor``); a bare ``Doctor`` with no prefix and no angle brackets is
139
+ a PARSE ERROR (live-confirmed: the OWL API's functional-syntax parser
140
+ reports ``Encountered " <PN_LOCAL> "Doctor "" ... Was expecting ... <PNAME_LN>``).
141
+ This kit's own :class:`~unicode_logic_kit.dl.concepts.Atomic`/role/individual
142
+ names are arbitrary opaque strings (may contain the kit's own Unicode DL
143
+ glyphs, whitespace, parentheses, ...) with no such restriction, and — just
144
+ as in ``dl.owl_reasoner``'s identical situation — every question this
145
+ module answers is a yes/no (or a `Set`/`List` of the CALLER's OWN concept
146
+ objects, for realize/retrieval — never "what is this entity called back"),
147
+ so there is nothing to lose by never sending the original name through
148
+ Hets's OWL/IRI machinery at all. :class:`_NameMap` allocates one fresh
149
+ ``:C<n>``/``:R<n>``/``:I<n>`` PNAME_LN token per distinct kit name on first
150
+ use (a bare ``:`` DEFAULT-prefixed local name — declared once, via
151
+ ``Prefix(:=<...>)``, per uploaded document — is a valid PNAME_LN and needs
152
+ no per-name IRI-safety reasoning at all, unlike ``owl_functional.py``'s
153
+ ``to_owl_functional``, which instead renders a kit name back VERBATIM,
154
+ bracketed as a full IRI when it contains ``"://"`` — that module's contract
155
+ is "faithful round-trip of THIS name", this module's is "a yes/no verdict
156
+ Hets can compute", and the synthetic-name approach is simpler because it
157
+ does not need to be).
158
+
159
+ Wire fact this module relies on: the development-graph node name Hets
160
+ assigns an uploaded OWL file is exactly the ``Ontology(<iri> ...)`` IRI
161
+ declared inside it (live-confirmed against every probe file used in this
162
+ spike) — so :func:`_render_document` always declares the SAME fixed
163
+ :data:`_ONTOLOGY_IRI`, and every :meth:`~unicode_logic_kit.hets.client.HetsClient.consistency_check`
164
+ call in this module passes that same constant as ``node``, with no per-call
165
+ ``GET /dg`` round trip needed to discover it (contrast
166
+ :mod:`unicode_logic_kit.hets.bridge`, which DOES need that round trip, because
167
+ its CASL input's node name is caller-chosen, not a constant this module
168
+ controls).
169
+
170
+ The data layer
171
+ ----------------
172
+ FaCT++ is an SROIQ(D) reasoner, so unlike ``dl.owl_reasoner`` (whose HermiT
173
+ bridge refuses the data layer by name) THIS oracle can be asked about it:
174
+ :func:`_render_document` writes the data box and the data assertions, with
175
+ OWL 2's own datatype and facet names verbatim (``xsd:integer`` means the same
176
+ to FaCT++ as to us) and the same fresh synthetic names for data properties
177
+ (``:P<n>``) and user-defined datatypes (``:T<n>``) it gives classes and roles --
178
+ a LITERAL's datatype included: ``"5"^^:T1``, never the kit's own datatype name,
179
+ which the document does not declare.
180
+ The ``xsd``/``rdfs``/``rdf`` prefixes are declared only for a knowledge base
181
+ that mentions a datatype, so every data-free document is unchanged. This is an
182
+ independent check on the kit's own two-sorted FOL image, not a replacement for
183
+ it; no test here starts the container.
184
+
185
+ The one primitive: :func:`_kb_consistent`
186
+ --------------------------------------------
187
+ Every public ``external_*`` function reduces to it, exactly the way
188
+ ``dl.owl_reasoner``'s whole public API reduces to its own ``_kb_consistent``,
189
+ and ``dl.tableau``'s to ``_solve`` — see "What each function decides" in
190
+ ``dl.owl_reasoner``'s module docstring, which this module's reduction chain
191
+ mirrors call-for-call (``external_subsumes`` reduces to
192
+ ``external_concept_satisfiable`` exactly the way ``subsumes`` reduces to
193
+ ``concept_satisfiable``, and so on).
194
+
195
+ Opt-in, never in a default chain
196
+ -----------------------------------
197
+ Exactly :mod:`unicode_logic_kit.atp.hets_backend`'s cost-model discipline: a
198
+ Docker container start is minutes-expensive, so this module NEVER starts one
199
+ (:func:`hets_owl_available` and every ``external_*`` function only ever call
200
+ ``discover_hets_url(start_container=False)``) and is never wired into any
201
+ automatic reasoning chain — a caller opts in by importing this module and
202
+ calling it directly, the same way ``dl.owl_reasoner`` is opt-in.
203
+
204
+ Public API: :func:`hets_owl_available`, :func:`external_concept_satisfiable`,
205
+ :func:`external_concept_unsatisfiable`, :func:`external_subsumes`,
206
+ :func:`external_equivalent`, :func:`external_abox_consistent`,
207
+ :func:`external_instance_check`, :func:`external_instance_retrieval`,
208
+ :func:`external_realize`, :func:`external_realize_all`, :class:`HetsOwlError`.
209
+ """
210
+
211
+ from typing import Dict, List, Optional, Set
212
+
213
+ from ..dl.concepts import (
214
+ Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
215
+ InverseRole, Nominal, HasValue, DataExists, DataForAll, DataHasValue,
216
+ DataAtLeast, DataAtMost,
217
+ )
218
+ from ..dl.datatypes import (
219
+ DataRange, Literal, is_builtin_datatype, render_datarange_fs, render_literal_fs,
220
+ )
221
+ from ..dl.tableau import (
222
+ TBox, ABox, _abox_individual_names, _reject_abox_roles,
223
+ _reject_concept_roles_deep, _tbox_class_expressions, _validate_data_box,
224
+ _validate_role_box,
225
+ )
226
+ from ..dl.translate import _check_name_punning, _collect_vocabulary
227
+ from .client import HetsClient
228
+ from .docker import discover_hets_url, hets_available
229
+
230
+ __all__ = [
231
+ "hets_owl_available",
232
+ "external_concept_satisfiable", "external_concept_unsatisfiable",
233
+ "external_subsumes", "external_equivalent",
234
+ "external_abox_consistent", "external_instance_check", "external_instance_retrieval",
235
+ "external_realize", "external_realize_all",
236
+ "HetsOwlError",
237
+ ]
238
+
239
+ #: The only prover identifier this module ever passes — see the module
240
+ #: docstring's "Phase 0 spike findings" point 3 for why an unset reasoner
241
+ #: (Hets's own default choice) is broken for OWL input, and point 2 for why
242
+ #: no other listed identifier is a genuine DL reasoner in this image.
243
+ _REASONER = "Fact"
244
+
245
+ #: Fixed ontology IRI every uploaded probe document declares — also the
246
+ #: development-graph node name passed to ``consistency_check`` (see the
247
+ #: module docstring's "Wire fact this module relies on").
248
+ _ONTOLOGY_IRI = "http://unicode-logic-kit.invalid/hets-owl-probe"
249
+
250
+ #: The default-prefix IRI backing every synthetic ``:C<n>``/``:R<n>``/``:I<n>``
251
+ #: PNAME_LN token (see the module docstring's "Names" section). Never
252
+ #: resolved or dereferenced — it exists only to make the ``Prefix(:=<...>)``
253
+ #: declaration well-formed.
254
+ _PREFIX_IRI = "http://unicode-logic-kit.invalid/owl#"
255
+
256
+ _FILENAME = "kit_owl_probe.ofn"
257
+
258
+
259
+ class HetsOwlError(RuntimeError):
260
+ """A reachable Hets server could not decide a knowledge base: FaCT++
261
+ reported ``"Timeout"`` (see the module docstring's point 4), an
262
+ unrecognised result string, or a goal count other than one (this module
263
+ always builds a single-goal ``consistency-check`` request, so anything
264
+ else means the server answered a different question than asked — the
265
+ same "infra" discipline :meth:`unicode_logic_kit.atp.hets_backend.HetsBackend.decide`
266
+ already applies to its own single-conjecture case). Deliberately a plain
267
+ ``RuntimeError`` (not :class:`~unicode_logic_kit.atp.protocol.BackendUnavailable`,
268
+ which every ``external_*`` function here still raises UNCHANGED, via
269
+ :func:`~unicode_logic_kit.hets.docker.discover_hets_url`, when no server is
270
+ reachable at all): the server answering *something* unusable is a
271
+ different failure mode than there being no server to ask, and callers
272
+ that already special-case ``BackendUnavailable`` for "no Hets right now"
273
+ must not have that same handling silently swallow "Hets is up but this
274
+ query broke it".
275
+ """
276
+
277
+
278
+ def hets_owl_available() -> bool:
279
+ """True iff a Hets server answers right now — the DL/OWL route's twin of
280
+ :func:`unicode_logic_kit.hets.docker.hets_available`.
281
+
282
+ Literally delegates to it (same discovery, same never-start-a-container
283
+ discipline): the "Fact" reasoner this module always requests is a fixed
284
+ property of the digest-pinned image every part of :mod:`unicode_logic_kit.hets`
285
+ already assumes (see :data:`unicode_logic_kit.hets.docker.HETS_IMAGE`), not
286
+ something that needs its own per-call ``GET /provers`` re-verification —
287
+ mirrors how :mod:`unicode_logic_kit.hets.docker`'s own docstring documents
288
+ which FOL provers work in this image as a static fact, not a live probe.
289
+ """
290
+ return hets_available()
291
+
292
+
293
+ # --------------------------------------------------------------------------- #
294
+ # Rendering: Concept/TBox/ABox -> OWL 2 Functional-Style Syntax text, over the
295
+ # FULL ALCHQ + I + O fragment (see the module docstring's "Why this module
296
+ # renders its own OWL text" and "Names" sections).
297
+ # --------------------------------------------------------------------------- #
298
+
299
+ class _NameMap:
300
+ """Per-call allocator of fresh, synthetic PNAME_LN tokens (``:C1``, ``:R1``,
301
+ ``:I1``, ...) for this kit's class/role/individual names — never the kit's
302
+ own name string verbatim (see the module docstring's "Names" section).
303
+ """
304
+
305
+ def __init__(self):
306
+ self._classes: Dict[str, str] = {}
307
+ self._roles: Dict[str, str] = {}
308
+ self._individuals: Dict[str, str] = {}
309
+ self._data_properties: Dict[str, str] = {}
310
+ self._datatypes: Dict[str, str] = {}
311
+
312
+ def cls(self, name: str) -> str:
313
+ if name not in self._classes:
314
+ self._classes[name] = f":C{len(self._classes) + 1}"
315
+ return self._classes[name]
316
+
317
+ def role(self, name: str) -> str:
318
+ if name not in self._roles:
319
+ self._roles[name] = f":R{len(self._roles) + 1}"
320
+ return self._roles[name]
321
+
322
+ def ind(self, name: str) -> str:
323
+ if name not in self._individuals:
324
+ self._individuals[name] = f":I{len(self._individuals) + 1}"
325
+ return self._individuals[name]
326
+
327
+ def dprop(self, name: str) -> str:
328
+ """A fresh ``:P<n>`` token for a DATA property name."""
329
+ if name not in self._data_properties:
330
+ self._data_properties[name] = f":P{len(self._data_properties) + 1}"
331
+ return self._data_properties[name]
332
+
333
+ def dtype(self, name: str) -> str:
334
+ """A datatype name as OWL 2 text: a BUILT-IN datatype keeps its own
335
+ name (``xsd:integer`` means the same to Hets as to us), a user-defined
336
+ one gets a fresh ``:T<n>`` token."""
337
+ if is_builtin_datatype(name):
338
+ return name
339
+ if name not in self._datatypes:
340
+ self._datatypes[name] = f":T{len(self._datatypes) + 1}"
341
+ return self._datatypes[name]
342
+
343
+ def class_tokens(self) -> List[str]:
344
+ return list(self._classes.values())
345
+
346
+ def role_tokens(self) -> List[str]:
347
+ return list(self._roles.values())
348
+
349
+ def individual_tokens(self) -> List[str]:
350
+ return list(self._individuals.values())
351
+
352
+ def data_property_tokens(self) -> List[str]:
353
+ return list(self._data_properties.values())
354
+
355
+ def datatype_tokens(self) -> List[str]:
356
+ return list(self._datatypes.values())
357
+
358
+
359
+ def _render_datarange(datarange: DataRange, names: _NameMap) -> str:
360
+ """A data range as OWL 2 Functional-Style text, with user-defined datatypes
361
+ under their synthetic names."""
362
+ return render_datarange_fs(datarange, names.dtype)
363
+
364
+
365
+ def _render_literal(literal: Literal, names: _NameMap) -> str:
366
+ """A literal as OWL 2 Functional-Style text, its datatype under the SAME
367
+ name :func:`_render_datarange` gives it: a user-defined datatype is declared
368
+ as ``:T<n>``, so a literal written with the kit's own name would use a name
369
+ the document never declared."""
370
+ return render_literal_fs(literal, names.dtype)
371
+
372
+
373
+ def _render_role_expr(role_field, names: _NameMap) -> str:
374
+ """Render a ``role`` field (a plain role name, or an :class:`InverseRole`)
375
+ as an OWL 2 ``ObjectPropertyExpression`` — the one place this renderer
376
+ covers a construct ``dl.owl_functional`` refuses outright (see the
377
+ module docstring).
378
+ """
379
+ if isinstance(role_field, InverseRole):
380
+ return f"ObjectInverseOf({names.role(role_field.role)})"
381
+ return names.role(role_field)
382
+
383
+
384
+ def _render_ce(c: Concept, names: _NameMap) -> str:
385
+ """Render ``c`` as an OWL 2 Functional-Style ``ClassExpression``, over the
386
+ full ALCHQ + I + O fragment (see :mod:`unicode_logic_kit.dl.concepts` for
387
+ the AST this mirrors, and the module docstring for why this duplicates a
388
+ small amount of ``dl.owl_functional._render_ce``-shaped logic rather than
389
+ reusing it).
390
+ """
391
+ if isinstance(c, Top):
392
+ return "owl:Thing"
393
+ if isinstance(c, Bottom):
394
+ return "owl:Nothing"
395
+ if isinstance(c, Atomic):
396
+ return names.cls(c.name)
397
+ if isinstance(c, Nominal):
398
+ return f"ObjectOneOf({names.ind(c.individual)})"
399
+ if isinstance(c, HasValue):
400
+ return (f"ObjectHasValue({_render_role_expr(c.role, names)} "
401
+ f"{names.ind(c.individual)})")
402
+ if isinstance(c, Not):
403
+ return f"ObjectComplementOf({_render_ce(c.concept, names)})"
404
+ if isinstance(c, And):
405
+ return f"ObjectIntersectionOf({_render_ce(c.left, names)} {_render_ce(c.right, names)})"
406
+ if isinstance(c, Or):
407
+ return f"ObjectUnionOf({_render_ce(c.left, names)} {_render_ce(c.right, names)})"
408
+ if isinstance(c, Exists):
409
+ return f"ObjectSomeValuesFrom({_render_role_expr(c.role, names)} {_render_ce(c.concept, names)})"
410
+ if isinstance(c, ForAll):
411
+ return f"ObjectAllValuesFrom({_render_role_expr(c.role, names)} {_render_ce(c.concept, names)})"
412
+ if isinstance(c, AtLeast):
413
+ return f"ObjectMinCardinality({c.n} {_render_role_expr(c.role, names)} {_render_ce(c.concept, names)})"
414
+ if isinstance(c, AtMost):
415
+ return f"ObjectMaxCardinality({c.n} {_render_role_expr(c.role, names)} {_render_ce(c.concept, names)})"
416
+ if isinstance(c, DataExists):
417
+ return f"DataSomeValuesFrom({names.dprop(c.prop)} {_render_datarange(c.datarange, names)})"
418
+ if isinstance(c, DataForAll):
419
+ return f"DataAllValuesFrom({names.dprop(c.prop)} {_render_datarange(c.datarange, names)})"
420
+ if isinstance(c, DataHasValue):
421
+ return f"DataHasValue({names.dprop(c.prop)} {_render_literal(c.value, names)})"
422
+ if isinstance(c, DataAtLeast):
423
+ return (f"DataMinCardinality({c.n} {names.dprop(c.prop)} "
424
+ f"{_render_datarange(c.datarange, names)})")
425
+ if isinstance(c, DataAtMost):
426
+ return (f"DataMaxCardinality({c.n} {names.dprop(c.prop)} "
427
+ f"{_render_datarange(c.datarange, names)})")
428
+ raise TypeError(f"hets.owl_backend: unsupported concept {type(c).__name__}")
429
+
430
+
431
+ #: ``(TBox field, OWL 2 keyword)`` for the seven one-role characteristic
432
+ #: axioms, in the order ``dl.tableau._AXIOM_KINDS`` lists them. A table rather
433
+ #: than seven loops, for the same reason ``dl.owl_functional`` uses one: they
434
+ #: differ only in the keyword, and a FaCT++ oracle that silently dropped one
435
+ #: would agree with the in-house tableau for the wrong reason.
436
+ #: ``transitive_roles`` is NOT here: a transitivity declaration is rendered
437
+ #: with the role inclusions, ahead of everything else, which is the ordering
438
+ #: this module has had since the RBox landed and which several pinned
439
+ #: expected-document strings depend on.
440
+ _CHARACTERISTIC_AXIOMS = (
441
+ ("asymmetric_roles", "AsymmetricObjectProperty"),
442
+ ("irreflexive_roles", "IrreflexiveObjectProperty"),
443
+ ("functional_roles", "FunctionalObjectProperty"),
444
+ ("symmetric_roles", "SymmetricObjectProperty"),
445
+ ("reflexive_roles", "ReflexiveObjectProperty"),
446
+ ("inverse_functional_roles", "InverseFunctionalObjectProperty"),
447
+ )
448
+
449
+
450
+ def _guard_document(tbox: TBox, abox: ABox, concepts=()) -> None:
451
+ """Refuse, by name, what no route may answer, BEFORE a word of the document
452
+ is written.
453
+
454
+ A renderer gives every name it meets a synthetic one of its own (``:R1``), so
455
+ an OWL 2 built-in property name (``owl:topObjectProperty`` and the other
456
+ three, and ``=`` / ``≠``) as the role of a class expression came out as an
457
+ ordinary role, and FaCT++ was asked about a different restriction — while
458
+ every other route refuses the same concept by name. These are the validations
459
+ :func:`unicode_logic_kit.dl.owl_reasoner._guard_inputs` runs for the HermiT
460
+ route, CALLED here and not copied, so one expression is refused with one
461
+ message wherever it is asked: the stored role box and data box are validated
462
+ before anything is unpacked (a hand-built wrong-arity pair is a
463
+ :class:`~unicode_logic_kit.dl.tableau.RoleExpressionError`, not a bare
464
+ ``ValueError``), no ABox assertion carries a built-in property name, and no
465
+ class expression — in a TBox inclusion, in a domain or range filler, in a
466
+ DATA property domain filler (a class expression this route renders; the
467
+ HermiT route refuses the whole data box instead), in an ABox concept
468
+ assertion (which is where every QUERY concept ends up) — uses one as a role,
469
+ at any depth. A NAME used as both an object property and a data property, or
470
+ as both a class and a datatype, is refused too
471
+ (:func:`~unicode_logic_kit.dl.translate._check_name_punning`, the one the FOL
472
+ image runs): this renderer would write the two kinds under separate tokens
473
+ (``:R1`` / ``:P1``), so the oracle would answer a knowledge base the kit's own
474
+ route refuses. Not run here: the data-layer refusal (FaCT++ is an SROIQ(D)
475
+ reasoner and IS asked about the data layer) and the simple-role check.
476
+
477
+ ``concepts`` are query concepts that are NOT yet in the ABox: an entry point
478
+ that may make no Hets call at all (``external_realize`` over an empty
479
+ vocabulary, ``external_realize_all`` and ``external_instance_retrieval`` over
480
+ an empty ABox) runs the guard itself, as ``dl.owl_reasoner`` does, so that it
481
+ does not return a quiet ``[]`` for a knowledge base every other route refuses.
482
+ """
483
+ where = "hets.owl_backend"
484
+ _validate_role_box(tbox, where=where)
485
+ _validate_data_box(tbox, where=where)
486
+ _reject_abox_roles(abox, where=where)
487
+ for concept in _tbox_class_expressions(tbox):
488
+ _reject_concept_roles_deep(concept, where=where)
489
+ for _prop, filler in tbox.data_property_domains:
490
+ _reject_concept_roles_deep(filler, where=where)
491
+ for _individual, concept in abox.concept_assertions:
492
+ _reject_concept_roles_deep(concept, where=where)
493
+ for concept in concepts:
494
+ _reject_concept_roles_deep(concept, where=where)
495
+ # Over the whole knowledge base, which includes every query concept (they
496
+ # reach this renderer as ABox assertions).
497
+ _check_name_punning(_collect_vocabulary(tbox, abox), where)
498
+
499
+
500
+ def _render_document(tbox: TBox, abox: ABox) -> str:
501
+ """Render ``(tbox, abox)`` as one OWL 2 Functional-Style ``Ontology(...)``
502
+ document under the fixed :data:`_ONTOLOGY_IRI`, with a ``Declaration(...)``
503
+ for every synthetic name used (mirrors ``dl.owl_functional.to_owl_functional``'s
504
+ own declaration-block convention, for the same reason: cheap, harmless,
505
+ and matches what a hand-written OWL ontology looks like) — the whole role
506
+ box first, then the TBox's inclusions, then the ABox's assertions, matching
507
+ that sibling module's own axiom ordering.
508
+
509
+ EVERY role-box field is rendered, including the ones
510
+ ``dl.owl_functional`` cannot read back (an ``ObjectInverseOf`` on either
511
+ side of a sub-property axiom): this module exists to be a SECOND,
512
+ INDEPENDENT oracle, and an oracle that drops the axiom under test agrees
513
+ with everything. What no route answers is refused first
514
+ (:func:`_guard_document`).
515
+ """
516
+ _guard_document(tbox, abox)
517
+ names = _NameMap()
518
+ axiom_lines: List[str] = []
519
+ for sub_role, super_role in tbox.role_inclusions:
520
+ # _render_role_expr, not names.role: an InverseRole must come out as
521
+ # ObjectInverseOf(...), and using it as a _NameMap KEY (what this did
522
+ # until 0.30.0) invents a brand-new atomic property instead -- so an
523
+ # inverse-role inclusion and a chain rendered CHARACTER-FOR-CHARACTER
524
+ # identically, and FaCT++ was asked about neither of them.
525
+ axiom_lines.append(f"SubObjectPropertyOf({_render_role_expr(sub_role, names)} "
526
+ f"{_render_role_expr(super_role, names)})")
527
+ for role in sorted(tbox.transitive_roles):
528
+ axiom_lines.append(f"TransitiveObjectProperty({names.role(role)})")
529
+ # The remaining role-box fields, in the order dl.tableau._AXIOM_KINDS lists
530
+ # them -- the same order dl.owl_functional._render_role_box uses, so the
531
+ # two renderers' documents line up axiom for axiom.
532
+ for left, right in tbox.disjoint_role_pairs:
533
+ axiom_lines.append(
534
+ f"DisjointObjectProperties({names.role(left)} {names.role(right)})")
535
+ for field_name, keyword in _CHARACTERISTIC_AXIOMS:
536
+ for role in sorted(getattr(tbox, field_name)):
537
+ axiom_lines.append(f"{keyword}({names.role(role)})")
538
+ for p, q in tbox.inverse_role_pairs:
539
+ axiom_lines.append(
540
+ f"InverseObjectProperties({names.role(p)} {names.role(q)})")
541
+ for chain, super_role in tbox.role_chains:
542
+ body = " ".join(_render_role_expr(role, names) for role in chain)
543
+ axiom_lines.append(f"SubObjectPropertyOf(ObjectPropertyChain({body}) "
544
+ f"{names.role(super_role)})")
545
+ for role, filler in tbox.role_domains:
546
+ axiom_lines.append(f"ObjectPropertyDomain({names.role(role)} "
547
+ f"{_render_ce(filler, names)})")
548
+ for role, filler in tbox.role_ranges:
549
+ axiom_lines.append(f"ObjectPropertyRange({names.role(role)} "
550
+ f"{_render_ce(filler, names)})")
551
+ # The DATA box: FaCT++ is an SROIQ(D) reasoner, so unlike the HermiT route
552
+ # this oracle CAN be asked about the data layer. Datatype and facet names
553
+ # are OWL 2's own and stay verbatim; a user-defined datatype and every data
554
+ # property get fresh synthetic names, like classes and roles.
555
+ for sub_prop, super_prop in tbox.data_property_inclusions:
556
+ axiom_lines.append(f"SubDataPropertyOf({names.dprop(sub_prop)} {names.dprop(super_prop)})")
557
+ for left, right in tbox.disjoint_data_property_pairs:
558
+ axiom_lines.append(f"DisjointDataProperties({names.dprop(left)} {names.dprop(right)})")
559
+ for prop in sorted(tbox.functional_data_properties):
560
+ axiom_lines.append(f"FunctionalDataProperty({names.dprop(prop)})")
561
+ for prop, filler in tbox.data_property_domains:
562
+ axiom_lines.append(f"DataPropertyDomain({names.dprop(prop)} {_render_ce(filler, names)})")
563
+ for prop, datarange in tbox.data_property_ranges:
564
+ axiom_lines.append(f"DataPropertyRange({names.dprop(prop)} "
565
+ f"{_render_datarange(datarange, names)})")
566
+ for name, datarange in tbox.datatype_definitions:
567
+ axiom_lines.append(f"DatatypeDefinition({names.dtype(name)} "
568
+ f"{_render_datarange(datarange, names)})")
569
+ for sub, sup in tbox.inclusions:
570
+ axiom_lines.append(f"SubClassOf({_render_ce(sub, names)} {_render_ce(sup, names)})")
571
+ for individual, concept in abox.concept_assertions:
572
+ axiom_lines.append(f"ClassAssertion({_render_ce(concept, names)} {names.ind(individual)})")
573
+ for a, b, role in abox.role_assertions:
574
+ axiom_lines.append(
575
+ f"ObjectPropertyAssertion({names.role(role)} {names.ind(a)} {names.ind(b)})")
576
+ for a, b in abox.distinct_assertions:
577
+ axiom_lines.append(f"DifferentIndividuals({names.ind(a)} {names.ind(b)})")
578
+ for a, b in abox.same_assertions:
579
+ axiom_lines.append(f"SameIndividual({names.ind(a)} {names.ind(b)})")
580
+ for a, b, role in abox.negative_role_assertions:
581
+ axiom_lines.append(f"NegativeObjectPropertyAssertion({names.role(role)} "
582
+ f"{names.ind(a)} {names.ind(b)})")
583
+ for individual, prop, value in abox.data_assertions:
584
+ axiom_lines.append(f"DataPropertyAssertion({names.dprop(prop)} "
585
+ f"{names.ind(individual)} {_render_literal(value, names)})")
586
+ for individual, prop, value in abox.negative_data_assertions:
587
+ axiom_lines.append(f"NegativeDataPropertyAssertion({names.dprop(prop)} "
588
+ f"{names.ind(individual)} {_render_literal(value, names)})")
589
+
590
+ decl_lines = (
591
+ [f"Declaration(Class({t}))" for t in names.class_tokens()]
592
+ + [f"Declaration(ObjectProperty({t}))" for t in names.role_tokens()]
593
+ + [f"Declaration(DataProperty({t}))" for t in names.data_property_tokens()]
594
+ + [f"Declaration(Datatype({t}))" for t in names.datatype_tokens()]
595
+ + [f"Declaration(NamedIndividual({t}))" for t in names.individual_tokens()]
596
+ )
597
+ body_lines = decl_lines + axiom_lines
598
+ header = (f"Prefix(:=<{_PREFIX_IRI}>)\n"
599
+ f"Prefix(owl:=<http://www.w3.org/2002/07/owl#>)\n")
600
+ if _collect_vocabulary(tbox, abox).has_data():
601
+ # The datatypes' own prefixes, declared so the document does not lean on
602
+ # the implicit ones -- only for a knowledge base that mentions a
603
+ # datatype at all, which leaves every data-free document as it was.
604
+ header += ("Prefix(xsd:=<http://www.w3.org/2001/XMLSchema#>)\n"
605
+ "Prefix(rdfs:=<http://www.w3.org/2000/01/rdf-schema#>)\n"
606
+ "Prefix(rdf:=<http://www.w3.org/1999/02/22-rdf-syntax-ns#>)\n")
607
+ header += f"Ontology(<{_ONTOLOGY_IRI}>"
608
+ if not body_lines:
609
+ return header + ")"
610
+ body = "\n".join(f" {line}" for line in body_lines)
611
+ return f"{header}\n{body}\n)"
612
+
613
+
614
+ # --------------------------------------------------------------------------- #
615
+ # The one HETS-invoking primitive.
616
+ # --------------------------------------------------------------------------- #
617
+
618
+ def _kb_consistent(tbox: Optional[TBox], abox: ABox, *,
619
+ time_limit: int, url: Optional[str]) -> bool:
620
+ """Render ``(tbox, abox)``, upload it, and ask ``POST /consistency-check``
621
+ with ``reasoner="Fact"``. The ONE Hets-invoking primitive every other
622
+ function in this module reduces to (see the module docstring's "The one
623
+ primitive" section).
624
+
625
+ Raises:
626
+ BackendUnavailable: no Hets server is reachable (from
627
+ :func:`~unicode_logic_kit.hets.docker.discover_hets_url`, when
628
+ ``url`` is not given).
629
+ RuntimeError: the upload or the consistency-check call itself failed
630
+ (:class:`~unicode_logic_kit.hets.client.HetsClient`'s own contract
631
+ — an HTTP error, an HETS ``"*** Error"`` body, malformed JSON).
632
+ HetsOwlError: the server answered, but not with a genuine
633
+ ``"Consistent"``/``"Inconsistent"`` verdict (a ``"Timeout"``, an
634
+ unrecognised result string, or other than exactly one goal
635
+ result) — see :class:`HetsOwlError`.
636
+ """
637
+ tbox = tbox if tbox is not None else TBox()
638
+ # Rendered BEFORE a server is looked for: a question that is refused by name
639
+ # (see _guard_document) must not need a reachable Hets to be refused.
640
+ text = _render_document(tbox, abox)
641
+ if url is None:
642
+ url, _container = discover_hets_url(start_container=False)
643
+ client = HetsClient(url, timeout=float(time_limit + 30))
644
+ iri = client.upload(text, _FILENAME)
645
+ goals = client.consistency_check(iri, _ONTOLOGY_IRI, reasoner=_REASONER,
646
+ time_limit=time_limit)
647
+ if len(goals) != 1:
648
+ raise HetsOwlError(
649
+ f"hets.owl_backend: expected exactly 1 goal result from "
650
+ f"consistency-check, got {len(goals)}: {goals!r}")
651
+ result = (goals[0].get("result") or "").strip()
652
+ if result == "Consistent":
653
+ return True
654
+ if result == "Inconsistent":
655
+ return False
656
+ raise HetsOwlError(
657
+ f"hets.owl_backend: consistency-check returned an unusable result "
658
+ f"{result!r} (never guessed as Consistent/Inconsistent — see "
659
+ f"HetsOwlError's docstring): {goals[0]!r}")
660
+
661
+
662
+ # --------------------------------------------------------------------------- #
663
+ # Public API — mirrors dl.tableau's / dl.owl_reasoner's exactly, over the
664
+ # ALCHQ + I + O fragment (see the module docstring's "The one primitive").
665
+ # --------------------------------------------------------------------------- #
666
+
667
+ def external_concept_satisfiable(concept: Concept, tbox: Optional[TBox] = None, *,
668
+ time_limit: int = 15, url: Optional[str] = None) -> bool:
669
+ """Return True iff ``concept`` is satisfiable with respect to ``tbox`` —
670
+ this module's twin of :func:`unicode_logic_kit.dl.tableau.concept_satisfiable`
671
+ / :func:`unicode_logic_kit.dl.owl_reasoner.external_concept_satisfiable`,
672
+ over the full ALCHQ + I + O fragment. Reduced to :func:`_kb_consistent`
673
+ via a single fresh probe individual, exactly the way
674
+ ``dl.owl_reasoner``'s own function does.
675
+ """
676
+ probe_abox = ABox().assert_concept("_probe", concept)
677
+ return _kb_consistent(tbox, probe_abox, time_limit=time_limit, url=url)
678
+
679
+
680
+ def external_concept_unsatisfiable(concept: Concept, tbox: Optional[TBox] = None, *,
681
+ time_limit: int = 15, url: Optional[str] = None) -> bool:
682
+ """Return True iff ``concept`` is unsatisfiable with respect to ``tbox``."""
683
+ return not external_concept_satisfiable(concept, tbox, time_limit=time_limit, url=url)
684
+
685
+
686
+ def external_subsumes(sub: Concept, sup: Concept, tbox: Optional[TBox] = None, *,
687
+ time_limit: int = 15, url: Optional[str] = None) -> bool:
688
+ """Return True iff ``tbox`` entails ``sub ⊑ sup`` — the same ``sub ⊓ ¬sup``
689
+ unsatisfiability reduction :func:`unicode_logic_kit.dl.tableau.subsumes` uses.
690
+ """
691
+ return not external_concept_satisfiable(And(sub, Not(sup)), tbox,
692
+ time_limit=time_limit, url=url)
693
+
694
+
695
+ def external_equivalent(c: Concept, d: Concept, tbox: Optional[TBox] = None, *,
696
+ time_limit: int = 15, url: Optional[str] = None) -> bool:
697
+ """Return True iff ``tbox`` entails ``c ≡ d`` (mutual subsumption)."""
698
+ return (external_subsumes(c, d, tbox, time_limit=time_limit, url=url)
699
+ and external_subsumes(d, c, tbox, time_limit=time_limit, url=url))
700
+
701
+
702
+ def external_abox_consistent(abox: ABox, tbox: Optional[TBox] = None, *,
703
+ time_limit: int = 15, url: Optional[str] = None) -> bool:
704
+ """Return True iff the knowledge base ``(tbox, abox)`` is consistent."""
705
+ return _kb_consistent(tbox, abox, time_limit=time_limit, url=url)
706
+
707
+
708
+ def _abox_with(abox: ABox, individual: str, concept: Concept) -> ABox:
709
+ """A copy of ``abox`` with one extra concept assertion ``individual : concept``
710
+ (used by :func:`external_instance_check`'s entailment reduction — mirrors
711
+ :func:`unicode_logic_kit.dl.owl_reasoner._abox_with` exactly).
712
+
713
+ :meth:`~unicode_logic_kit.dl.tableau.ABox.copy`, not a field-by-field
714
+ reconstruction: this module is one of the kit's INDEPENDENT oracles, and a
715
+ probe that silently drops an assertion kind the caller supplied asks FaCT++
716
+ about a strictly WEAKER knowledge base than the one the caller has, so the
717
+ two oracles would agree for the wrong reason. (Field-by-field is what this
718
+ did until 0.30.0, and it dropped ``same_assertions`` and
719
+ ``negative_role_assertions`` — and would have dropped the data assertions —
720
+ the moment they existed.)
721
+ """
722
+ probe = abox.copy()
723
+ probe.assert_concept(individual, concept)
724
+ return probe
725
+
726
+
727
+ def external_instance_check(abox: ABox, individual: str, concept: Concept,
728
+ tbox: Optional[TBox] = None, *,
729
+ time_limit: int = 15, url: Optional[str] = None) -> bool:
730
+ """Return True iff ``(tbox, abox)`` entails ``individual : concept`` — the
731
+ same "does asserting the complement make it inconsistent" reduction
732
+ :func:`unicode_logic_kit.dl.tableau.instance_check` uses. Open-world,
733
+ exactly like that function: False means NOT entailed, not "entailed false".
734
+ """
735
+ return not external_abox_consistent(
736
+ _abox_with(abox, individual, Not(concept)), tbox, time_limit=time_limit, url=url)
737
+
738
+
739
+ def _all_individuals(abox: ABox) -> Set[str]:
740
+ """Every individual name mentioned in ``abox`` (ANY assertion list), and NO
741
+ anonymous fallback: an ABox that names nobody has nobody to retrieve or
742
+ realize.
743
+
744
+ It IS :func:`unicode_logic_kit.dl.tableau._abox_individual_names`, the scan the
745
+ tableau's two sweeps (``instance_retrieval``, ``realize_all``), the HermiT
746
+ route and ``kb_to_fol(...).individuals`` all read, driven by ``dl.tableau.
747
+ _AXIOM_KINDS``' ``individual_positions``: an assertion kind added to the
748
+ table is enumerated by every route that sweeps individuals from its row
749
+ alone. NOT ``dl.tableau._individuals``, which adds the anonymous ``"a"`` for
750
+ an empty ABox because ``abox_consistent`` needs SOME node for the TBox to
751
+ run on — that node is not an individual of the knowledge base, and sweeping
752
+ it asked FaCT++ about a phantom and reported it as a member
753
+ (``TBox().add(⊤, A)`` over an empty ABox retrieved ``{"a"}`` here and
754
+ ``set()`` from the tableau and from HermiT). A local re-implementation of
755
+ the scan is what this was until 0.30.0, and it omitted ``same_assertions``
756
+ and ``negative_role_assertions``.
757
+ """
758
+ return set(_abox_individual_names(abox))
759
+
760
+
761
+ def external_instance_retrieval(abox: ABox, concept: Concept, tbox: Optional[TBox] = None, *,
762
+ time_limit: int = 15, url: Optional[str] = None) -> Set[str]:
763
+ """Return every individual of ``abox`` that ``(tbox, abox)`` entails is a
764
+ ``concept`` — sweeps :func:`external_instance_check` exactly like
765
+ :func:`unicode_logic_kit.dl.tableau.instance_retrieval` does.
766
+ """
767
+ _guard_document(tbox if tbox is not None else TBox(), abox, [concept])
768
+ return {ind for ind in _all_individuals(abox)
769
+ if external_instance_check(abox, ind, concept, tbox, time_limit=time_limit, url=url)}
770
+
771
+
772
+ def external_realize(abox: ABox, individual: str, vocabulary: List[Concept],
773
+ tbox: Optional[TBox] = None, *,
774
+ time_limit: int = 15, url: Optional[str] = None) -> List[Concept]:
775
+ """Return ``individual``'s most-specific concepts from ``vocabulary`` — the
776
+ same filter-then-drop-non-minimal reduction
777
+ :func:`unicode_logic_kit.dl.tableau.realize` uses.
778
+ """
779
+ _guard_document(tbox if tbox is not None else TBox(), abox, vocabulary)
780
+ candidates = [c for c in vocabulary
781
+ if external_instance_check(abox, individual, c, tbox,
782
+ time_limit=time_limit, url=url)]
783
+ return [c for c in candidates
784
+ if not any(external_subsumes(d, c, tbox, time_limit=time_limit, url=url)
785
+ and not external_subsumes(c, d, tbox, time_limit=time_limit, url=url)
786
+ for d in candidates)]
787
+
788
+
789
+ def external_realize_all(abox: ABox, vocabulary: List[Concept], tbox: Optional[TBox] = None, *,
790
+ time_limit: int = 15, url: Optional[str] = None) -> Dict[str, List[Concept]]:
791
+ """Return :func:`external_realize` for every individual named in ``abox``."""
792
+ _guard_document(tbox if tbox is not None else TBox(), abox, vocabulary)
793
+ return {ind: external_realize(abox, ind, vocabulary, tbox, time_limit=time_limit, url=url)
794
+ for ind in sorted(_all_individuals(abox))}