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,566 @@
1
+ """Five per-logic :class:`~unicode_logic_kit.atp.protocol.ProverBackend` adapters.
2
+
3
+ Each substructural / non-classical logic below already has its own
4
+ hand-built decision procedure or bounded search elsewhere in the kit
5
+ (:mod:`atp.lj`, :mod:`atp.lambek`, :mod:`atp.linear`,
6
+ :mod:`semantics.relevant`, :mod:`fol.modal_translation`). This module wraps
7
+ each one in the uniform :class:`~unicode_logic_kit.atp.protocol.Verdict`
8
+ contract WITHOUT pretending the five share one shape — they do not, so each
9
+ adapter is reasoned about on its own terms (what a ``True``/``False``/
10
+ ``None`` from the underlying routine actually PROVES, versus where it is
11
+ merely "nothing found within the bound") rather than registered
12
+ mechanically off one template:
13
+
14
+ * :class:`IntBackend` (``logic="intuitionistic"``) — Dyckhoff's G4ip via
15
+ :func:`~unicode_logic_kit.atp.lj.int_prove`, a genuine terminating decision
16
+ procedure on the propositional fragment. REFUTED carries a best-effort
17
+ Kripke witness from
18
+ :func:`~unicode_logic_kit.semantics.intuitionistic.int_countermodel`.
19
+ * :class:`LambekBackend` (``logic="lambek"``) — the Lambek calculus L via
20
+ :func:`~unicode_logic_kit.atp.lambek.lambek_derivable`. ``premises`` is the
21
+ LITERAL ORDERED antecedent (word order is the entire point of L), so the
22
+ shared :func:`~unicode_logic_kit.atp.protocol._implication` And-fold is
23
+ never used here — L has no exchange/weakening to make a conjunction
24
+ interchangeable with its conjuncts in the first place.
25
+ * :class:`IllBackend` (``logic="ill"``) — intuitionistic linear logic via
26
+ :func:`~unicode_logic_kit.atp.linear.ill_derivable`: a complete decision
27
+ procedure on the ``!``-free fragment, a depth/step-bounded search once
28
+ ``!`` occurs (see :func:`~unicode_logic_kit.atp.linear._has_bang`).
29
+ * :class:`RelevantBackend` (``logic="relevant"``) — the relevant logic B via
30
+ :func:`~unicode_logic_kit.semantics.relevant.rel_countermodel`. NEVER
31
+ PROVED: ``rel_valid``/``rel_countermodel`` are sound but INCOMPLETE (a
32
+ bounded Routley-Meyer search), so "no countermodel found" is honestly
33
+ UNKNOWN(bound_hit). A genuine PROVED route needs a real Isabelle proof
34
+ (:mod:`hol.isabelle_relevant`'s ``to_isabelle_relevant``/``battery_proof``
35
+ driven as a subprocess) and is an explicit out-of-scope follow-up.
36
+ * :class:`HybridBackend` (``logic="hybrid"``) — hybrid modal logic H(@) via
37
+ the standard translation (:mod:`fol.modal_translation`) into classical
38
+ FOL, closed under the frame axioms and decided by a per-call tracked Z3
39
+ ``Solver()`` (:func:`~unicode_logic_kit.atp.protocol._z3_track_and_check`,
40
+ the same routine :class:`~unicode_logic_kit.atp.protocol.Z3Backend` uses) —
41
+ deliberately NOT
42
+ :func:`~unicode_logic_kit.fol.modal_translation.hybrid_is_valid` (nor the
43
+ :func:`~unicode_logic_kit.atp.z3_models.is_valid` underneath it), whose bare
44
+ bool collapses a genuine countermodel (SAT) and a solver timeout/unknown
45
+ into the same ``False``.
46
+
47
+ Routing: :func:`unicode_logic_kit.api.prove`'s ``logic="auto"`` detects
48
+ ``"lambek"`` / ``"ill"`` / ``"hybrid"`` from an unambiguous syntactic
49
+ marker — each is the sole occupant of its own parser mode's node types
50
+ (Product/Under/Over; the ``linear`` mode's Tensor/With/OPlus/LinearImplies/
51
+ OfCourse/One/Top/Zero; Nominal/At). Intuitionistic and relevant logic reuse
52
+ the PLAIN classical propositional AST with no syntactic marker of their
53
+ own, so ``logic=`` must always be given explicitly for those two —
54
+ auto-detection never guesses between a "classical", "intuitionistic" and
55
+ "relevant" reading of the same formula.
56
+ """
57
+
58
+ import time
59
+ from typing import Optional, Sequence
60
+
61
+ from .._deadline import instant as _instant, run_until as _run_until
62
+ from ..fol.nodes import Node
63
+ from ._substructural_input import ILL, LAMBEK, Calculus, unreadable_reason
64
+ from .protocol import (
65
+ ProverBackend, Verdict, PROVED, REFUTED, UNKNOWN,
66
+ _timed, _implication, _z3_track_and_check, _z3_model_assignment,
67
+ )
68
+
69
+ __all__ = [
70
+ "IntBackend", "LambekBackend", "IllBackend", "RelevantBackend", "HybridBackend",
71
+ ]
72
+
73
+
74
+ def _unreadable_refusal(logic: str, calculus: Calculus, formula: Node,
75
+ premises: Sequence[Node]) -> Optional[str]:
76
+ """Why a propositional substructural calculus cannot read this input, or ``None``.
77
+
78
+ Intuitionistic linear logic and the Lambek calculus read the connectives they have rules
79
+ for (``calculus.glyphs``) and atoms: no individuals, no sorts, no quantifiers, no counting,
80
+ no identity, and no connective of another logic. An atom over terms is one category (a
81
+ sound reading, argued in ``atp/_substructural_input.py``); any other node read as one more
82
+ opaque category would make a derivability verdict about a DIFFERENT formula
83
+ (``∀x P(x)`` does not derive ``P(alpha)`` there, and it does in every first-order
84
+ reading; ``And(A, B)`` does not derive ``A``, and it does classically), so the node is
85
+ refused by name instead and the text says what to do.
86
+ """
87
+ return unreadable_reason([*premises, formula], logic, calculus)
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Witness serializers — IntKripkeModel / RelevantModel are not JSON-native.
92
+ # ---------------------------------------------------------------------------
93
+
94
+ def _int_kripke_witness_to_dict(model, world: int) -> dict:
95
+ """JSON-able witness for an
96
+ :class:`~unicode_logic_kit.semantics.intuitionistic.IntKripkeModel`
97
+ countermodel: the up-set order, the monotone valuation, and (for a
98
+ first-order witness, which :class:`IntBackend` never actually reaches —
99
+ see its docstring) the per-world domains.
100
+ """
101
+ out = {
102
+ "kind": "intuitionistic_kripke",
103
+ "world": world,
104
+ "upset": {str(w): sorted(ws) for w, ws in model.upset.items()},
105
+ "valuation": {k: sorted(ws) for k, ws in model.valuation.items()},
106
+ }
107
+ if model.domains is not None:
108
+ out["domains"] = {str(w): sorted(ds) for w, ds in model.domains.items()}
109
+ return out
110
+
111
+
112
+ def _relevant_model_to_dict(model, world: str) -> dict:
113
+ """JSON-able witness for a
114
+ :class:`~unicode_logic_kit.semantics.relevant.RelevantModel` countermodel.
115
+
116
+ ``RelevantModel`` is frozen/hashable but not JSON-native (its
117
+ ``star``/``valuation`` are the module's own ``_FrozenMap``); this
118
+ unpacks every field the constructor takes into plain lists/dicts, so
119
+ both ``json.dumps`` and rebuilding a ``RelevantModel`` from the dict
120
+ round-trip cleanly (the differential test exercises exactly that).
121
+ """
122
+ return {
123
+ "kind": "relevant_routley_meyer",
124
+ "world": world,
125
+ "worlds": list(model.worlds),
126
+ "normal": sorted(model.normal),
127
+ "star": {w: model.star[w] for w in model.worlds},
128
+ "R": sorted([list(triple) for triple in model.R]),
129
+ "valuation": {k: sorted(ws) for k, ws in model.valuation.items()},
130
+ }
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # Intuitionistic propositional logic
135
+ # ---------------------------------------------------------------------------
136
+
137
+ class IntBackend(ProverBackend):
138
+ """Propositional intuitionistic logic via Dyckhoff's G4ip (:mod:`atp.lj`).
139
+
140
+ ``int_prove`` is a genuine, terminating decision procedure on the
141
+ propositional fragment (see its own docstring): PROVED and REFUTED are
142
+ both definitive. Quantified input is out of scope for G4ip (propositional
143
+ only — :func:`~unicode_logic_kit.atp.lj._reject_quantified`); it comes back
144
+ UNKNOWN(unsupported) rather than raising, so a batch run over mixed-arity
145
+ formulas records the gap instead of crashing. A REFUTED verdict's witness
146
+ is a best-effort call to
147
+ :func:`~unicode_logic_kit.semantics.intuitionistic.int_countermodel` on the
148
+ folded goal ``(∧ premises) → formula`` — the REFUTED status itself is
149
+ always definitive (it comes from ``int_prove``, not from this search),
150
+ but the witness search is NOT: ``int_countermodel``'s bounded Kripke
151
+ search at its default ``max_worlds=3`` can still miss a countermodel for
152
+ some genuine non-theorems, since the finite-model-property bound grows
153
+ with the formula (see ``int_valid``'s own docstring for a worked
154
+ example needing 4 worlds). The ``Verdict`` contract permits
155
+ ``countermodel=None`` for exactly this reason, so a bound miss never
156
+ turns into a broken Verdict — only a REFUTED with no witness attached.
157
+
158
+ The call's ``timeout`` bounds both searches
159
+ (:func:`~unicode_logic_kit._deadline.run_until`): an ``int_prove`` that has not
160
+ finished is UNKNOWN(timeout), and a witness search that has not finished
161
+ leaves a REFUTED verdict without its witness. An ``int_prove`` that spends its
162
+ step budget (the calculus terminates, but the number of steps is exponential in
163
+ the nesting of implications) is UNKNOWN(bound_hit): a spent budget is a bound,
164
+ not a failure of the backend.
165
+ """
166
+
167
+ name = "intuitionistic"
168
+ logics = frozenset({"intuitionistic"})
169
+ external = False
170
+
171
+ def available(self) -> bool:
172
+ return True
173
+
174
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
175
+ timeout: int = 10000, **options) -> Verdict:
176
+ from .lj import int_prove, _MAX_STEPS
177
+ from ..semantics.intuitionistic import int_countermodel
178
+
179
+ premises = list(premises)
180
+ cm_kwargs = {k: options.pop(k) for k in
181
+ ("max_worlds", "domain_elements", "max_steps") if k in options}
182
+ deadline = _instant(timeout)
183
+ started = time.perf_counter()
184
+ try:
185
+ (finished, proved), elapsed = _timed(
186
+ lambda: _run_until(deadline, lambda: int_prove(premises, formula)))
187
+ except NotImplementedError as exc:
188
+ return Verdict(UNKNOWN, self.name, logic="intuitionistic",
189
+ reason="unsupported", detail=str(exc))
190
+ except RecursionError:
191
+ # The search recurses once per rule it applies along a branch of the derivation,
192
+ # so the interpreter's recursion limit is a bound of it, next to the step budget:
193
+ # a formula whose nesting is small but whose search is long (nested Peirce
194
+ # formulas) reaches it. Nothing was decided.
195
+ import sys
196
+ return Verdict(UNKNOWN, self.name, logic="intuitionistic", reason="bound_hit",
197
+ wall_time=time.perf_counter() - started,
198
+ detail="the proof search recursed deeper than the interpreter's "
199
+ f"recursion limit ({sys.getrecursionlimit()}) allows before "
200
+ "it decided the sequent: no verdict was reached")
201
+ except RuntimeError as exc:
202
+ # The prover's step counter ran out; any other RuntimeError is a failure.
203
+ if "step budget" not in str(exc):
204
+ raise
205
+ return Verdict(UNKNOWN, self.name, logic="intuitionistic", reason="bound_hit",
206
+ wall_time=time.perf_counter() - started,
207
+ detail=f"the prover used up its step budget ({_MAX_STEPS} steps) "
208
+ "before it decided the sequent: G4ip terminates, but the "
209
+ "number of steps is exponential in the nesting of "
210
+ "implications, so no verdict was reached")
211
+ if not finished:
212
+ # ``int_prove`` is a decision procedure, but its cost is exponential in the formula
213
+ return Verdict(UNKNOWN, self.name, logic="intuitionistic", reason="timeout",
214
+ wall_time=elapsed,
215
+ detail=f"no verdict within the {timeout} ms limit")
216
+ if proved:
217
+ return Verdict(PROVED, self.name, logic="intuitionistic", wall_time=elapsed)
218
+
219
+ # The verdict is settled; the witness is a second, best-effort search that gets what
220
+ # is left of the limit, and a REFUTED answer needs no witness (``countermodel=None``).
221
+ goal = _implication(formula, premises)
222
+ found, cm = _run_until(deadline, lambda: int_countermodel(goal, **cm_kwargs))
223
+ witness = _int_kripke_witness_to_dict(*cm) if found and cm is not None else None
224
+ return Verdict(REFUTED, self.name, logic="intuitionistic", wall_time=elapsed,
225
+ countermodel=witness)
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # The Lambek calculus L
230
+ # ---------------------------------------------------------------------------
231
+
232
+ class LambekBackend(ProverBackend):
233
+ """The Lambek calculus L via :mod:`atp.lambek` — a complete, terminating
234
+ decision procedure (see that module's docstring: every rule's premises
235
+ are strictly smaller than its conclusion, so ``lambek_prove`` returning
236
+ ``None`` PROVES underivability).
237
+
238
+ ``premises`` is read as the sequence's LITERAL ORDER, never folded
239
+ through the shared ``∧``-based :func:`~unicode_logic_kit.atp.protocol._implication`
240
+ helper: L drops exchange along with weakening/contraction, so
241
+ ``A, A\\B ⊢ B`` and ``A\\B, A ⊢ B`` are different sequents with different
242
+ answers — collapsing ``premises`` into an unordered classical conjunction
243
+ would silently erase exactly the distinction this calculus exists to
244
+ draw (the regression battery in ``tests/test_logic_backends.py`` pins
245
+ this pair directly). Every PROVED verdict is re-validated internally by
246
+ :func:`~unicode_logic_kit.atp.lambek.check_lambek_proof` (inside
247
+ ``lambek_prove``), so a search bug can only lose proofs, never invent
248
+ one. A REFUTED verdict's ``countermodel`` is always ``None`` — L has no
249
+ semantic model theory in this kit, only the syntactic decision procedure
250
+ (which is itself exhaustive, so ``None`` from the search IS the
251
+ refutation, not an admission of incompleteness).
252
+
253
+ The calculus reads the connectives ``• \\ /`` and atoms; an atom over terms is one
254
+ category. Every other node is refused by name, UNKNOWN(unsupported): a quantifier (sorted
255
+ or not), a counting or cardinality node, a sorted constant, an equality atom and a node of
256
+ another logic (``And``, ``Not``, ``Box``, ...) have no rule in L, and an opaque category
257
+ in their place would answer about another formula. So is an EMPTY premise list: L has no
258
+ sequent with an empty antecedent (Lambek's restriction), so there is nothing to decide,
259
+ and a backend states that as an answer instead of raising.
260
+
261
+ The call's ``timeout`` bounds the search (:func:`~unicode_logic_kit._deadline.run_until`):
262
+ the search is exhaustive and terminating, and its cost is exponential in the sequent, so a
263
+ search that has not finished at the limit is UNKNOWN(timeout), never a refutation.
264
+ """
265
+
266
+ name = "lambek"
267
+ logics = frozenset({"lambek"})
268
+ external = False
269
+
270
+ def available(self) -> bool:
271
+ return True
272
+
273
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
274
+ timeout: int = 10000, **options) -> Verdict:
275
+ from .lambek import lambek_prove
276
+
277
+ premises = list(premises)
278
+ deadline = _instant(timeout)
279
+ if not premises:
280
+ return Verdict(UNKNOWN, self.name, logic="lambek", reason="unsupported",
281
+ detail="lambek: decide() needs a nonempty, ORDERED premises "
282
+ "sequence — the Lambek calculus has no sequent with an "
283
+ "empty antecedent (Lambek's own restriction; see "
284
+ "atp.lambek's module docstring), so there is no notion "
285
+ "of unconditional validity to fall back to. Give the "
286
+ "premises the goal is read from, in order.")
287
+ refusal = _unreadable_refusal("lambek", LAMBEK, formula, premises)
288
+ if refusal is not None:
289
+ return Verdict(UNKNOWN, self.name, logic="lambek", reason="unsupported",
290
+ detail=refusal)
291
+
292
+ try:
293
+ (finished, derivation), elapsed = _timed(
294
+ lambda: _run_until(deadline, lambda: lambek_prove(premises, formula)))
295
+ except NotImplementedError as exc:
296
+ return Verdict(UNKNOWN, self.name, logic="lambek",
297
+ reason="unsupported", detail=str(exc))
298
+ if not finished:
299
+ # The search is exhaustive and terminating, but its cost is exponential in the sequent.
300
+ return Verdict(UNKNOWN, self.name, logic="lambek", reason="timeout",
301
+ wall_time=elapsed,
302
+ detail=f"no verdict within the {timeout} ms limit")
303
+ if derivation is not None:
304
+ return Verdict(PROVED, self.name, logic="lambek", wall_time=elapsed)
305
+ return Verdict(REFUTED, self.name, logic="lambek", wall_time=elapsed,
306
+ countermodel=None,
307
+ detail="no cut-free L derivation exists — lambek_prove's "
308
+ "backward search is exhaustive and terminating "
309
+ "(every rule strictly shrinks the sequent), so this "
310
+ "is a genuine syntactic refutation, not a bound")
311
+
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # Intuitionistic linear logic (ILL)
315
+ # ---------------------------------------------------------------------------
316
+
317
+ class IllBackend(ProverBackend):
318
+ """Intuitionistic linear logic (ILL) via :mod:`atp.linear`.
319
+
320
+ The cut-free backward search is a complete, terminating decision
321
+ procedure on the ``!``-FREE fragment (every rule strictly shrinks the
322
+ sequent there — see that module's docstring), so PROVED/REFUTED are both
323
+ definitive when no ``!`` occurs (checked via
324
+ :func:`~unicode_logic_kit.atp.linear._has_bang` on ``formula`` and every
325
+ premise) AND the depth bound in force is at least the sequent's own
326
+ "safe" bound (``total`` size when ``!``-free, ``2*total + 4`` once ``!``
327
+ occurs — exactly :func:`~unicode_logic_kit.atp.linear.ill_prove`'s own
328
+ default). A caller-supplied ``max_depth`` can shrink the search below
329
+ that safe bound (e.g. to force the bounded path deterministically for
330
+ testing); a ``False`` result from a search truncated below the safe
331
+ bound proves nothing, so it is reported UNKNOWN(bound_hit) too, never
332
+ REFUTED. Once ``!`` occurs, the contraction rule ``!C`` can GROW a
333
+ sequent, so the search is depth/step-bounded there regardless, and a
334
+ ``False`` means only "no derivation found within the bound" — reported
335
+ as UNKNOWN(bound_hit), NEVER REFUTED, exactly matching
336
+ :func:`~unicode_logic_kit.atp.linear.ill_prove`'s own honesty contract.
337
+
338
+ The calculus reads the connectives ``⊗ & ⊕ ⊸ ! 𝟙 ⊤ 𝟘`` and atoms; an atom over terms is
339
+ one category. Every other node is refused by name, UNKNOWN(unsupported): a quantifier
340
+ (sorted or not), a counting or cardinality node, a sorted constant, an equality atom and a
341
+ node of another logic (``And``, ``Not``, ``Box``, ...) have no rule in ILL, and an opaque
342
+ category in their place would answer about another formula.
343
+
344
+ The call's ``timeout`` bounds the search (:func:`~unicode_logic_kit._deadline.run_until`):
345
+ even the ``!``-free search, a complete decision procedure, is exponential in the sequent,
346
+ so a search that has not finished at the limit is UNKNOWN(timeout), never a refutation.
347
+ """
348
+
349
+ name = "ill"
350
+ logics = frozenset({"ill"})
351
+ external = False
352
+
353
+ def available(self) -> bool:
354
+ return True
355
+
356
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
357
+ timeout: int = 10000, **options) -> Verdict:
358
+ from .linear import ill_derivable, _has_bang, _size
359
+
360
+ premises = list(premises)
361
+ deadline = _instant(timeout)
362
+ refusal = _unreadable_refusal("ill", ILL, formula, premises)
363
+ if refusal is not None:
364
+ return Verdict(UNKNOWN, self.name, logic="ill", reason="unsupported",
365
+ detail=refusal)
366
+ max_depth = options.pop("max_depth", None)
367
+ max_steps = options.pop("max_steps", 200000)
368
+ bang = _has_bang(formula) or any(_has_bang(p) for p in premises)
369
+ # The same "safe" depth ill_prove itself falls back to when max_depth
370
+ # is None (see its docstring) — a smaller CALLER-supplied max_depth
371
+ # is an intentionally truncated search, not a complete one.
372
+ total = sum(_size(p) for p in premises) + _size(formula)
373
+ safe_depth = 2 * total + 4 if bang else total
374
+ bounded = max_depth is not None and max_depth < safe_depth
375
+
376
+ try:
377
+ (finished, proved), elapsed = _timed(
378
+ lambda: _run_until(deadline, lambda: ill_derivable(
379
+ premises, formula, max_depth=max_depth, max_steps=max_steps)))
380
+ except NotImplementedError as exc:
381
+ return Verdict(UNKNOWN, self.name, logic="ill",
382
+ reason="unsupported", detail=str(exc))
383
+ if not finished:
384
+ # Even the !-free search, a complete decision procedure, is exponential in the sequent.
385
+ return Verdict(UNKNOWN, self.name, logic="ill", reason="timeout",
386
+ wall_time=elapsed,
387
+ detail=f"no verdict within the {timeout} ms limit")
388
+ if proved:
389
+ return Verdict(PROVED, self.name, logic="ill", wall_time=elapsed)
390
+ if bang or bounded:
391
+ return Verdict(UNKNOWN, self.name, logic="ill", reason="bound_hit",
392
+ wall_time=elapsed,
393
+ detail="no ILL derivation found within max_depth/"
394
+ "max_steps — the !-bearing fragment's search "
395
+ "is bounded, not complete, and/or max_depth "
396
+ "was set below the sequent's own safe bound, "
397
+ "so this is honestly unknown, never a refutation")
398
+ return Verdict(REFUTED, self.name, logic="ill", wall_time=elapsed,
399
+ countermodel=None,
400
+ detail="the !-free fragment's search is a complete "
401
+ "decision procedure; no cut-free ILL derivation "
402
+ "exists (a syntactic refutation — ILL has no "
403
+ "semantic model theory in this kit)")
404
+
405
+
406
+ # ---------------------------------------------------------------------------
407
+ # Relevant logic B
408
+ # ---------------------------------------------------------------------------
409
+
410
+ class RelevantBackend(ProverBackend):
411
+ """The relevant logic B via the bounded Routley-Meyer search in
412
+ :mod:`semantics.relevant`.
413
+
414
+ NEVER emits PROVED. :func:`~unicode_logic_kit.semantics.relevant.rel_valid`
415
+ (equivalently, "no countermodel from
416
+ :func:`~unicode_logic_kit.semantics.relevant.rel_countermodel`") is sound
417
+ but INCOMPLETE — an exhaustive search only up to ``max_worlds`` worlds —
418
+ so a clean search is reported UNKNOWN(bound_hit), never treated as a
419
+ proof of validity, exactly as ``rel_valid``'s own docstring insists. A
420
+ REFUTED verdict is always backed by a genuine countermodel: the search
421
+ itself re-verifies it with
422
+ :func:`~unicode_logic_kit.semantics.relevant.rel_satisfies` before
423
+ returning (see ``rel_countermodel``'s docstring), and this backend
424
+ serializes that exact model into ``countermodel`` (see
425
+ :func:`_relevant_model_to_dict`).
426
+
427
+ A genuine PROVED route for B needs a real Isabelle proof over EVERY
428
+ Routley-Meyer interpretation (not just small ones) — see
429
+ :mod:`unicode_logic_kit.hol.isabelle_relevant`'s ``to_isabelle_relevant`` /
430
+ ``battery_proof`` — and is an explicit out-of-scope follow-up, not part
431
+ of this backend.
432
+
433
+ The call's ``timeout`` bounds the search (:func:`~unicode_logic_kit._deadline.run_until`);
434
+ a search it cut off is UNKNOWN(timeout), not bound_hit.
435
+ """
436
+
437
+ name = "relevant"
438
+ logics = frozenset({"relevant"})
439
+ external = False
440
+
441
+ def available(self) -> bool:
442
+ return True
443
+
444
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
445
+ timeout: int = 10000, **options) -> Verdict:
446
+ from ..semantics.relevant import rel_countermodel
447
+
448
+ max_worlds = options.pop("max_worlds", 2)
449
+ goal = _implication(formula, premises)
450
+ try:
451
+ (finished, result), elapsed = _timed(
452
+ lambda: _run_until(_instant(timeout),
453
+ lambda: rel_countermodel(goal, max_worlds=max_worlds)))
454
+ except TypeError as exc:
455
+ return Verdict(UNKNOWN, self.name, logic="relevant",
456
+ reason="unsupported", detail=str(exc))
457
+ if not finished:
458
+ return Verdict(UNKNOWN, self.name, logic="relevant", reason="timeout",
459
+ wall_time=elapsed,
460
+ detail=f"no B-countermodel found within the {timeout} ms limit")
461
+ if result is None:
462
+ return Verdict(UNKNOWN, self.name, logic="relevant", reason="bound_hit",
463
+ wall_time=elapsed,
464
+ detail=f"no B-countermodel with <= {max_worlds} worlds "
465
+ "found — rel_countermodel's search is sound but "
466
+ "incomplete, so this is NEVER treated as a proof "
467
+ "(see rel_valid's own honesty contract)")
468
+ model, world = result
469
+ return Verdict(REFUTED, self.name, logic="relevant", wall_time=elapsed,
470
+ countermodel=_relevant_model_to_dict(model, world))
471
+
472
+
473
+ # ---------------------------------------------------------------------------
474
+ # Hybrid modal logic H(@)
475
+ # ---------------------------------------------------------------------------
476
+
477
+ class HybridBackend(ProverBackend):
478
+ """Hybrid modal logic H(@) and H(@,↓) via the standard translation + a real Z3 ``Solver``.
479
+
480
+ The ↓ binder needs nothing extra here: the standard translation maps
481
+ ``↓x.φ`` to ``φ``'s translation with ``x`` bound to the current world
482
+ variable (the bounded fragment of FOL), which is faithful in both
483
+ directions, so ``unsat`` is a proof and ``sat`` a genuine countermodel of
484
+ the translation, hence of the formula. H(@,↓) is undecidable, so Z3 may
485
+ time out; that stays UNKNOWN. (The narrower
486
+ :func:`~unicode_logic_kit.atp.hybrid_down.down_is_valid` keeps its
487
+ PROVED-only contract.)
488
+
489
+ Deliberately does NOT call
490
+ :func:`~unicode_logic_kit.fol.modal_translation.hybrid_is_valid` (nor the
491
+ :func:`~unicode_logic_kit.atp.z3_models.is_valid` it delegates to): both
492
+ return a bare ``bool``, so a genuine countermodel (Z3 SAT) and Z3 giving
493
+ up (UNKNOWN/timeout) are both reported as the same ``False`` — exactly
494
+ the ambiguity this uniform layer exists to remove. This backend instead
495
+ builds the SAME ST-closed goal ``hybrid_is_valid`` builds —
496
+ :func:`~unicode_logic_kit.fol.modal_translation.standard_translation`
497
+ closed under :func:`~unicode_logic_kit.fol.modal_translation.frame_axioms`
498
+ (the frame axioms of every relation the goal mentions, and the membership of
499
+ each sorted constant ``c:S`` in ``S`` at every world) — and decides it with the kit's own tracked per-call
500
+ :func:`~unicode_logic_kit.atp.protocol._z3_track_and_check` (the identical
501
+ routine :class:`~unicode_logic_kit.atp.protocol.Z3Backend` uses for plain
502
+ FOL), so Z3's own ``unsat``/``sat``/``unknown`` map to real
503
+ PROVED / REFUTED(countermodel) / UNKNOWN(timeout|incomplete).
504
+
505
+ ``premises`` fold through the shared
506
+ :func:`~unicode_logic_kit.atp.protocol._implication` And/Implies helper at
507
+ the MODAL level first — exactly like
508
+ :class:`~unicode_logic_kit.atp.protocol.ModalTableauBackend` — before
509
+ translation: hybrid logic keeps full classical structure over ``∧``/
510
+ ``→``, so this fold is sound here (unlike the substructural Lambek/ILL
511
+ backends, where it would not be). ``frame`` (default ``"K"``) selects the
512
+ alethic frame class, exactly as it does for ``hybrid_is_valid``; so do the two other
513
+ keywords of that function: ``systems`` (the frame system of the agent-indexed families,
514
+ e.g. ``{"epistemic": "S5"}``, which makes ``Knows(a, P) → P`` valid) and ``temporal_closure``
515
+ (default ``True``: the temporal relation is reflexive and transitive; ``False`` drops
516
+ both, so ``Ⓖφ → φ`` is no longer valid). All three reach
517
+ :func:`~unicode_logic_kit.fol.modal_translation.frame_axioms` and nothing else, and an
518
+ unknown frame system or modal family is UNKNOWN(unsupported), never an exception.
519
+ """
520
+
521
+ name = "hybrid"
522
+ logics = frozenset({"hybrid"})
523
+ external = False
524
+
525
+ def available(self) -> bool:
526
+ return True
527
+
528
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
529
+ timeout: int = 10000, **options) -> Verdict:
530
+ from ..fol.modal_translation import standard_translation, frame_axioms
531
+ from ..fol.nodes import Quantifier, Variable, And, Implies
532
+
533
+ frame = options.pop("frame", "K")
534
+ systems = options.pop("systems", None)
535
+ temporal_closure = options.pop("temporal_closure", True)
536
+ goal_modal = _implication(formula, list(premises))
537
+ try:
538
+ translated = standard_translation(goal_modal, world="w")
539
+ closed = Quantifier("∀", Variable("w"), translated)
540
+ hyp = None
541
+ # The axioms of every relation the goal's translation emits (so ``Ⓖφ → φ``
542
+ # and ``Ⓞφ → Ⓟφ`` are decided as ``hybrid_is_valid`` decides them) and the
543
+ # membership of every sorted constant in its sort at every world.
544
+ for axiom in frame_axioms(goal_modal, frame, systems=systems,
545
+ temporal_closure=temporal_closure):
546
+ hyp = axiom if hyp is None else And(hyp, axiom)
547
+ st_goal = closed if hyp is None else Implies(hyp, closed)
548
+ z3_formula = st_goal.to_z3()
549
+ except (NotImplementedError, ValueError) as exc:
550
+ return Verdict(UNKNOWN, self.name, logic="hybrid",
551
+ reason="unsupported", detail=str(exc))
552
+
553
+ from z3 import sat, unsat
554
+
555
+ (res, solver), elapsed = _timed(
556
+ lambda: _z3_track_and_check(z3_formula, [], timeout))
557
+ if res == unsat:
558
+ return Verdict(PROVED, self.name, logic="hybrid", wall_time=elapsed)
559
+ if res == sat:
560
+ assignment = _z3_model_assignment(solver.model(), 0)
561
+ return Verdict(REFUTED, self.name, logic="hybrid", wall_time=elapsed,
562
+ countermodel={"kind": "z3_model", "assignment": assignment})
563
+ why = solver.reason_unknown()
564
+ reason = "timeout" if ("timeout" in why or "cancel" in why) else "incomplete"
565
+ return Verdict(UNKNOWN, self.name, logic="hybrid", reason=reason,
566
+ wall_time=elapsed, detail=why)