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,339 @@
1
+ """HETS (the Heterogeneous Tool Set) as an opt-in prover backend.
2
+
3
+ :class:`HetsBackend` (registry name ``"hets"``) decides classical FOL and
4
+ MSFOL entailment by exporting the problem to CASL
5
+ (:mod:`unicode_logic_kit.fol.casl_export` — premises become axioms, the goal a
6
+ ``%implied`` conjecture), uploading it to a running ``hets-server`` REST
7
+ instance (:mod:`unicode_logic_kit.hets`), and mapping the per-goal result back
8
+ onto the kit's :class:`~unicode_logic_kit.atp.protocol.Verdict`:
9
+
10
+ =========== ========== =====================================================
11
+ Hets result Verdict Notes
12
+ =========== ========== =====================================================
13
+ ``Proved`` PROVED ``detail`` carries reasoner + comorphism provenance
14
+ ``Disproved`` REFUTED any reasoner may report it (live-verified: the
15
+ default SPASS route disproves simple non-theorems
16
+ too); ``darwin-non-fd`` is the DEDICATED finite
17
+ model finder for when the others stay Open
18
+ ``Open`` UNKNOWN ``reason="incomplete"`` — NEVER mapped to refuted
19
+ (this image's eprover/Vampire wrappers are broken
20
+ and always report Open; see ``hets/docker.py``)
21
+ =========== ========== =====================================================
22
+
23
+ What this buys: the Hets reasoner stable (SPASS, the darwins, MathServe
24
+ brokered provers, …) behind one backend, each answer stamped with the
25
+ comorphism Hets actually used (e.g. ``CASL2TPTP_FOF`` for SPASS,
26
+ ``CASL2SoftFOL`` for darwin) — and native many-sorted CASL, so kit MSFOL
27
+ problems go through WITHOUT the single-sort collapse every TPTP route needs.
28
+
29
+ **A typed reading that differs from the kit's is refused.** The CASL export is a
30
+ typed text: it infers the sort of every position (an unannotated constant, or the
31
+ value of a function, is declared at the sort of the position it is used in) and its
32
+ sorts are disjoint types, while the kit has ONE universe, sorts that are subsets of
33
+ it, and constants and function values that are in a sort only when a formula says
34
+ so. :class:`HetsBackend` decides, so for a problem on which the two readings differ
35
+ it answers ``UNKNOWN`` / ``"unsupported"`` with the reason in ``detail`` (the
36
+ checks are those of :func:`~unicode_logic_kit.atp.tptp_tff.check_typed_reading`, shared
37
+ with the TF0 and NXF writers) instead of a verdict about another problem; the
38
+ fof route (``backends=["vampire"], tff=False`` and the like) asks the kit's question
39
+ for those. The export itself (``to_casl_spec``) keeps writing the typed reading.
40
+ The sort the export gives every unsorted position is ``Thing``, or ``Thing1``, ``Thing2``, ...
41
+ when the problem has a sort spelled like it: a user sort of the default sort's name would
42
+ be one sort with it.
43
+
44
+ Cost model and chain policy: a Docker container start is minutes-expensive
45
+ (image pull even more so), exactly like the Isabelle route — so this backend
46
+ is NEVER part of any default chain and ``decide()`` NEVER starts a container
47
+ itself. It only talks to an already-running server found by
48
+ :func:`unicode_logic_kit.hets.discover_hets_url` (``$UFK_HETS_URL``, then
49
+ ``localhost:8000``); nothing reachable raises ``BackendUnavailable`` with
50
+ the start-it-yourself instructions, never a silent skip. Callers who want
51
+ managed lifecycle wrap their session in
52
+ :class:`unicode_logic_kit.hets.HetsContainer`.
53
+
54
+ ``decide()`` options (all keyword, via ``**options``): ``reasoner`` (Hets
55
+ prover identifier, default ``"SPASS"`` — the one verified reliable in the
56
+ official image; ``"darwin-non-fd"`` is the dedicated finite model finder
57
+ for disproof when other reasoners stay Open), ``translation`` (a comorphism
58
+ name from ``HetsClient.translations``, default: let Hets choose), ``url``
59
+ (skip discovery and use this server). The gate in
60
+ :func:`~unicode_logic_kit.atp.protocol.run_backend` asks
61
+ :meth:`HetsBackend.available_for` with the options of the call, so a call that
62
+ names ``url=`` is answered for THAT server (``GET /version`` on it), not for
63
+ ``$UFK_HETS_URL`` / localhost, which are only probed when the call names no
64
+ ``url=``.
65
+
66
+ :meth:`HetsBackend.check_consistency` is the extra non-protocol route over
67
+ ``POST /consistency-check``: it asks whether the PREMISES have a model at
68
+ all. It deliberately returns a plain dict (``consistent`` True/False/None +
69
+ provenance), not a :class:`Verdict` — Verdict's status axis answers
70
+ "entailed?", and overloading PROVED to mean "consistent" would corrupt
71
+ every consumer that treats PROVED as "goal follows".
72
+ """
73
+
74
+ import time
75
+ from typing import Dict, Optional, Sequence
76
+
77
+ from ..fol.nodes import Node
78
+ from .protocol import ProverBackend, Verdict, PROVED, REFUTED, UNKNOWN, ERROR
79
+
80
+ __all__ = ["HetsBackend"]
81
+
82
+ #: The reasoner used when the caller does not choose one. SPASS is the
83
+ #: verified-working prover of the official image (translation CASL2TPTP_FOF);
84
+ #: eprover and Vampire are broken there (always Open) — see hets/docker.py.
85
+ _DEFAULT_REASONER = "SPASS"
86
+
87
+ _SPEC_NAME = "KitProblem"
88
+
89
+
90
+ def _default_sort_for(formulas: Sequence[Node]) -> str:
91
+ """The sort the CASL export gives every unsorted position: ``Thing``, or the first of
92
+ ``Thing1``, ``Thing2``, ... that no sort of ``formulas`` is spelled like. A user sort of
93
+ the default sort's name would be one sort with it (the export refuses that), and the
94
+ kit's reading of an unsorted position is the whole universe, not a sort of the user's."""
95
+ taken = {n.sort for f in formulas for n in f.walk() if isinstance(getattr(n, "sort", None), str)}
96
+ candidate, i = "Thing", 0
97
+ while candidate in taken:
98
+ i += 1
99
+ candidate = f"Thing{i}"
100
+ return candidate
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Solver-version provenance (K1). HETS is an HTTP server, not a spawned
104
+ # binary, so the analogue of atp.protocol._binary_version's subprocess memo
105
+ # is one GET /version per discovered base_url, memoized process-wide via
106
+ # the already-existing but (until now) unused HetsClient.version().
107
+ # ---------------------------------------------------------------------------
108
+
109
+ _VERSION_CACHE: Dict[str, Optional[str]] = {}
110
+
111
+
112
+ def _hets_version(client, url: str) -> Optional[str]:
113
+ """``client.version()`` (``GET /version``), memoized per ``url`` for the
114
+ life of this process — every call after the first for the same
115
+ ``base_url`` returns the cached string (or cached ``None``) without a
116
+ second HTTP round trip. A failure degrades to ``None`` rather than
117
+ raising — this is best-effort provenance, never grounds to turn a sound
118
+ decide() result into an ERROR verdict, so this catches broadly: a real
119
+ ``HetsClient.version()`` only ever raises ``RuntimeError`` (server
120
+ dropped between the availability check and this call, a malformed
121
+ response — see ``hets.client``'s own contract), but ``client`` here is
122
+ whatever :meth:`HetsBackend.decide`/:meth:`HetsBackend.solver_version`
123
+ constructed (or, in a test double that stands in for the whole
124
+ ``HetsClient`` surface but has not modelled this one extra method,
125
+ might not even implement ``version()`` at all — an ``AttributeError``
126
+ must degrade exactly like a ``RuntimeError`` does, not propagate and
127
+ turn a working ``decide()`` call into a crash over a provenance nicety).
128
+ """
129
+ if url in _VERSION_CACHE:
130
+ return _VERSION_CACHE[url]
131
+ try:
132
+ version: Optional[str] = client.version()
133
+ except Exception: # noqa: BLE001 - best-effort provenance, must not sink a sound verdict
134
+ version = None
135
+ _VERSION_CACHE[url] = version
136
+ return version
137
+
138
+
139
+ def _prover_id(goal: dict):
140
+ """The reasoner identifier out of a normalized goal dict.
141
+
142
+ ``used_prover`` arrives as ``{"identifier": …, "name": …}`` from the
143
+ server; fall back to the raw value (or None) if the shape ever changes.
144
+ """
145
+ prover = goal.get("used_prover")
146
+ if isinstance(prover, dict):
147
+ return prover.get("identifier", prover.get("name"))
148
+ return prover
149
+
150
+
151
+ class HetsBackend(ProverBackend):
152
+ """CASL export + hets-server REST prove, results as kit Verdicts."""
153
+
154
+ name = "hets"
155
+ logics = frozenset({"fol"})
156
+ external = True # needs Docker (or a remote hets-server)
157
+
158
+ def available(self) -> bool:
159
+ """True iff a hets-server answers right now (no container start)."""
160
+ from ..hets import hets_available
161
+
162
+ return hets_available()
163
+
164
+ def available_for(self, options: dict) -> bool:
165
+ """Whether the server :meth:`decide` will talk to answers: the one named by
166
+ ``url=`` when the call names one (``decide`` then skips discovery), else
167
+ :meth:`available`. Never starts a container."""
168
+ url = options.get("url")
169
+ if url is None:
170
+ return self.available()
171
+ from ..hets.docker import _probe_health
172
+
173
+ return _probe_health(url)
174
+
175
+ def solver_version(self) -> Optional[str]:
176
+ """The reachable server's ``GET /version`` banner, via
177
+ :meth:`~unicode_logic_kit.hets.client.HetsClient.version` — memoized
178
+ per discovered ``base_url`` for the life of the process (see
179
+ :func:`_hets_version`). ``None`` when no server is currently
180
+ reachable (the same "never guess" discipline as :meth:`available`),
181
+ so callers such as
182
+ :func:`~unicode_logic_kit.eval.batch.batch_decide`'s cache key can
183
+ call this unconditionally, with no prior availability check.
184
+ """
185
+ from ..hets import HetsClient, discover_hets_url
186
+ from .protocol import BackendUnavailable
187
+
188
+ try:
189
+ url, _container = discover_hets_url()
190
+ except BackendUnavailable:
191
+ return None
192
+ return _hets_version(HetsClient(url), url)
193
+
194
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
195
+ timeout: int = 10000, **options) -> Verdict:
196
+ from ..fol.casl_export import to_casl_spec
197
+ from ..hets import HetsClient, discover_hets_url
198
+ from .tptp_tff import check_typed_reading
199
+
200
+ reasoner = options.pop("reasoner", _DEFAULT_REASONER)
201
+ translation = options.pop("translation", None)
202
+ url = options.pop("url", None)
203
+
204
+ # Solver-version provenance (K1): resolve it up front ONLY when the
205
+ # caller passed an explicit `url=` override — they already opted
206
+ # into contacting THAT specific server (see the module docstring's
207
+ # `url=` CAVEAT), so looking its version up before the CASL-fragment
208
+ # check below adds no network touch beyond what was already asked
209
+ # for, and even an out-of-fragment formula's UNKNOWN/"unsupported"
210
+ # verdict then carries it. Without an explicit `url=`, this stays
211
+ # None here: resolving it would mean running discover_hets_url()'s
212
+ # own network health probe for a formula that will never reach a
213
+ # prover, which test_out_of_fragment_is_unsupported_before_any_network
214
+ # (tests/test_hets_backend.py, unowned) deliberately forbids —
215
+ # discovery must never run before the fragment check for the
216
+ # default-discovery path (see that test's own docstring). In that
217
+ # path solver_version is instead resolved below, right after
218
+ # discover_hets_url() succeeds.
219
+ solver_version = _hets_version(HetsClient(url), url) if url is not None else None
220
+
221
+ default_sort = _default_sort_for(list(premises) + [formula])
222
+ try:
223
+ spec = to_casl_spec(list(premises), conjectures=[formula],
224
+ spec_name=_SPEC_NAME, default_sort=default_sort)
225
+ # The export is a typed text: it gives an unannotated constant or a
226
+ # function value the sort of the position it is used at, and its sorts
227
+ # are disjoint types. A decision must answer the kit's question, so a
228
+ # problem for which that reading differs is refused (see
229
+ # check_typed_reading), not answered about something else.
230
+ check_typed_reading(list(premises) + [formula], writer="hets",
231
+ unsorted_type=default_sort)
232
+ except (ValueError, NotImplementedError) as exc:
233
+ # Outside the CASL FOL/MSFOL fragment (modal node, sort
234
+ # conflict, free variable, …), or a typed reading that differs from
235
+ # the kit's: honestly unsupported, never a silent mistranslation.
236
+ return Verdict(UNKNOWN, self.name, reason="unsupported",
237
+ solver_version=solver_version,
238
+ detail=f"{type(exc).__name__}: {exc}")
239
+
240
+ if url is None:
241
+ url, _ = discover_hets_url() # raises BackendUnavailable
242
+ # Memoized per base_url (see _hets_version) — resolved only now
243
+ # (not above) because discovery itself must not run before the
244
+ # fragment check just above.
245
+ solver_version = _hets_version(HetsClient(url), url)
246
+
247
+ # Hets takes its budget in whole seconds; the HTTP timeout must
248
+ # outlast it (upload + translation + prover startup).
249
+ time_limit = max(1, timeout // 1000)
250
+ client = HetsClient(url, timeout=float(time_limit + 30))
251
+ # Memoized per base_url (see _hets_version) — the first decide() (or
252
+ # solver_version()) call for this server pays one GET /version, every
253
+ # later one for the same url is free (including the lookup above,
254
+ # under either path: this is the SAME cache, keyed by url).
255
+
256
+ try:
257
+ start = time.perf_counter()
258
+ iri = client.upload(spec, "kit_problem.casl")
259
+ goals = client.prove(iri, _SPEC_NAME, reasoner=reasoner,
260
+ translation=translation,
261
+ time_limit=time_limit)
262
+ elapsed = time.perf_counter() - start
263
+ except RuntimeError as exc:
264
+ return Verdict(ERROR, self.name, reason="infra",
265
+ solver_version=solver_version,
266
+ detail=f"{type(exc).__name__}: {exc}")
267
+
268
+ if len(goals) != 1:
269
+ # Exactly one %implied conjecture was emitted, so anything else
270
+ # means the server answered a different question than asked.
271
+ return Verdict(ERROR, self.name, reason="infra",
272
+ solver_version=solver_version,
273
+ detail=f"hets returned {len(goals)} goal results "
274
+ "for a single-conjecture spec")
275
+
276
+ goal = goals[0]
277
+ provenance = (f"reasoner={_prover_id(goal)}, "
278
+ f"translation={goal.get('used_translation')}")
279
+ result = (goal.get("result") or "").strip()
280
+
281
+ if result == "Proved":
282
+ return Verdict(PROVED, self.name, wall_time=elapsed,
283
+ solver_version=solver_version, detail=provenance)
284
+ if result == "Disproved":
285
+ return Verdict(REFUTED, self.name, wall_time=elapsed,
286
+ solver_version=solver_version, detail=provenance)
287
+ # "Open" and anything unrecognised: not settled. Open is NOT a
288
+ # refutation (and in this image often just a broken wrapper).
289
+ tail = (goal.get("prover_output") or "").strip()[-200:]
290
+ return Verdict(UNKNOWN, self.name, reason="incomplete",
291
+ wall_time=elapsed, solver_version=solver_version,
292
+ detail=f"{provenance}, result={result or 'absent'}"
293
+ + (f", output tail: {tail}" if tail else ""))
294
+
295
+ def check_consistency(self, premises: Sequence[Node],
296
+ timeout: int = 10000, **options) -> dict:
297
+ """Ask ``POST /consistency-check``: do the premises have a model?
298
+
299
+ Returns ``{"consistent": True | False | None, "result": <verbatim>,
300
+ "reasoner": …, "translation": …, "wall_time": …}`` — ``None`` when
301
+ the checker did not settle it. Raises ``BackendUnavailable`` when no
302
+ server is reachable, ``ValueError``/``NotImplementedError`` when
303
+ the premises leave the CASL fragment, and ``RuntimeError`` when the
304
+ server itself fails mid-flight (HTTP error, ``*** Error`` body,
305
+ malformed JSON — ``HetsClient``'s contract, passed through): unlike
306
+ ``decide`` there is no Verdict envelope to absorb ANY of these, so
307
+ every failure raises rather than being half-mapped.
308
+ """
309
+ from ..fol.casl_export import to_casl_spec
310
+ from ..hets import HetsClient, discover_hets_url
311
+ from .tptp_tff import check_typed_reading
312
+
313
+ reasoner = options.pop("reasoner", "darwin-non-fd")
314
+ translation = options.pop("translation", None)
315
+ url = options.pop("url", None)
316
+
317
+ default_sort = _default_sort_for(list(premises))
318
+ spec = to_casl_spec(list(premises), spec_name=_SPEC_NAME, default_sort=default_sort)
319
+ # Same refusal as decide(): a consistency answer about a typed reading that
320
+ # differs from the kit's is an answer about another problem.
321
+ check_typed_reading(list(premises), writer="hets", unsorted_type=default_sort)
322
+ if url is None:
323
+ url, _ = discover_hets_url()
324
+ time_limit = max(1, timeout // 1000)
325
+ client = HetsClient(url, timeout=float(time_limit + 30))
326
+
327
+ start = time.perf_counter()
328
+ iri = client.upload(spec, "kit_consistency.casl")
329
+ goals = client.consistency_check(iri, _SPEC_NAME, reasoner=reasoner,
330
+ time_limit=time_limit)
331
+ elapsed = time.perf_counter() - start
332
+
333
+ result = (goals[0].get("result") or "").strip() if goals else ""
334
+ consistent = {"Consistent": True, "Inconsistent": False}.get(result)
335
+ first = goals[0] if goals else {}
336
+ return {"consistent": consistent, "result": result,
337
+ "reasoner": _prover_id(first),
338
+ "translation": first.get("used_translation"),
339
+ "wall_time": elapsed}
@@ -0,0 +1,120 @@
1
+ """The ↓ binder's "route B" combinator: down_is_valid + KripkeEnumBackend (N1).
2
+
3
+ ``fol.modal_translation.down_is_valid`` and ``atp.kripke_enum.KripkeEnumBackend``
4
+ are each already a complete, independently-correct decision route on their own
5
+ half of the verdict space for full hybrid logic H(@,↓):
6
+
7
+ * ``down_is_valid`` (the standard translation + a direct Z3 ``Solver()`` call)
8
+ can PROVE validity (Z3 ``unsat`` of the negated goal) but, by design, never
9
+ claims REFUTED — see its own docstring for why that is a deliberate
10
+ completeness sacrifice, not a soundness one.
11
+ * ``KripkeEnumBackend`` (bounded finite-Kripke-model enumeration) can REFUTE
12
+ validity with a concrete, ``satisfies_modal``-verified countermodel, but
13
+ exhausting its search space never proves anything (H(@,↓) — indeed modal
14
+ logic generally — has no small-model property bounding a countermodel to
15
+ ``max_worlds`` worlds).
16
+
17
+ Together they cover exactly the two halves of what H(@,↓) validity's
18
+ undecidability still leaves DECIDABLE-in-the-relevant-direction: co-r.e.
19
+ (refutable, via search) and — for the instances Z3 actually closes —
20
+ provable (via the meaning-preserving standard translation, Areces/
21
+ Blackburn/Marx 1999). Neither route alone can decide the fragment; running
22
+ BOTH and reporting whichever one succeeds is the honest, maximal answer this
23
+ kit can give in one call, and it is exactly what
24
+ ``docs/guide/hybrid.md``'s existing tableau-vs-Z3 pattern for H(@) already
25
+ models for the decidable fragment. See ``fol._hybrid_nodes``' module
26
+ docstring for the three-route architecture this module's function is the
27
+ "route B" entry point for (route A is ``semantics.kripke.satisfies_modal``
28
+ on a model the caller already has).
29
+
30
+ This is a thin combinator over two already-complete, already-tested routes:
31
+ no new semantic rule is added here, and no ``Verdict`` this module returns
32
+ carries a status either underlying route would not have produced on its own.
33
+ """
34
+
35
+ from typing import Dict, Optional
36
+
37
+ from ..fol.nodes import Node
38
+ from ..fol.modal_translation import down_is_valid
39
+ from .kripke_enum import KripkeEnumBackend
40
+ from .protocol import PROVED, REFUTED, UNKNOWN, Verdict
41
+
42
+ __all__ = ["down_decide"]
43
+
44
+
45
+ def down_decide(formula: Node, frame: str = "K", timeout: int = 10000,
46
+ *, systems: Optional[Dict[str, str]] = None,
47
+ max_worlds: int = 3, max_atoms: Optional[int] = None,
48
+ max_models: int = 200000) -> Verdict:
49
+ """Decide a hybrid-modal ``formula`` (H(@,↓), ``Down``/↓ included) over ``frame``.
50
+
51
+ Tries :func:`~unicode_logic_kit.fol.modal_translation.down_is_valid` first
52
+ (fast: one Z3 call on a small translated formula). If it returns
53
+ ``PROVED``, that Verdict is returned as-is. Otherwise — ``UNKNOWN`` from
54
+ ``down_is_valid``, which NEVER claims ``REFUTED`` on its own — this falls
55
+ back to :class:`~unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend`'s
56
+ bounded finite-model search for an actual countermodel:
57
+
58
+ * a countermodel found → ``REFUTED``, carrying that model (already
59
+ independently confirmed by ``satisfies_modal`` — see
60
+ :func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search`'s own
61
+ soundness argument — this function performs no re-verification of its
62
+ own beyond calling that route);
63
+ * the search space exhausted, or its budget ran out, or the formula falls
64
+ outside the propositional/ground modal fragment ``satisfies_modal``
65
+ understands → ``UNKNOWN``, with BOTH routes' ``detail`` text combined
66
+ (so a caller can see whether Z3 timed out / found nothing to prove, and
67
+ separately why the bounded search did not find a countermodel either).
68
+
69
+ This never returns ``REFUTED`` for a formula ``down_is_valid`` alone
70
+ reported ``PROVED`` for (only one of the two branches ever runs — see
71
+ above), and it never returns ``PROVED`` from anywhere but
72
+ ``down_is_valid`` (:class:`KripkeEnumBackend` is refutation-only and never
73
+ reports ``PROVED`` either).
74
+
75
+ Args:
76
+ formula: the hybrid-modal formula (with or without ``Down``/↓ — a
77
+ plain H(@) formula is handled identically, just more
78
+ conservatively on the PROVED side than
79
+ :func:`~unicode_logic_kit.fol.modal_translation.hybrid_is_valid`
80
+ would be for that decidable fragment).
81
+ frame: the alethic frame name, shared by both routes (``down_is_valid``
82
+ for the frame axioms, ``KripkeEnumBackend`` for the relation
83
+ conditions it enumerates over — see :mod:`unicode_logic_kit.fol.frames`).
84
+ timeout: milliseconds, the limit of the whole call. ``down_is_valid``'s
85
+ Z3 call runs under it, and the bounded search that follows is
86
+ given what is left of it (at least one millisecond), ending at a
87
+ candidate model with ``reason="timeout"`` when that runs out — on
88
+ top of its own budget, ``max_worlds``/``max_atoms``/``max_models``,
89
+ below.
90
+ systems, max_worlds, max_atoms, max_models: forwarded verbatim to
91
+ :class:`KripkeEnumBackend`'s ``decide`` (equivalently,
92
+ :func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search`) — see
93
+ their docstrings.
94
+ """
95
+ proved = down_is_valid(formula, frame=frame, timeout=timeout)
96
+ if proved.status == PROVED:
97
+ return proved
98
+
99
+ enum_options = {"frame": frame, "max_worlds": max_worlds,
100
+ "max_atoms": max_atoms, "max_models": max_models}
101
+ if systems is not None:
102
+ enum_options["systems"] = systems
103
+ left = max(1, int(timeout - proved.wall_time * 1000))
104
+ enum_verdict = KripkeEnumBackend().decide(formula, timeout=left, **enum_options)
105
+
106
+ if enum_verdict.status == REFUTED:
107
+ return Verdict(
108
+ REFUTED, "down_decide", logic="hybrid",
109
+ wall_time=proved.wall_time + enum_verdict.wall_time,
110
+ countermodel=enum_verdict.countermodel,
111
+ detail=(f"down_is_valid: {proved.reason} ({proved.detail}); "
112
+ f"kripke-enum: {enum_verdict.detail}"),
113
+ )
114
+ return Verdict(
115
+ UNKNOWN, "down_decide", logic="hybrid",
116
+ reason=enum_verdict.reason or proved.reason,
117
+ wall_time=proved.wall_time + enum_verdict.wall_time,
118
+ detail=(f"down_is_valid: {proved.reason} ({proved.detail}); "
119
+ f"kripke-enum: {enum_verdict.reason} ({enum_verdict.detail})"),
120
+ )