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,790 @@
1
+ """An external OWL 2 DL reasoner backend for ``dl/`` — ALCHQ **+ inverse roles
2
+ (I) + nominals (O)**, via `owlready2 <https://owlready2.readthedocs.io>`_'s
3
+ bundled HermiT.
4
+
5
+ :mod:`unicode_logic_kit.dl.tableau`'s in-house tableau decides ALCHQ (and
6
+ refuses :class:`~unicode_logic_kit.dl.concepts.InverseRole`/
7
+ :class:`~unicode_logic_kit.dl.concepts.Nominal` by name — see that module's
8
+ "Inverse roles and nominals (I, O)" section for why: inverse roles break its
9
+ subset-blocking termination argument, and nominals need an individual-merging
10
+ machinery it has none of). This module decides the FULL fragment — ALCHQ plus
11
+ both — by handing the same :class:`~unicode_logic_kit.dl.tableau.TBox`/
12
+ :class:`~unicode_logic_kit.dl.tableau.ABox`/:class:`~unicode_logic_kit.dl.concepts.Concept`
13
+ AST to `HermiT <http://www.hermit-reasoner.com/>`_, a mainstream,
14
+ OWL2-DL-complete, actively maintained tableau reasoner, via ``owlready2``'s
15
+ Python bindings and bundled ``HermiT.jar``. This is the intentionally
16
+ NON-in-house route C45 (an in-house SHIQ/SHOIQ tableau rewrite) was rejected
17
+ in favour of: the boundary this kit draws is "detect and refuse I/O by name",
18
+ never "attempt to decide them with a homegrown algorithm".
19
+
20
+ Optional dependency, availability check
21
+ ------------------------------------------
22
+ ``owlready2`` (`PyPI <https://pypi.org/project/owlready2/>`_) is an optional
23
+ extra — :func:`available` is PURE DISCOVERY (``importlib.util.find_spec``, no
24
+ import), mirroring :meth:`unicode_logic_kit.atp.cvc5_backend.Cvc5Backend.available`'s
25
+ exact convention, so probing it never pays the import cost. Every public
26
+ function in this module calls :func:`available` internally and raises
27
+ :class:`OwlReasonerError` (reusing
28
+ :class:`unicode_logic_kit.atp.protocol.BackendUnavailable` the way
29
+ :mod:`unicode_logic_kit.hets.docker` already imports across a subpackage
30
+ boundary — the same precedent this module follows for ``dl/``'s own first
31
+ ``atp`` import) when it is missing, rather than silently skipping. A JVM is
32
+ also required (HermiT runs as a ``java`` subprocess, invoked BY owlready2,
33
+ not by this module): this kit assumes one is already on ``PATH`` (Java is
34
+ NOT probed by :func:`available`, since owlready2 only ever discovers it at
35
+ actual reasoner-invocation time — a JVM launch failure surfaces as
36
+ ``OwlReadyJavaError``, re-raised here as :class:`OwlReasonerError` naming
37
+ what failed, never silently). On Windows, ``owlready2.JAVA_EXE`` (default
38
+ ``"java"``) may need to be set explicitly if ``java`` is not literally on
39
+ ``PATH`` under that name; this module never touches that setting itself.
40
+
41
+ Licensing
42
+ ----------
43
+ ``owlready2`` itself is **LGPL-3.0-or-later** (confirmed from the installed
44
+ package's own metadata: ``pip show owlready2`` prints
45
+ ``License-Expression: LGPL-3.0-or-later``) — the first LGPL/copyleft optional
46
+ dependency this kit carries (see the ``owl`` extra note in ``pyproject.toml``,
47
+ added via this task's ``integration`` request). Its wheel ALSO bundles a
48
+ second reasoner, Pellet, which is dual-licensed **AGPL-3.0** (confirmed from
49
+ ``owlready2/pellet/LICENSE.txt`` in the installed package) — this module
50
+ never calls ``owlready2.sync_reasoner_pellet``, only
51
+ ``sync_reasoner_hermit`` (whose bundled ``HermiT.jar`` is LGPL, same as the
52
+ Python package), so no AGPL code path is ever exercised by this kit; the AGPL
53
+ jar merely ships, unused, inside the dependency's wheel.
54
+
55
+ Translation route — direct to owlready2's Python object model, not through
56
+ ``owl_functional.py``'s text
57
+ ------------------------------------------------------------------------------
58
+ :mod:`unicode_logic_kit.dl.owl_functional` is this kit's OWL 2 Functional-Style
59
+ Syntax document reader/writer, and stays the ALCHQ-only "document writer" for
60
+ that fragment (unchanged by this module — see its own docstring): it is NOT
61
+ extended to parse/render ``ObjectInverseOf``/``ObjectOneOf``, because
62
+ ``owlready2`` has no Functional-Style Syntax reader at all (confirmed: it
63
+ reads only RDF/XML, OWL/XML and NTriples — see ``owlready2/namespace.py``'s
64
+ ``Ontology.load``/``graph.parse``), so round-tripping through that text would
65
+ buy nothing here and risk a second, redundant, untested translation path.
66
+ Instead this module translates :class:`~unicode_logic_kit.dl.tableau.TBox`/
67
+ :class:`~unicode_logic_kit.dl.tableau.ABox`/:class:`~unicode_logic_kit.dl.concepts.Concept`
68
+ DIRECTLY into ``owlready2``'s own Python class-expression API
69
+ (``And``/``Or``/``Not``/``Inverse``/``OneOf``/``Restriction`` — see
70
+ ``owlready2/class_construct.py``), the same object model ``owlready2``'s own
71
+ documentation builds ontologies with. Every entity (class/role/individual)
72
+ gets a FRESH, synthetic, opaque local name (``C1``, ``R1``, ``I1``, …), never
73
+ this kit's own name string verbatim: this kit's :class:`~unicode_logic_kit.dl.concepts.Atomic`/
74
+ role/individual names are arbitrary opaque strings that may contain
75
+ characters illegal in an OWL/RDF local name (whitespace, ``#``, non-ASCII
76
+ glyphs used elsewhere in this kit — see :mod:`unicode_logic_kit.dl.owl_functional`'s
77
+ own "IRIs and names" section for the identical concern there), and since
78
+ every query this module answers is a yes/no verdict (never "what is this
79
+ entity called back"), there is nothing to lose by never round-tripping the
80
+ original name through owlready2's IRI machinery at all.
81
+
82
+ Each of this module's ``external_*`` functions builds a FRESH ``owlready2.World()``
83
+ (never the shared global default world) and a fresh ``Ontology`` inside it, so
84
+ two calls never share reasoner state — confirmed empirically (see this
85
+ task's scratch notes): mutating one ``World``'s entities is invisible to
86
+ another. A **general concept inclusion** ``sub ⊑ sup`` (this kit's ``TBox``
87
+ is always a GENERAL TBox — see :mod:`unicode_logic_kit.dl.tableau`) is asserted
88
+ via ``owlready2.GeneralClassAxiom``, the native OWL 2 mechanism for a
89
+ ``SubClassOf`` whose LEFT side is an arbitrary (possibly anonymous) class
90
+ expression — EXCEPT for the two antecedent shapes ``GeneralClassAxiom``
91
+ cannot take directly (confirmed empirically: its left side must be a genuine
92
+ ``Construct``, e.g. ``And``/``Or``/``Not``/``Restriction``/``OneOf``, not a
93
+ plain named entity like ``owl:Thing`` — mutating ``owl:Thing`` itself would
94
+ also leak across every ``World`` at once, since it is a single shared Python
95
+ object, not a per-world one): ``sub = Bottom()`` is skipped outright (``∅ ⊑
96
+ sup`` is a tautology, exactly mirroring how the in-house tableau's own
97
+ ``internalized()`` would trivially satisfy it too), and ``sub = Top()``
98
+ routes through a lazily-created per-ontology helper class equivalent to
99
+ ``owl:Thing`` (``_Domain.equivalent_to = [Thing]``, then
100
+ ``_Domain.is_a.append(translated sup)``) — which correctly forces every
101
+ individual into ``sup``, confirmed empirically, without ever touching the
102
+ shared ``Thing`` object itself. An ``Atomic`` antecedent uses the normal,
103
+ better-supported ``named_class.is_a.append(...)`` path instead of a
104
+ ``GeneralClassAxiom`` at all (no anonymous left side needed when the
105
+ antecedent already names a class).
106
+
107
+ What each function decides
108
+ ------------------------------
109
+ Every ``external_*`` function mirrors its :mod:`unicode_logic_kit.dl.tableau`
110
+ namesake's SIGNATURE and REDUCTION exactly (``external_subsumes`` reduces to
111
+ ``external_concept_satisfiable`` exactly the way ``subsumes`` reduces to
112
+ ``concept_satisfiable``, and so on) — this is deliberate: it is what makes
113
+ the differential test battery in ``tests/test_owl_reasoner.py`` meaningful
114
+ (every ALCHQ fixture in ``tests/test_dl_alc.py``/``test_dl_alcq.py``/
115
+ ``test_dl_rbox.py``/``test_dl_classify.py`` decided through BOTH this
116
+ module's reduction chain and the tableau's own is comparing the SAME
117
+ question, not two differently-shaped ones), and it lets this module cover
118
+ the full I/O-extended fragment (concept/ABox consistency, subsumption,
119
+ equivalence, instance checking/retrieval, realization) with only ONE genuine
120
+ HermiT-invoking primitive underneath (:func:`_kb_consistent`), exactly the
121
+ way the in-house tableau has only ``_solve`` underneath ITS whole public API.
122
+
123
+ Each call to :func:`_kb_consistent` spawns a fresh ``java`` subprocess (via
124
+ ``owlready2.sync_reasoner_hermit``), so this module is orders of magnitude
125
+ slower per call than the in-house tableau — expected and acceptable for an
126
+ occasional external cross-check, not a hot-path reasoner; the differential
127
+ test battery deliberately runs a curated, small fixture set rather than
128
+ every existing ALCHQ test file verbatim, to keep the live test run's JVM
129
+ subprocess count bounded.
130
+
131
+ Public API: :func:`available`, :func:`external_concept_satisfiable`,
132
+ :func:`external_concept_unsatisfiable`, :func:`external_subsumes`,
133
+ :func:`external_equivalent`, :func:`external_abox_consistent`,
134
+ :func:`external_instance_check`, :func:`external_instance_retrieval`,
135
+ :func:`external_realize`, :func:`external_realize_all`, :class:`OwlReasonerError`.
136
+ """
137
+
138
+ import importlib.util
139
+ from typing import Dict, List, Optional, Set
140
+
141
+ from ..atp.protocol import BackendUnavailable
142
+ from .concepts import (
143
+ Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
144
+ InverseRole, Nominal, HasValue, DATA_CONCEPTS,
145
+ )
146
+ from .tableau import (
147
+ TBox, ABox, UnsupportedAxiomError, UnsupportedConceptError, _RBox,
148
+ _abox_individual_names, _check_simple_role_box, _concept_individual_names,
149
+ _data_layer_kinds, _reject_abox_roles, _reject_concept_role,
150
+ _reject_concept_roles_deep, _tbox_class_expressions, _validate_data_box,
151
+ _validate_role_box,
152
+ )
153
+
154
+ __all__ = [
155
+ "available",
156
+ "external_concept_satisfiable", "external_concept_unsatisfiable",
157
+ "external_subsumes", "external_equivalent",
158
+ "external_abox_consistent", "external_instance_check", "external_instance_retrieval",
159
+ "external_realize", "external_realize_all",
160
+ "OwlReasonerError",
161
+ ]
162
+
163
+
164
+ class OwlReasonerError(BackendUnavailable):
165
+ """``owlready2`` is not installed, or HermiT (invoked through it) failed to
166
+ run — a missing/broken JVM, a HermiT crash, or any other reasoner-invocation
167
+ failure that is not itself an ``OwlReadyInconsistentOntologyError`` (which
168
+ this module treats as an ordinary, expected "inconsistent" verdict, not an
169
+ error — see :func:`_kb_consistent`). Reuses
170
+ :class:`unicode_logic_kit.atp.protocol.BackendUnavailable` (see the module
171
+ docstring), so a caller that already handles that exception for the kit's
172
+ other optional backends (cvc5, Hets/Docker, …) handles this one the same
173
+ way for free.
174
+ """
175
+
176
+
177
+ def available() -> bool:
178
+ """True iff ``owlready2`` is importable — pure discovery, no import, no JVM
179
+ probe (see the module docstring's "Optional dependency" section).
180
+ """
181
+ return importlib.util.find_spec("owlready2") is not None
182
+
183
+
184
+ def _require_available():
185
+ """Return the imported ``owlready2`` module, or raise :class:`OwlReasonerError`
186
+ naming the missing extra (see :func:`available`). The import itself stays
187
+ lazy — done here, not at this module's top level — so importing
188
+ ``dl.owl_reasoner`` never requires ``owlready2`` to be installed; only
189
+ actually USING it does (see the module docstring's "Optional dependency"
190
+ section).
191
+ """
192
+ if not available():
193
+ raise OwlReasonerError(
194
+ "dl.owl_reasoner: owlready2 is not installed — install the optional "
195
+ "'owl' extra (`pip install unicode-logic-kit[owl]`, i.e. "
196
+ "`pip install owlready2`) to use the external, HermiT-backed OWL 2 "
197
+ "DL reasoner for InverseRole/Nominal (I/O) beyond this kit's "
198
+ "in-house ALCHQ tableau (see dl.tableau's 'Inverse roles and "
199
+ "nominals (I, O)' section).")
200
+ import owlready2
201
+ return owlready2
202
+
203
+
204
+ # --------------------------------------------------------------------------- #
205
+ # Translation: Concept/TBox/ABox -> a fresh owlready2 World/Ontology.
206
+ # --------------------------------------------------------------------------- #
207
+
208
+ class _Ctx:
209
+ """Per-call translation state: the fresh ``owlready2``/``World``/``Ontology``
210
+ triple, and lazily-populated name -> owlready2-entity maps (see the module
211
+ docstring's "Translation route" section for why entities get fresh
212
+ synthetic names rather than this kit's own name strings verbatim).
213
+ """
214
+
215
+ __slots__ = ("ow", "world", "onto", "characteristics",
216
+ "_classes", "_roles", "_individuals", "_domain", "_counter", "_below")
217
+
218
+ def __init__(self, ow, world, onto, characteristics: Dict[str, Set[str]]):
219
+ self.ow = ow
220
+ self.world = world
221
+ self.onto = onto
222
+ # ``owlready2 base name -> the role names carrying that characteristic``
223
+ # (see :data:`_CHARACTERISTIC_BASES`). All SIX characteristics, not just
224
+ # transitivity: owlready2 needs every one of them among the property
225
+ # class's BASES at creation time, so they cannot be added later and a
226
+ # route that carried only one would silently drop the other five — and
227
+ # this route is where the in-house tableau's own refusals send the user.
228
+ self.characteristics = characteristics
229
+ self._classes: Dict[str, object] = {}
230
+ self._roles: Dict[str, object] = {}
231
+ self._individuals: Dict[str, object] = {}
232
+ self._domain = None
233
+ self._counter = 0
234
+ # ``("class" | "role") -> name -> the names directly above it`` for the inclusions
235
+ # between two NAMED entities that were stated as owlready2 sub-class / sub-property
236
+ # links (see :meth:`state_named_inclusion`).
237
+ self._below: Dict[str, Dict[str, Set[str]]] = {"class": {}, "role": {}}
238
+
239
+ def state_named_inclusion(self, kind: str, sub_name: str, sup_name: str,
240
+ sub_entity, sup_entity) -> None:
241
+ """Assert ``sub ⊑ sup`` between two named classes (``kind="class"``) or two plain
242
+ properties (``kind="role"``).
243
+
244
+ owlready2 keeps a named class (property) under another as a Python base class, and
245
+ Python refuses a cycle of bases with a ``TypeError``. Two named entities below each
246
+ other, an entity below itself, a longer ring, ``EquivalentClasses`` and
247
+ ``EquivalentObjectProperties`` all read as such a cycle. An inclusion that would close
248
+ one is therefore stated as the equivalence ``sub ≡ sup``, which is what the ring says:
249
+ ``sup`` is already below ``sub`` through the links stated so far, so ``sub ⊑ sup``
250
+ makes every entity on the ring equivalent, and ``sub ≡ sup`` adds only the converse
251
+ that the ring already gives. An entity below itself says nothing and is not stated.
252
+ """
253
+ if sub_name == sup_name:
254
+ return
255
+ links = self._below[kind]
256
+ reachable, pending = set(), [sup_name]
257
+ while pending:
258
+ name = pending.pop()
259
+ if name in reachable:
260
+ continue
261
+ reachable.add(name)
262
+ pending.extend(links.get(name, ()))
263
+ if sub_name in reachable:
264
+ sub_entity.equivalent_to.append(sup_entity)
265
+ else:
266
+ links.setdefault(sub_name, set()).add(sup_name)
267
+ sub_entity.is_a.append(sup_entity)
268
+
269
+ def _fresh_name(self, prefix: str) -> str:
270
+ self._counter += 1
271
+ return f"{prefix}{self._counter}"
272
+
273
+ def cls(self, name: str):
274
+ """The owlready2 named class for this kit's ``Atomic`` name ``name``."""
275
+ if name not in self._classes:
276
+ self._classes[name] = self.ow.types.new_class(self._fresh_name("C"), (self.ow.Thing,))
277
+ return self._classes[name]
278
+
279
+ def _role_bases(self, name: str) -> tuple:
280
+ """``(ObjectProperty, *every declared characteristic's owlready2 class)``
281
+ for the role named ``name`` — the bases tuple ``role_obj`` creates it with.
282
+ """
283
+ bases = [self.ow.ObjectProperty]
284
+ for base_name, roles in self.characteristics.items():
285
+ if name in roles:
286
+ bases.append(getattr(self.ow, base_name))
287
+ return tuple(bases)
288
+
289
+ def role_obj(self, name: str):
290
+ """The owlready2 object property for this kit's role name ``name``, with
291
+ every declared characteristic among its bases at CREATION time
292
+ (owlready2 needs them up front, not added after the fact).
293
+ """
294
+ if name not in self._roles:
295
+ self._roles[name] = self.ow.types.new_class(
296
+ self._fresh_name("R"), self._role_bases(name))
297
+ return self._roles[name]
298
+
299
+ def role(self, role_field):
300
+ """Resolve a ``role`` field (a plain ``str``, or an
301
+ :class:`~unicode_logic_kit.dl.concepts.InverseRole`) to the owlready2
302
+ property expression to use in a restriction: the property itself, or
303
+ ``owlready2.Inverse(property)``.
304
+ """
305
+ if isinstance(role_field, InverseRole):
306
+ return self.ow.Inverse(self.role_obj(role_field.role))
307
+ return self.role_obj(role_field)
308
+
309
+ def ind(self, name: str):
310
+ """The owlready2 named individual for this kit's individual name ``name``."""
311
+ if name not in self._individuals:
312
+ self._individuals[name] = self.ow.Thing(self._fresh_name("I"))
313
+ return self._individuals[name]
314
+
315
+ def domain_class(self):
316
+ """A per-ontology class equivalent to ``owl:Thing`` — see the module
317
+ docstring's "Translation route" section for why a ``Top()`` GCI
318
+ antecedent routes through this rather than ``owlready2.Thing`` itself.
319
+ """
320
+ if self._domain is None:
321
+ self._domain = self.ow.types.new_class(self._fresh_name("D"), (self.ow.Thing,))
322
+ self._domain.equivalent_to = [self.ow.Thing]
323
+ return self._domain
324
+
325
+
326
+ def _translate_concept(concept: Concept, ctx: _Ctx):
327
+ """Translate ``concept`` into an owlready2 class expression (see the module
328
+ docstring's "Translation route" section). Covers the FULL ALCHQ + I + O
329
+ fragment this module decides — every :class:`Concept` subtype, including
330
+ :class:`InverseRole`-valued roles and :class:`Nominal`.
331
+
332
+ An OWL 2 built-in property name as a restriction's role is refused by name
333
+ (:func:`~unicode_logic_kit.dl.tableau._reject_concept_role`), exactly as the
334
+ in-house tableau and the FOL image refuse it: owlready2 would create an
335
+ ORDINARY property of that name, and an oracle that answers about a
336
+ different restriction agrees with nothing for the right reason.
337
+ """
338
+ _reject_concept_role(concept, where="dl.owl_reasoner")
339
+ if isinstance(concept, Top):
340
+ return ctx.ow.Thing
341
+ if isinstance(concept, Bottom):
342
+ return ctx.ow.Nothing
343
+ if isinstance(concept, Atomic):
344
+ return ctx.cls(concept.name)
345
+ if isinstance(concept, Nominal):
346
+ return ctx.ow.OneOf([ctx.ind(concept.individual)])
347
+ if isinstance(concept, HasValue):
348
+ # owlready2 spells ObjectHasValue(P a) as `prop.value(individual)`.
349
+ return ctx.role(concept.role).value(ctx.ind(concept.individual))
350
+ if isinstance(concept, Not):
351
+ return ctx.ow.Not(_translate_concept(concept.concept, ctx))
352
+ if isinstance(concept, And):
353
+ return ctx.ow.And([_translate_concept(concept.left, ctx),
354
+ _translate_concept(concept.right, ctx)])
355
+ if isinstance(concept, Or):
356
+ return ctx.ow.Or([_translate_concept(concept.left, ctx),
357
+ _translate_concept(concept.right, ctx)])
358
+ if isinstance(concept, Exists):
359
+ return ctx.role(concept.role).some(_translate_concept(concept.concept, ctx))
360
+ if isinstance(concept, ForAll):
361
+ return ctx.role(concept.role).only(_translate_concept(concept.concept, ctx))
362
+ if isinstance(concept, AtLeast):
363
+ return ctx.role(concept.role).min(concept.n, _translate_concept(concept.concept, ctx))
364
+ if isinstance(concept, AtMost):
365
+ return ctx.role(concept.role).max(concept.n, _translate_concept(concept.concept, ctx))
366
+ if isinstance(concept, DATA_CONCEPTS):
367
+ raise UnsupportedConceptError(
368
+ f"dl.owl_reasoner: the data restriction {type(concept).__name__} "
369
+ f"({concept.to_unicode()}) is not wired to this external route: "
370
+ f"owlready2 could express it, but this module does not translate the "
371
+ f"data layer, and an oracle that quietly dropped it would agree with "
372
+ f"everything. Ask the FOL image: kb = dl.kb_to_fol(tbox, abox, "
373
+ f"query=[concept]), then api.prove(kb.unsatisfiability_goal(concept), "
374
+ f"kb.tbox_premises) (kb.subsumption_goal and kb.instance_goal are the "
375
+ f"other two questions; 'proved' transfers to OWL 2, 'refuted' does not: "
376
+ f"see kb.refutation_is_decisive) — or decide facet arithmetic over "
377
+ f"the data ranges alone with atp.z3_arith.is_valid_arith.")
378
+ raise TypeError(f"dl.owl_reasoner: unsupported concept {type(concept).__name__}")
379
+
380
+
381
+ def _add_gci(sub: Concept, sup: Concept, ctx: _Ctx) -> None:
382
+ """Assert the general concept inclusion ``sub ⊑ sup`` into ``ctx``'s
383
+ ontology, choosing the cheapest correct owlready2 encoding for the shape
384
+ of ``sub`` (see the module docstring's "Translation route" section for
385
+ why ``Top``/``Bottom``/``Atomic`` antecedents each need their own case).
386
+ """
387
+ if isinstance(sub, Bottom):
388
+ return # ∅ ⊑ sup is a tautology; nothing to assert
389
+ if isinstance(sub, Top):
390
+ ctx.domain_class().is_a.append(_translate_concept(sup, ctx))
391
+ return
392
+ if isinstance(sub, Atomic):
393
+ if isinstance(sup, Atomic):
394
+ ctx.state_named_inclusion("class", sub.name, sup.name, ctx.cls(sub.name), ctx.cls(sup.name))
395
+ else:
396
+ ctx.cls(sub.name).is_a.append(_translate_concept(sup, ctx))
397
+ return
398
+ gca = ctx.ow.GeneralClassAxiom(_translate_concept(sub, ctx))
399
+ gca.is_a.append(_translate_concept(sup, ctx))
400
+
401
+
402
+ #: ``TBox field -> the owlready2 property class that expresses it as a BASE``.
403
+ #: owlready2 requires a characteristic among the property class's bases at
404
+ #: creation time, so ``_Ctx`` has to know all six BEFORE any role is built —
405
+ #: hence a table read by ``_characteristics`` rather than six inline tests.
406
+ _CHARACTERISTIC_BASES = {
407
+ "transitive_roles": "TransitiveProperty",
408
+ "symmetric_roles": "SymmetricProperty",
409
+ "asymmetric_roles": "AsymmetricProperty",
410
+ "reflexive_roles": "ReflexiveProperty",
411
+ "irreflexive_roles": "IrreflexiveProperty",
412
+ "functional_roles": "FunctionalProperty",
413
+ "inverse_functional_roles": "InverseFunctionalProperty",
414
+ }
415
+
416
+
417
+ def _characteristics(tbox: TBox) -> Dict[str, Set[str]]:
418
+ """``owlready2 base name -> the role names carrying it`` for ``tbox``."""
419
+ return {base: set(getattr(tbox, field))
420
+ for field, base in _CHARACTERISTIC_BASES.items()}
421
+
422
+
423
+ def _role_expr(role_field, ctx: _Ctx):
424
+ """A role-box entry (a plain name, or an
425
+ :class:`~unicode_logic_kit.dl.concepts.InverseRole`) as the owlready2
426
+ property expression to use on either side of a sub-property axiom.
427
+
428
+ ``ctx.role`` already does exactly this for a restriction's role; calling it
429
+ here is what stops an ``InverseRole`` being used as a DICT KEY and coming
430
+ back out as a brand-new atomic property — which is what ``_build_kb`` did
431
+ until 0.30.0, making ``external_subsumes(…, TBox().add_role_inclusion('r',
432
+ InverseRole('s')))`` answer a question about an ontology that never had the
433
+ axiom.
434
+ """
435
+ return ctx.role(role_field)
436
+
437
+
438
+ def _plain_sub_role(sub_role, super_role):
439
+ """``(sub_role, super_role)`` with a PLAIN role on the left — owlready2
440
+ attaches a sub-property axiom to the property CLASS on the left
441
+ (``is_a.append``), which an ``Inverse(...)`` expression is not (it crashed
442
+ with an ``AttributeError`` for ``s⁻ ⊑ r``, while ``r ⊑ s⁻`` built).
443
+
444
+ The rewrite is an equivalence, not an approximation: ``r⁻ ⊑ s`` says every
445
+ ``r(y, x)`` is an ``s(x, y)``, i.e. every ``r(x, y)`` is an ``s(y, x)``,
446
+ which is ``r ⊑ s⁻``; and ``r⁻ ⊑ s⁻`` is ``r ⊑ s``, the inverses cancelling.
447
+ """
448
+ if isinstance(sub_role, InverseRole):
449
+ inner = (super_role.role if isinstance(super_role, InverseRole)
450
+ else InverseRole(super_role))
451
+ return sub_role.role, inner
452
+ return sub_role, super_role
453
+
454
+
455
+ def _add_inverse_pair(ctx: _Ctx, partner: Dict[str, str], p: str, q: str) -> None:
456
+ """Assert ``InverseObjectProperties(p q)`` (``p ≡ q⁻``) into ``ctx``.
457
+
458
+ owlready2's ``inverse_property`` holds ONE property, and assigning it a
459
+ second time REPLACES the first — so ``Inv(p, q)`` together with ``Inv(p, r)``
460
+ kept only one of them and the oracle reasoned over a weaker knowledge base.
461
+ The second pair is not lost by being kept as an EQUIVALENCE instead: if ``p``
462
+ already has the inverse ``q0`` then ``q0 ≡ q`` (both are ``p⁻``), and the
463
+ symmetric case likewise. ``partner`` records the one inverse each property
464
+ was given.
465
+ """
466
+ if p in partner:
467
+ if partner[p] != q:
468
+ ctx.role_obj(partner[p]).equivalent_to.append(ctx.role_obj(q))
469
+ elif q in partner:
470
+ if partner[q] != p:
471
+ ctx.role_obj(partner[q]).equivalent_to.append(ctx.role_obj(p))
472
+ else:
473
+ ctx.role_obj(p).inverse_property = ctx.role_obj(q)
474
+ partner[p], partner[q] = q, p
475
+
476
+
477
+ def _build_kb(ctx: _Ctx, tbox: TBox, abox: ABox) -> None:
478
+ """Populate ``ctx``'s ontology with ``tbox``'s role box + GCIs and ``abox``'s
479
+ assertions. Must run inside a ``with ctx.onto:`` block (every
480
+ ``ctx.ow.types.new_class``/``GeneralClassAxiom`` call needs the current
481
+ namespace set — see ``owlready2``'s own convention).
482
+
483
+ EVERY role-box field is rendered. This module is one of the kit's two
484
+ INDEPENDENT oracles, and it is the one the in-house tableau's own refusal
485
+ messages send the user to — an oracle that silently dropped the axiom under
486
+ test would agree with everything, and would make those messages dishonest.
487
+ ``tests/test_dl_route_agreement.py`` checks a verdict per field that FLIPS
488
+ when the field is dropped, which is what makes that claim checkable.
489
+ """
490
+ for sub_role, super_role in tbox.role_inclusions:
491
+ sub_role, super_role = _plain_sub_role(sub_role, super_role)
492
+ if isinstance(super_role, str):
493
+ ctx.state_named_inclusion("role", sub_role, super_role,
494
+ _role_expr(sub_role, ctx), _role_expr(super_role, ctx))
495
+ else:
496
+ _role_expr(sub_role, ctx).is_a.append(_role_expr(super_role, ctx))
497
+ # APPEND, never assign: owlready2 stores a property's property chains,
498
+ # domains and ranges as lists, and `role.domain = [...]` REPLACES what an
499
+ # earlier axiom put there. Two ObjectPropertyDomain axioms on one role are
500
+ # a CONJUNCTION (an element with an r-successor is in both classes), so
501
+ # assigning kept only the last and the oracle reasoned over a weaker
502
+ # knowledge base than the tableau and the FOL image -- silently, which is
503
+ # the one thing an oracle must not do. The same for ranges and for two
504
+ # chains with one super-property.
505
+ for chain, super_role in tbox.role_chains:
506
+ ctx.role_obj(super_role).property_chain.append(
507
+ ctx.ow.PropertyChain([ctx.role_obj(role) for role in chain]))
508
+ partner: Dict[str, str] = {}
509
+ for p, q in tbox.inverse_role_pairs:
510
+ _add_inverse_pair(ctx, partner, p, q)
511
+ for left, right in tbox.disjoint_role_pairs:
512
+ ctx.ow.AllDisjoint([ctx.role_obj(left), ctx.role_obj(right)])
513
+ # The six characteristics need no statement here: they are already among
514
+ # each property class's bases (see _Ctx.role_obj). A role that occurs ONLY
515
+ # in a characteristic declaration still has to be created, though, or the
516
+ # declaration would never reach the ontology at all.
517
+ for roles in ctx.characteristics.values():
518
+ for role in sorted(roles):
519
+ ctx.role_obj(role)
520
+ for role, filler in tbox.role_domains:
521
+ ctx.role_obj(role).domain.append(_translate_concept(filler, ctx))
522
+ for role, filler in tbox.role_ranges:
523
+ ctx.role_obj(role).range.append(_translate_concept(filler, ctx))
524
+ for sub, sup in tbox.inclusions:
525
+ _add_gci(sub, sup, ctx)
526
+ for individual, concept in abox.concept_assertions:
527
+ ctx.ind(individual).is_a.append(_translate_concept(concept, ctx))
528
+ for a, b, role in abox.role_assertions:
529
+ role_obj = ctx.role_obj(role)
530
+ if issubclass(role_obj, ctx.ow.FunctionalProperty):
531
+ # owlready2 keeps the value of a FUNCTIONAL property as ONE attribute (None
532
+ # while unset), not as a list to append to, and a second assignment would
533
+ # REPLACE the first: two successors of one individual (which functionality
534
+ # then identifies, or which `distinct_assertions` makes inconsistent) would
535
+ # reach HermiT as one. The assertion r(a, b) is stated instead as the class
536
+ # assertion a : ∃r.{b} (ObjectHasValue), which OWL 2 defines to mean the same
537
+ # and which the negative assertions below already use. The test is the property
538
+ # CLASS, not the role names the TBox declared functional: a sub-property of a
539
+ # functional property is a Python subclass of it and holds one value as well.
540
+ ctx.ind(a).is_a.append(role_obj.value(ctx.ind(b)))
541
+ else:
542
+ getattr(ctx.ind(a), role_obj.name).append(ctx.ind(b))
543
+ for a, b in abox.distinct_assertions:
544
+ ctx.ow.AllDifferent([ctx.ind(a), ctx.ind(b)])
545
+ for a, b in abox.same_assertions:
546
+ # owlready2's own spelling of SameIndividual: the two individuals
547
+ # become one entity by equivalence, which is what HermiT then reads.
548
+ ctx.ind(a).equivalent_to.append(ctx.ind(b))
549
+ for a, b, role in abox.negative_role_assertions:
550
+ role_obj = ctx.role_obj(role)
551
+ ctx.ind(a).is_a.append(ctx.ow.Not(role_obj.value(ctx.ind(b))))
552
+
553
+
554
+ def _reject_data_layer(tbox: TBox, abox: ABox) -> None:
555
+ """Refuse, by name, a knowledge base that carries a DATA axiom kind.
556
+
557
+ This module is one of the kit's two INDEPENDENT oracles, and an oracle that
558
+ silently dropped the axiom under test would agree with everything. owlready2
559
+ has the machinery for data properties and datatypes, but this module's
560
+ translation does not use it, so the honest answer is a refusal that names
561
+ the kinds and the routes that DO answer (the FOL image; for facet arithmetic
562
+ alone, ``atp.z3_arith``). Data CONCEPTS are refused at translation time, in
563
+ :func:`_translate_concept`.
564
+ """
565
+ kinds = _data_layer_kinds(tbox, abox)
566
+ if kinds:
567
+ raise UnsupportedAxiomError(
568
+ f"dl.owl_reasoner: this knowledge base carries data-layer axiom "
569
+ f"kinds ({', '.join(kinds)}), which this external route does not "
570
+ f"translate — it refuses them by name rather than answer for a "
571
+ f"weaker knowledge base. The in-house tableau has no data domain "
572
+ f"either, so the route that answers is the FOL image: "
573
+ f"kb = dl.kb_to_fol(tbox, abox, query=[concept]), then "
574
+ f"api.prove(kb.unsatisfiability_goal(concept), kb.tbox_premises) — "
575
+ f"or kb.subsumption_goal / kb.instance_goal; 'proved' transfers to "
576
+ f"OWL 2, 'refuted' does not (kb.refutation_is_decisive). Facet "
577
+ f"arithmetic over the data ranges alone is decided by "
578
+ f"atp.z3_arith.is_valid_arith.")
579
+
580
+
581
+ def _guard_inputs(tbox: Optional[TBox], abox: Optional[ABox],
582
+ concepts=()) -> None:
583
+ """The refusals every ``external_*`` function owes BEFORE it builds
584
+ anything — in ONE place, so an entry point that makes no HermiT call at all
585
+ (``external_realize`` on an empty vocabulary, ``external_realize_all`` and
586
+ ``external_instance_retrieval`` on an empty ABox) still runs them: those
587
+ returned a quiet ``[]`` for a knowledge base the other seven refuse.
588
+
589
+ The stored role box and data box must be well-formed — the SAME validation
590
+ the in-house tableau and the FOL image run (:func:`_validate_role_box`) —;
591
+ an ABox assertion must not carry an OWL 2 built-in property name; the data
592
+ layer is refused by name; the role box must satisfy OWL 2's simple-role
593
+ restriction; and no class expression may use a built-in property name as a
594
+ role.
595
+ """
596
+ tbox = tbox if tbox is not None else TBox()
597
+ _validate_role_box(tbox, where="dl.owl_reasoner")
598
+ _validate_data_box(tbox, where="dl.owl_reasoner")
599
+ _reject_abox_roles(abox, where="dl.owl_reasoner")
600
+ _reject_data_layer(tbox, abox if abox is not None else ABox())
601
+ _check_simple_role_box(tbox, _RBox.from_tbox(tbox))
602
+ for concept in concepts:
603
+ _reject_concept_roles_deep(concept, where="dl.owl_reasoner")
604
+ for concept in _tbox_class_expressions(tbox):
605
+ _reject_concept_roles_deep(concept, where="dl.owl_reasoner")
606
+ if abox is not None:
607
+ for _individual, concept in abox.concept_assertions:
608
+ _reject_concept_roles_deep(concept, where="dl.owl_reasoner")
609
+
610
+
611
+ def _kb_consistent(tbox: Optional[TBox], abox: ABox) -> bool:
612
+ """Build a fresh ``owlready2`` ``World``/``Ontology`` encoding ``(tbox,
613
+ abox)``, run HermiT via ``sync_reasoner_hermit``, and return True iff the
614
+ knowledge base is consistent. The ONE HermiT-invoking primitive every
615
+ other function in this module reduces to (see the module docstring's
616
+ "What each function decides" section).
617
+
618
+ Raises:
619
+ OwlReasonerError: owlready2 is missing, or HermiT/the JVM failed for
620
+ a reason other than genuine inconsistency.
621
+ ~unicode_logic_kit.dl.tableau.NonSimpleRoleError:
622
+ the role box violates OWL 2's own SIMPLE-role
623
+ restriction (Structural Specification §11). Checked with the KIT's
624
+ own check, so the same role box is refused with the same message on
625
+ EVERY route, rather than surfacing as a HermiT error here and as a
626
+ tableau error there.
627
+ """
628
+ # The kit's own refusals come first, so that a knowledge base it refuses is refused with
629
+ # the same error whether or not owlready2 is installed.
630
+ tbox = tbox if tbox is not None else TBox()
631
+ _guard_inputs(tbox, abox)
632
+ ow = _require_available()
633
+ world = ow.World()
634
+ onto = world.get_ontology("http://unicode-logic-kit.invalid/kb#")
635
+ ctx = _Ctx(ow, world, onto, _characteristics(tbox))
636
+ with onto:
637
+ _build_kb(ctx, tbox, abox)
638
+ try:
639
+ ow.sync_reasoner_hermit(world, debug=0)
640
+ return True
641
+ except ow.OwlReadyInconsistentOntologyError:
642
+ return False
643
+ except ow.OwlReadyJavaError as exc:
644
+ raise OwlReasonerError(
645
+ f"dl.owl_reasoner: HermiT (invoked via owlready2, requires a JVM "
646
+ f"on PATH) failed: {exc}") from exc
647
+
648
+
649
+ # --------------------------------------------------------------------------- #
650
+ # Public API — mirrors dl.tableau's, over the ALCHQ + I + O fragment.
651
+ # --------------------------------------------------------------------------- #
652
+
653
+ #: The name :func:`external_concept_satisfiable` gives its probe individual when
654
+ #: no individual of the question already has it.
655
+ _PROBE_INDIVIDUAL = "_probe"
656
+
657
+
658
+ def _fresh_individual(base: str, taken: Set[str]) -> str:
659
+ """``base``, or ``base`` followed by the first number that makes it a name
660
+ outside ``taken`` — an individual name the question does not already use.
661
+
662
+ Individual names are compared exactly (they are case-sensitive strings), so
663
+ exact membership in ``taken`` is the whole test.
664
+ """
665
+ name, number = base, 0
666
+ while name in taken:
667
+ number += 1
668
+ name = f"{base}{number}"
669
+ return name
670
+
671
+
672
+ def external_concept_satisfiable(concept: Concept, tbox: Optional[TBox] = None) -> bool:
673
+ """Return True iff ``concept`` is satisfiable with respect to ``tbox`` —
674
+ the external-reasoner twin of :func:`unicode_logic_kit.dl.tableau.concept_satisfiable`,
675
+ but over the FULL ALCHQ + I + O fragment (inverse roles, nominals). Reduced
676
+ to :func:`_kb_consistent` via a single probe individual, exactly the way
677
+ the in-house tableau reduces it to one fresh branch node. The probe is
678
+ called by a name that no nominal or value restriction of ``concept`` or of
679
+ ``tbox`` mentions: a probe that shared the name of such an individual would
680
+ BE that individual, and the question would be asked about it and not about
681
+ some element.
682
+ """
683
+ _guard_inputs(tbox, ABox().assert_concept(_PROBE_INDIVIDUAL, concept))
684
+ _require_available()
685
+ probe = _fresh_individual(_PROBE_INDIVIDUAL, _concept_individual_names(concept).union(
686
+ *(_concept_individual_names(expression)
687
+ for expression in _tbox_class_expressions(tbox))))
688
+ return _kb_consistent(tbox, ABox().assert_concept(probe, concept))
689
+
690
+
691
+ def external_concept_unsatisfiable(concept: Concept, tbox: Optional[TBox] = None) -> bool:
692
+ """Return True iff ``concept`` is unsatisfiable with respect to ``tbox``."""
693
+ return not external_concept_satisfiable(concept, tbox)
694
+
695
+
696
+ def external_subsumes(sub: Concept, sup: Concept, tbox: Optional[TBox] = None) -> bool:
697
+ """Return True iff ``tbox`` entails ``sub ⊑ sup`` — the same ``sub ⊓ ¬sup``
698
+ unsatisfiability reduction :func:`unicode_logic_kit.dl.tableau.subsumes` uses.
699
+ """
700
+ return not external_concept_satisfiable(And(sub, Not(sup)), tbox)
701
+
702
+
703
+ def external_equivalent(c: Concept, d: Concept, tbox: Optional[TBox] = None) -> bool:
704
+ """Return True iff ``tbox`` entails ``c ≡ d`` (mutual subsumption)."""
705
+ return external_subsumes(c, d, tbox) and external_subsumes(d, c, tbox)
706
+
707
+
708
+ def external_abox_consistent(abox: ABox, tbox: Optional[TBox] = None) -> bool:
709
+ """Return True iff the knowledge base ``(tbox, abox)`` is consistent."""
710
+ return _kb_consistent(tbox, abox)
711
+
712
+
713
+ def _abox_with(abox: ABox, individual: str, concept: Concept) -> ABox:
714
+ """A copy of ``abox`` with one extra concept assertion ``individual : concept``
715
+ (used by :func:`external_instance_check`'s entailment reduction, mirroring
716
+ :func:`unicode_logic_kit.dl.tableau.instance_check`'s own copy-and-extend).
717
+
718
+ :meth:`~unicode_logic_kit.dl.tableau.ABox.copy`, not a field-by-field
719
+ reconstruction: this module is one of the kit's INDEPENDENT oracles, and
720
+ an oracle that silently drops an assertion kind the caller supplied
721
+ agrees with the in-house tableau for the wrong reason. (Field-by-field is
722
+ what this did until 0.30.0, and it dropped ``same_assertions`` /
723
+ ``negative_role_assertions`` the moment they existed.)
724
+ """
725
+ probe = abox.copy()
726
+ probe.assert_concept(individual, concept)
727
+ return probe
728
+
729
+
730
+ def external_instance_check(abox: ABox, individual: str, concept: Concept,
731
+ tbox: Optional[TBox] = None) -> bool:
732
+ """Return True iff ``(tbox, abox)`` entails ``individual : concept`` — the
733
+ same "does asserting the complement make it inconsistent" reduction
734
+ :func:`unicode_logic_kit.dl.tableau.instance_check` uses. Open-world, exactly
735
+ like that function: False means NOT entailed, not "entailed false".
736
+ """
737
+ return not external_abox_consistent(_abox_with(abox, individual, Not(concept)), tbox)
738
+
739
+
740
+ def _all_individuals(abox: ABox) -> Set[str]:
741
+ """Every individual name mentioned in ``abox`` (ANY assertion list) — the
742
+ SAME scan the tableau's sweeps and ``kb_to_fol(...).individuals`` read
743
+ (:func:`~unicode_logic_kit.dl.tableau._abox_individual_names`, driven by the
744
+ axiom-kind table), and NO anonymous fallback individual: an ABox that names
745
+ nobody has no one to retrieve or realize. (This used to fall back to an
746
+ invented ``"a"`` and report it as a member — ``TBox().add(Top(), A)`` over
747
+ an empty ABox "retrieved" ``{"a"}`` — and its own hand-written scan missed
748
+ the data assertions.)
749
+ """
750
+ return set(_abox_individual_names(abox))
751
+
752
+
753
+ def external_instance_retrieval(abox: ABox, concept: Concept,
754
+ tbox: Optional[TBox] = None) -> Set[str]:
755
+ """Return every individual of ``abox`` that ``(tbox, abox)`` entails is a
756
+ ``concept`` — sweeps :func:`external_instance_check` exactly like
757
+ :func:`unicode_logic_kit.dl.tableau.instance_retrieval` does.
758
+
759
+ The shared refusals run FIRST (:func:`_guard_inputs`): an ABox that names
760
+ nobody makes the sweep below call :func:`external_instance_check` zero
761
+ times, and a knowledge base the other entry points refuse got ``set()``.
762
+ """
763
+ _guard_inputs(tbox, abox, [concept])
764
+ return {ind for ind in sorted(_all_individuals(abox))
765
+ if external_instance_check(abox, ind, concept, tbox)}
766
+
767
+
768
+ def external_realize(abox: ABox, individual: str, vocabulary: List[Concept],
769
+ tbox: Optional[TBox] = None) -> List[Concept]:
770
+ """Return ``individual``'s most-specific concepts from ``vocabulary`` — the
771
+ same filter-then-drop-non-minimal reduction
772
+ :func:`unicode_logic_kit.dl.tableau.realize` uses. The shared refusals run
773
+ first (:func:`_guard_inputs`), since an empty ``vocabulary`` reaches no
774
+ HermiT call.
775
+ """
776
+ _guard_inputs(tbox, abox, vocabulary)
777
+ candidates = [c for c in vocabulary if external_instance_check(abox, individual, c, tbox)]
778
+ return [c for c in candidates
779
+ if not any(external_subsumes(d, c, tbox) and not external_subsumes(c, d, tbox)
780
+ for d in candidates)]
781
+
782
+
783
+ def external_realize_all(abox: ABox, vocabulary: List[Concept],
784
+ tbox: Optional[TBox] = None) -> Dict[str, List[Concept]]:
785
+ """Return :func:`external_realize` for every individual named in ``abox``
786
+ (``{}`` for an ABox that names none; the shared refusals run first, see
787
+ :func:`_guard_inputs`)."""
788
+ _guard_inputs(tbox, abox, vocabulary)
789
+ return {ind: external_realize(abox, ind, vocabulary, tbox)
790
+ for ind in sorted(_all_individuals(abox))}