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,944 @@
1
+ """Standard (relational) translation of propositional modal logic into FOL.
2
+
3
+ The *standard translation* ST embeds the propositional modal fragment into
4
+ classical first-order logic over an explicit "current world" term, so that the
5
+ existing FOL back-ends (Z3, Prover9, TPTP, the resolution engine, the Tarski
6
+ evaluator) can reason about modal formulas. A modal formula is true at a world
7
+ *w* in a Kripke model exactly when its translation ``ST(φ, w)`` is true in the
8
+ corresponding first-order structure (worlds as the domain, accessibility
9
+ relations and atom-predicates as the relations) — this correspondence is what
10
+ ``tests/test_modal_translation.py`` cross-checks against
11
+ :func:`unicode_logic_kit.semantics.kripke.satisfies_modal`.
12
+
13
+ Translation scheme, with ``w`` the current-world variable and ``w'`` a FRESH
14
+ world variable (names ``w0, w1, …`` are generated so nested modalities never
15
+ capture each other; the free current-world name and every variable name of
16
+ the formula are skipped, so even a caller who passes ``world="w0"`` keeps a
17
+ distinct, uncaptured free variable, and an atom ``P(w0)`` keeps its own ``w0``):
18
+
19
+ - ``Atom A`` (propositional / ground) → ``A(w)``: the atom's predicate is
20
+ applied to the current world. A nullary atom ``P`` becomes ``P(w)``; an atom
21
+ ``Likes(a, b)`` becomes ``Likes(a, b, w)`` (the world is appended as the last
22
+ argument, so ground arguments are preserved).
23
+ - ``Not / And / Or / Xor / Implies / Iff`` — map through structurally at the
24
+ same world.
25
+ - ``Box φ`` → ``∀w' (R(w, w') → ST(φ, w'))``.
26
+ - ``Diamond φ`` → ``∃w' (R(w, w') ∧ ST(φ, w'))``.
27
+ - ``Knows(a, φ)`` → ``∀w' (Rk_a(w, w') → ST(φ, w'))``.
28
+ - ``Believes(a, φ)`` → ``∀w' (Rb_a(w, w') → ST(φ, w'))``.
29
+ - ``EverybodyKnows(G, φ)`` (E_G, "everyone in G knows φ") →
30
+ ``⋀_{a∈G} ∀w' (Rk_a(w, w') → ST(φ, w'))`` — a per-agent :class:`Knows`
31
+ translation for every member of ``G``, ANDed together (one-step; union of
32
+ relations ≡ conjunction of boxes). Empty ``G`` is rejected (see Scope below).
33
+ - ``DistributedKnowledge(G, φ)`` (D_G, "φ is distributed knowledge in G") →
34
+ ``∀w' ((⋀_{a∈G} Rk_a(w, w')) → ST(φ, w'))`` — ONE box over the
35
+ INTERSECTION of the group's relations (every ``Rk_a(w,w')`` conjunct must
36
+ hold of the SAME ``w'``), not a per-agent conjunction of separate boxes.
37
+ - ``Says(a, φ)`` → ``∀w' (Rs_a(w, w') → ST(φ, w'))`` (assertive: box over
38
+ a per-agent "compatible with what a says" relation).
39
+ - ``Wants(a, φ)`` → ``∀w' (Rw_a(w, w') → ST(φ, w'))`` (bouletic: box over
40
+ a per-agent desire relation).
41
+ - ``Always φ`` → ``∀w' (T(w, w') → ST(φ, w'))`` (box over a temporal
42
+ accessibility predicate ``T``).
43
+ - ``Eventually φ`` → ``∃w' (T(w, w') ∧ ST(φ, w'))`` (diamond over ``T``).
44
+ - ``Next φ`` → ``∀w' (N(w, w') → ST(φ, w'))`` (box over a one-step
45
+ predicate ``N``).
46
+ - ``Obligatory φ`` → ``∀w' (D(w, w') → ST(φ, w'))`` (box over a deontic
47
+ accessibility predicate ``D``).
48
+ - ``Permitted φ`` → ``∃w' (D(w, w') ∧ ST(φ, w'))`` (diamond over ``D``).
49
+ - ``Nominal i`` → ``w = nom_i``: a nominal is a world-equality against a
50
+ dedicated world CONSTANT. The ``"nom_"`` prefix keeps these constants out of
51
+ the user's constant namespace, so an atom mentioning a ground constant ``i``
52
+ can never collide with the nominal ``i``. UNLESS ``i`` is currently BOUND by
53
+ an enclosing ``↓i`` (see ``Down`` below), in which case the translation uses
54
+ the bound world TERM instead — the ``bindings`` mechanism, not the ``nom_``
55
+ constant, decides which.
56
+ - ``At(i, φ)`` → ``ST(φ, nom_i)``: the satisfaction operator ``@i φ``
57
+ re-anchors the translation at the constant world term ``nom_i`` (the current
58
+ world term is simply replaced — no quantifier is introduced) — again unless
59
+ ``i`` is ``↓``-bound, in which case the bound term replaces ``nom_i``.
60
+ - ``Down(x, φ)`` (``↓x.φ``, N1) → ``ST(φ, world)`` with ``x`` now BOUND, for
61
+ the rest of ``φ``'s translation, to the CURRENT WORLD TERM ``world`` — no
62
+ fresh quantifier is introduced (this is the bounded-fragment translation:
63
+ ``↓`` rebinds a name to the current world, it does not existentially
64
+ quantify over worlds). Concretely: every ``Nominal(x)`` / ``At(x, …)`` in
65
+ ``φ`` — down to, but not inside, a nested ``Down(x, …)`` that shadows it —
66
+ translates to ``world`` (or re-anchors at it) instead of to a fresh
67
+ ``nom_x`` constant. This is threaded through ``_translate`` as one extra
68
+ parameter, ``bindings: Dict[str, Node]`` (name → the world TERM it is
69
+ currently bound to), consulted by the ``Nominal``/``At`` cases FIRST,
70
+ falling back to the ``nom_`` scheme only when the name is unbound — so
71
+ ``bindings={}`` (every call site outside ``Down``'s own recursion)
72
+ reproduces the pre-``Down`` translation byte-for-byte. Because ``↓`` makes
73
+ H(@,↓) UNDECIDABLE (unlike plain H(@)), no bare-bool validity check treats
74
+ a ``Down``-containing formula: ``hybrid_is_valid`` refuses it by name;
75
+ :func:`down_is_valid` (below) is the PROVED-only replacement.
76
+
77
+ Scope / caveats (v1):
78
+
79
+ - ``Always`` and ``Eventually`` are translated as a **box / diamond over an
80
+ assumed temporal accessibility predicate** ``T``. To make ``Always`` a genuine
81
+ "henceforth" (reflexive-transitive reachability) one would need extra frame
82
+ axioms forcing ``T`` to be reflexive and transitive — that is not pure
83
+ first-order logic (transitive closure is not first-order definable), so it is
84
+ out of scope here. The cross-check in the tests therefore drives the Tarski
85
+ structure with ``T`` interpreted as the SAME one-step ``"temporal"`` relation
86
+ used by the Kripke model, NOT its closure.
87
+ - ``Until`` is rejected: strong Until needs the transitive closure of the
88
+ temporal relation, which is not first-order definable.
89
+ - ``CommonKnowledge`` (C_G) is rejected for the exact same reason: it needs the
90
+ reflexive-transitive closure of the group's union relation.
91
+ - ``EverybodyKnows`` over an EMPTY group is rejected too: ``E_∅ φ`` is
92
+ vacuously true by definition (the union over zero relations is empty, so
93
+ "holds at every successor" holds vacuously), but this grammar has no
94
+ first-order truth constant to render that without borrowing an unrelated
95
+ atom from the caller's own vocabulary. ``DistributedKnowledge`` never
96
+ raises this way — its own constructor already refuses an empty group (see
97
+ ``fol._modal_nodes.DistributedKnowledge``'s docstring).
98
+ - ``Quantifier`` / ``SortedQuantifier`` are rejected: first-order (quantified)
99
+ modal logic with object domains is out of scope for v1. So are Łukasiewicz and
100
+ lambda nodes.
101
+
102
+ The accessibility-predicate names are fixed so the matching Tarski structure can
103
+ be built mechanically: ``"R"`` (alethic), ``"Rk_" + agent`` (epistemic),
104
+ ``"Rb_" + agent`` (doxastic), ``"Rs_" + agent`` (assertive), ``"Rw_" + agent``
105
+ (bouletic), ``"T"`` (temporal), ``"N"`` (next), ``"D"`` (deontic).
106
+ """
107
+
108
+ from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, List, NoReturn, Optional
109
+
110
+ if TYPE_CHECKING:
111
+ from ..atp.protocol import Verdict
112
+
113
+ from ._atom_keys import refuse_alike_agents
114
+ from ._msfl_nodes import key_text
115
+ from ._identifiers import fresh_variables, symbol_names
116
+ from .nodes import (
117
+ Node,
118
+ Variable, Constant, LambdaVar,
119
+ Atom, Not, And, Or, Xor, Implies, Iff,
120
+ Quantifier, SortedQuantifier,
121
+ Box, Diamond, Knows, Believes, Says, Wants,
122
+ EverybodyKnows, DistributedKnowledge, CommonKnowledge,
123
+ Always, Eventually, Next, Until,
124
+ Historically, Once, Previous, Since,
125
+ Obligatory, Permitted,
126
+ Nominal, At,
127
+ sort_membership_axioms, substitute,
128
+ )
129
+ from ._truth_constants import truth_value
130
+ # Down (the ↓ binder, N1) is not yet re-exported through fol.nodes / fol's
131
+ # public __init__ / the top-level unicode_logic_kit package — that three-file
132
+ # edit is outside this change's file ownership (see the change's own
133
+ # report); imported directly from its defining module instead, the same
134
+ # class object either import path would give.
135
+ from ._hybrid_nodes import Down
136
+ from .frames import (
137
+ FRAMES as _SHARED_FRAMES, UnsupportedFrameCondition,
138
+ is_first_order, resolve_frame, unguarded_frame_axiom,
139
+ )
140
+ from ..semantics._modal_reject import (
141
+ FUZZY_TYPES, LAMBDA_TYPES,
142
+ reject_equality, reject_fuzzy, reject_lambda,
143
+ )
144
+
145
+ # Accessibility predicate names (the contract with the matching Tarski
146
+ # structure; keep these stable).
147
+ _R_ALETHIC = "R"
148
+ _R_KNOWS_PREFIX = "Rk_"
149
+ _R_BELIEVES_PREFIX = "Rb_"
150
+ _R_SAYS_PREFIX = "Rs_"
151
+ _R_WANTS_PREFIX = "Rw_"
152
+
153
+
154
+ def _agent_key(agent: Node) -> str:
155
+ """Agent term's name for the per-agent relation (this propositional translation
156
+ rejects object quantifiers, so the agent is always a ground Constant here)."""
157
+ return getattr(agent, "name", None) or key_text(agent)
158
+ _R_TEMPORAL = "T"
159
+ _R_NEXT = "N"
160
+ _R_DEONTIC = "D"
161
+
162
+ # Prefix for the world constant a hybrid nominal translates to ("nom_" + name).
163
+ # The prefix keeps the generated constants disjoint from user constants, so a
164
+ # formula whose atoms mention a ground constant `i` cannot collide with the
165
+ # nominal `i`. This is the contract with any structure built for the image.
166
+ _NOM_PREFIX = "nom_"
167
+
168
+ # Equality is NOT an ordinary atom here. This translation is PROPOSITIONAL: an
169
+ # atom is a world-relative proposition, so appending the world to ``=`` would
170
+ # make identity a ternary, uninterpreted, world-varying relation — under which
171
+ # ``a = a`` comes back "not valid" (measured on 0.28.1) and ``□(a = b) → a = b``
172
+ # is "valid" only through reflexivity of R, not through identity. That is a
173
+ # silent approximation of a construct this layer has no semantics for (the
174
+ # Kripke evaluator reads atoms off a per-world valuation of ground-atom keys
175
+ # and interprets no terms either), so it is refused by name. First-order modal
176
+ # logic with rigid identity is :mod:`unicode_logic_kit.fol.qml`.
177
+ # ``≠`` is the same construct and is refused the same way: the kit prints
178
+ # ``a ≠ b`` as ``Atom("≠", (a, b))``, and appending the world there produced
179
+ # ``≠(a, b, w)`` — a ternary uninterpreted relation unrelated to the ``=`` one,
180
+ # so ``a = b ∨ a ≠ b`` came back "not valid" (measured on 0.28.1). Both
181
+ # spellings live in :data:`EQUALITY_PREDICATES`, and :func:`reject_equality`
182
+ # (shared with the Kripke evaluator, the modal tableau and the GMT embedding)
183
+ # is the single place that says why.
184
+ #: The predicate the FOL IMAGE uses for world identity (``@i j`` becomes
185
+ #: ``i = j``, and the temporal first-step axiom says ``T(w,v) → w = v ∨ …``).
186
+ #: An equality atom in the SOURCE is refused; one in the image is ordinary FOL.
187
+ _EQUALITY = "="
188
+
189
+ _EQUALITY_ROUTE = "the propositional standard translation"
190
+ _EQUALITY_ATOM_READING = ("an atom becomes a predicate with the world appended, "
191
+ "so '=' would become a world-varying uninterpreted "
192
+ "relation")
193
+ _EQUALITY_INSTEAD = ("Use unicode_logic_kit.fol.qml (quantified modal logic, "
194
+ "where '=' is rigid identity over the object domain) for a "
195
+ "formula with identity.")
196
+
197
+
198
+ def _reject_equality(formula: Node) -> None:
199
+ """:func:`reject_equality` with this route's wording (check-and-raise)."""
200
+ reject_equality(formula, "standard_translation", _EQUALITY_ROUTE,
201
+ atom_reading=_EQUALITY_ATOM_READING,
202
+ instead=_EQUALITY_INSTEAD)
203
+
204
+ # Universal/existential quantifier-type spellings used by the AST.
205
+ _FORALL = "∀"
206
+ _EXISTS = "∃"
207
+
208
+ # A USER predicate named like one of the accessibility relations above used to
209
+ # BE that relation once the translation appended its world argument (a
210
+ # propositional atom ``R`` became ``R(w)`` next to the binary relation
211
+ # ``R(w, v)`` and crashed Z3 on the arity clash). _user_predicate keeps the
212
+ # two namespaces apart: U+00B7 MIDDLE DOT is punctuation no parser puts into
213
+ # an identifier, so the renamed user name can collide with nothing.
214
+ _RESERVED_RELATIONS = frozenset({_R_ALETHIC, _R_TEMPORAL, _R_NEXT, _R_DEONTIC})
215
+ _RESERVED_RELATION_PREFIXES = (_R_KNOWS_PREFIX, _R_BELIEVES_PREFIX,
216
+ _R_SAYS_PREFIX, _R_WANTS_PREFIX)
217
+ _USER_MARK = "·"
218
+
219
+
220
+ def _user_predicate(name: str) -> str:
221
+ """The name a USER atom's predicate gets in the image: unchanged unless it
222
+ (with any trailing ``·`` stripped) is a relation name above, then with one
223
+ ``·`` appended — injective, and a formula that avoids those names
224
+ translates byte-for-byte as before (the same scheme as ``fol.qml``)."""
225
+ base = name.rstrip(_USER_MARK)
226
+ if base in _RESERVED_RELATIONS or base.startswith(_RESERVED_RELATION_PREFIXES):
227
+ return name + _USER_MARK
228
+ return name
229
+
230
+
231
+ class _FreshWorlds:
232
+ """A monotonic generator of fresh world-variable names ``w0, w1, …``.
233
+
234
+ Threading a single counter through one translation guarantees every modal
235
+ operator introduces a distinct bound world variable, so nested boxes /
236
+ diamonds cannot capture one another's worlds. A world variable never has the
237
+ spelling of a name the translation must leave alone: the free current-world
238
+ name (so a caller who passes ``world="w0"`` keeps a distinct, uncaptured free
239
+ variable), every name of the formula, of any kind (an atom ``P(w0)`` of the
240
+ source keeps its own ``w0`` under a box, which a bound world variable of that
241
+ name would capture) and every name the caller asks to avoid. Names are compared
242
+ exactly and with their case folded, because a target that reads a variable
243
+ ``w0`` and a variable ``W0`` as one word (TPTP, Prover9) would conflate them.
244
+
245
+ A variable of the formula that is spelled like the current-world name would be
246
+ read as the world itself, so it is renamed, once for the whole formula, to a name
247
+ of its own (:meth:`term` applies the renaming to a term of an atom).
248
+ """
249
+
250
+ def __init__(self, world: str = "", names: Iterable[str] = (),
251
+ variables: Iterable[str] = (), avoid: Iterable[str] = ()):
252
+ """Start the fresh-name counter at zero and settle the names that are never minted.
253
+
254
+ ``world`` is the free current-world name, ``names`` every name of the formula,
255
+ ``variables`` the names of its variables and ``avoid`` further names to keep
256
+ clear of.
257
+ """
258
+ self._n = 0
259
+ reserved = set(names) | set(variables) | set(avoid) | {world}
260
+ taken = reserved | {name.casefold() for name in reserved}
261
+ self._renamed: Dict[str, Variable] = {}
262
+ for name in sorted(set(variables)):
263
+ if name.casefold() == world.casefold():
264
+ new_name = fresh_variables(1, letter="x", avoid=taken)[0]
265
+ taken = taken | {new_name}
266
+ self._renamed[name] = Variable(new_name)
267
+ self._reserved = taken
268
+
269
+ def next(self) -> Variable:
270
+ """Return the next fresh world Variable (``w0``, ``w1``, …), skipping ``reserved``."""
271
+ while True:
272
+ name = f"w{self._n}"
273
+ self._n += 1
274
+ if name not in self._reserved:
275
+ return Variable(name)
276
+
277
+ def term(self, term: Node) -> Node:
278
+ """``term`` with each variable spelled like the current world renamed (else unchanged)."""
279
+ for old, new in self._renamed.items():
280
+ term = substitute(term, Variable(old), new)
281
+ return term
282
+
283
+
284
+ def _box_like(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
285
+ bindings: Dict[str, Node]) -> Node:
286
+ """Build ``∀w' (rel(world, w') → ST(body, w'))`` with a fresh ``w'``."""
287
+ w2 = fresh.next()
288
+ access = Atom(rel_name, (world, w2))
289
+ return Quantifier(_FORALL, w2, Implies(access, _translate(body, w2, fresh, bindings)))
290
+
291
+
292
+ def _diamond_like(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
293
+ bindings: Dict[str, Node]) -> Node:
294
+ """Build ``∃w' (rel(world, w') ∧ ST(body, w'))`` with a fresh ``w'``."""
295
+ w2 = fresh.next()
296
+ access = Atom(rel_name, (world, w2))
297
+ return Quantifier(_EXISTS, w2, And(access, _translate(body, w2, fresh, bindings)))
298
+
299
+
300
+ def _box_intersection(rel_names: List[str], world: Node, body: Node, fresh: _FreshWorlds,
301
+ bindings: Dict[str, Node]) -> Node:
302
+ """Build ``∀w' ((R1(w,w')∧R2(w,w')∧…) → ST(body,w'))`` — a box over the
303
+ INTERSECTION of several accessibility relations, one atom per relation
304
+ ANDed together into the antecedent, sharing ONE fresh ``w'`` (a single
305
+ successor has to satisfy every relation at once — this is exactly what
306
+ distinguishes :class:`DistributedKnowledge` from
307
+ :func:`_box_like`-per-agent-then-ANDed, which is what
308
+ :class:`EverybodyKnows` uses instead, and which shares no such single
309
+ witness). ``rel_names`` must be non-empty (guaranteed by
310
+ :class:`DistributedKnowledge`'s own constructor refusing an empty group).
311
+ """
312
+ w2 = fresh.next()
313
+ guard: Node = Atom(rel_names[0], (world, w2))
314
+ for name in rel_names[1:]:
315
+ guard = And(guard, Atom(name, (world, w2)))
316
+ return Quantifier(_FORALL, w2, Implies(guard, _translate(body, w2, fresh, bindings)))
317
+
318
+
319
+ def _box_converse(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
320
+ bindings: Dict[str, Node]) -> Node:
321
+ """Build ``∀w' (rel(w', world) → ST(body, w'))`` — a box over the CONVERSE relation."""
322
+ w2 = fresh.next()
323
+ access = Atom(rel_name, (w2, world))
324
+ return Quantifier(_FORALL, w2, Implies(access, _translate(body, w2, fresh, bindings)))
325
+
326
+
327
+ def _diamond_converse(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
328
+ bindings: Dict[str, Node]) -> Node:
329
+ """Build ``∃w' (rel(w', world) ∧ ST(body, w'))`` — a diamond over the CONVERSE relation."""
330
+ w2 = fresh.next()
331
+ access = Atom(rel_name, (w2, world))
332
+ return Quantifier(_EXISTS, w2, And(access, _translate(body, w2, fresh, bindings)))
333
+
334
+
335
+ def _translate(formula: Node, world: Node, fresh: _FreshWorlds,
336
+ bindings: Optional[Dict[str, Node]] = None) -> Node:
337
+ """Recursively translate ``formula`` relative to ``world`` (the worker).
338
+
339
+ ``world`` is the current-world TERM: the free Variable at the top, a bound
340
+ fresh Variable under a modality, or a ``nom_``-Constant under an ``@``-jump.
341
+
342
+ ``bindings`` (name → world TERM) holds the ``↓``-bound nominal names in
343
+ scope (N1); every call site outside :class:`Down`'s own recursion passes
344
+ ``{}`` (the default), which is exactly what makes ``bindings={}``
345
+ reproduce the pre-``Down`` translation byte-for-byte — see the module
346
+ docstring's ``Down`` bullet.
347
+ """
348
+ if bindings is None:
349
+ bindings = {}
350
+
351
+ # --- atomic: append the world as the last predicate argument ---
352
+ if isinstance(formula, Atom):
353
+ if truth_value(formula) is not None:
354
+ return formula # `$true` / `$false` are the same at every world: no world argument
355
+ _reject_equality(formula)
356
+ return Atom(_user_predicate(formula.predicate),
357
+ (*(fresh.term(arg) for arg in formula.args), world))
358
+
359
+ # --- classical connectives: structural at the same world ---
360
+ if isinstance(formula, Not):
361
+ return Not(_translate(formula.formula, world, fresh, bindings))
362
+ if isinstance(formula, And):
363
+ return And(_translate(formula.left, world, fresh, bindings),
364
+ _translate(formula.right, world, fresh, bindings))
365
+ if isinstance(formula, Or):
366
+ return Or(_translate(formula.left, world, fresh, bindings),
367
+ _translate(formula.right, world, fresh, bindings))
368
+ if isinstance(formula, Xor):
369
+ return Xor(_translate(formula.left, world, fresh, bindings),
370
+ _translate(formula.right, world, fresh, bindings))
371
+ if isinstance(formula, Implies):
372
+ return Implies(_translate(formula.left, world, fresh, bindings),
373
+ _translate(formula.right, world, fresh, bindings))
374
+ if isinstance(formula, Iff):
375
+ return Iff(_translate(formula.left, world, fresh, bindings),
376
+ _translate(formula.right, world, fresh, bindings))
377
+
378
+ # --- alethic ---
379
+ if isinstance(formula, Box):
380
+ return _box_like(_R_ALETHIC, world, formula.formula, fresh, bindings)
381
+ if isinstance(formula, Diamond):
382
+ return _diamond_like(_R_ALETHIC, world, formula.formula, fresh, bindings)
383
+
384
+ # --- epistemic / doxastic (both box-like / universal) ---
385
+ if isinstance(formula, Knows):
386
+ return _box_like(_R_KNOWS_PREFIX + _agent_key(formula.agent), world,
387
+ formula.formula, fresh, bindings)
388
+ if isinstance(formula, Believes):
389
+ return _box_like(_R_BELIEVES_PREFIX + _agent_key(formula.agent), world,
390
+ formula.formula, fresh, bindings)
391
+
392
+ # --- group epistemic: E_G and D_G are one-step (union/intersection of
393
+ # per-agent relations) and ARE first-order definable; C_G needs the
394
+ # reflexive-transitive CLOSURE of the union relation and is NOT (see the
395
+ # rejection below, alongside Until/Since). ---
396
+ if isinstance(formula, EverybodyKnows):
397
+ if not formula.group:
398
+ raise NotImplementedError(
399
+ "standard_translation: E_∅ φ (a group-epistemic operator over "
400
+ "an empty group) is vacuously TRUE at every world by "
401
+ "definition (everybody_knows's own empty-group convention), "
402
+ "but this grammar has no first-order truth constant to render "
403
+ "that without borrowing an unrelated atom. Evaluate it "
404
+ "directly with semantics.kripke.satisfies_modal / "
405
+ "semantics.action_models.everybody_knows instead."
406
+ )
407
+ conj = _box_like(_R_KNOWS_PREFIX + _agent_key(formula.group[0]), world,
408
+ formula.formula, fresh, bindings)
409
+ for agent in formula.group[1:]:
410
+ conj = And(conj, _box_like(_R_KNOWS_PREFIX + _agent_key(agent), world,
411
+ formula.formula, fresh, bindings))
412
+ return conj
413
+ if isinstance(formula, DistributedKnowledge):
414
+ rel_names = [_R_KNOWS_PREFIX + _agent_key(a) for a in formula.group]
415
+ return _box_intersection(rel_names, world, formula.formula, fresh, bindings)
416
+ if isinstance(formula, CommonKnowledge):
417
+ raise NotImplementedError(
418
+ "standard_translation: CommonKnowledge (C_G) is not first-order "
419
+ "definable — common knowledge is the REFLEXIVE-TRANSITIVE CLOSURE "
420
+ "of the group's union relation, and transitive closure has no "
421
+ "first-order rendering (the same obstacle this module already "
422
+ "reports for Until/Since — see the module docstring). Evaluate it "
423
+ "with the Kripke evaluator (semantics.kripke.satisfies_modal) "
424
+ "instead."
425
+ )
426
+
427
+ # --- assertive / bouletic (both box-like, per-agent relations) ---
428
+ if isinstance(formula, Says):
429
+ return _box_like(_R_SAYS_PREFIX + _agent_key(formula.agent), world,
430
+ formula.formula, fresh, bindings)
431
+ if isinstance(formula, Wants):
432
+ return _box_like(_R_WANTS_PREFIX + _agent_key(formula.agent), world,
433
+ formula.formula, fresh, bindings)
434
+
435
+ # --- deontic (box/diamond over a deontic accessibility predicate D) ---
436
+ if isinstance(formula, Obligatory):
437
+ return _box_like(_R_DEONTIC, world, formula.formula, fresh, bindings)
438
+ if isinstance(formula, Permitted):
439
+ return _diamond_like(_R_DEONTIC, world, formula.formula, fresh, bindings)
440
+
441
+ # --- temporal (box/diamond over an assumed accessibility predicate) ---
442
+ if isinstance(formula, Always):
443
+ return _box_like(_R_TEMPORAL, world, formula.formula, fresh, bindings)
444
+ if isinstance(formula, Eventually):
445
+ return _diamond_like(_R_TEMPORAL, world, formula.formula, fresh, bindings)
446
+ if isinstance(formula, Next):
447
+ return _box_like(_R_NEXT, world, formula.formula, fresh, bindings)
448
+
449
+ # --- past tense (box/diamond over the CONVERSE temporal/next predicate) ---
450
+ if isinstance(formula, Historically):
451
+ return _box_converse(_R_TEMPORAL, world, formula.formula, fresh, bindings)
452
+ if isinstance(formula, Once):
453
+ return _diamond_converse(_R_TEMPORAL, world, formula.formula, fresh, bindings)
454
+ if isinstance(formula, Previous):
455
+ return _box_converse(_R_NEXT, world, formula.formula, fresh, bindings)
456
+
457
+ # --- hybrid: a nominal is a world-equality; @ re-anchors the world term.
458
+ # ``bindings`` is consulted FIRST — a ↓-bound name uses its bound TERM
459
+ # directly (no fresh nom_ constant, no equality atom needed for Nominal:
460
+ # "x" bound to term t just means "the current world is t", i.e. w = t —
461
+ # same shape as the nom_ case, just with t instead of a fresh constant). ---
462
+ if isinstance(formula, Nominal):
463
+ bound = bindings.get(formula.name)
464
+ target = bound if bound is not None else Constant(_NOM_PREFIX + formula.name)
465
+ return Atom("=", (world, target))
466
+ if isinstance(formula, At):
467
+ bound = bindings.get(formula.nominal.name)
468
+ target = bound if bound is not None else Constant(_NOM_PREFIX + formula.nominal.name)
469
+ return _translate(formula.formula, target, fresh, bindings)
470
+ if isinstance(formula, Down):
471
+ # ↓x.φ: bind x to the CURRENT world term for the rest of φ's
472
+ # translation — NO fresh quantifier (the bounded-fragment
473
+ # translation; see the module docstring's Down bullet and
474
+ # fol._hybrid_nodes.Down's own docstring for why this is sound: ST
475
+ # stays meaning-preserving for full H(@,↓), this is not an
476
+ # approximation).
477
+ return _translate(formula.formula, world, fresh,
478
+ {**bindings, formula.variable.name: world})
479
+
480
+ # --- rejected ---
481
+ if isinstance(formula, (Until, Since)):
482
+ raise NotImplementedError(
483
+ "standard_translation: Until / Since are not first-order definable — "
484
+ "strong Until/Since need the transitive closure of the temporal "
485
+ "relation, which no pure FOL formula captures. Evaluate them with the "
486
+ "Kripke evaluator (semantics.kripke.satisfies_modal) instead."
487
+ )
488
+ if isinstance(formula, (Quantifier, SortedQuantifier)):
489
+ _reject_quantifier(formula)
490
+ if isinstance(formula, FUZZY_TYPES):
491
+ reject_fuzzy(formula, "standard_translation")
492
+ if isinstance(formula, LAMBDA_TYPES):
493
+ reject_lambda(formula, "standard_translation")
494
+
495
+ raise NotImplementedError(
496
+ f"standard_translation: unsupported node type {type(formula).__name__}."
497
+ )
498
+
499
+
500
+ def _reject_quantifier(formula: Node) -> NoReturn:
501
+ """Reject an object-level quantifier: FO-modal domains are out of scope."""
502
+ raise NotImplementedError(
503
+ f"standard_translation: {type(formula).__name__} is not supported — the "
504
+ "standard translation here covers the propositional modal fragment only. "
505
+ "For quantified (first-order) modal logic with object domains use "
506
+ "unicode_logic_kit.fol.qml (qml_translate / qml_is_valid, the FO shallow "
507
+ "embedding with explicit constant/varying/increasing/decreasing domain "
508
+ "regimes)."
509
+ )
510
+
511
+
512
+ def _check_nominal_collision(formula: Node) -> None:
513
+ """Raise if a user symbol's name collides with a nominal world constant.
514
+
515
+ Each nominal ``i`` (a ``Nominal`` or the label of an ``At``) translates to the
516
+ reserved world constant ``nom_i``. If the formula independently contains a
517
+ user symbol named ``nom_i`` -- a ``Constant``, a ``SortedConstant``, a
518
+ ``Function``, or any other node that carries a name and is neither a nominal nor
519
+ a variable -- the first-order image would
520
+ conflate the two terms and Z3's equality reasoning could report a genuinely
521
+ invalid formula as valid — a hole in the "``True`` is always a proof"
522
+ guarantee. The parser builds such a name from the text itself (``@i P(nom_i)``),
523
+ and a hand-built or deserialised AST can carry one in any position; the check fails
524
+ fast, exactly like a dangling nominal assignment.
525
+ """
526
+ nominal_names, user_names = set(), set()
527
+ for node in formula.walk():
528
+ if isinstance(node, Nominal):
529
+ nominal_names.add(node.name)
530
+ elif isinstance(node, At):
531
+ nominal_names.add(node.nominal.name)
532
+ elif (not isinstance(node, (Variable, LambdaVar))
533
+ and isinstance(getattr(node, "name", None), str)):
534
+ user_names.add(node.name)
535
+ clash = {_NOM_PREFIX + n for n in nominal_names} & user_names
536
+ if clash:
537
+ colliding = sorted(n for n in nominal_names if _NOM_PREFIX + n in clash)
538
+ raise ValueError(
539
+ f"standard_translation: user symbol(s) {sorted(clash)} collide with the "
540
+ f"reserved world constant(s) for nominal(s) {colliding}; the "
541
+ f"{_NOM_PREFIX!r} prefix is reserved for the hybrid translation — rename "
542
+ "the user constant/function."
543
+ )
544
+
545
+
546
+ def standard_translation(formula: Node, world: str = "w",
547
+ avoid: Iterable[str] = ()) -> Node:
548
+ """Translate a propositional modal ``formula`` into a classical FOL Node.
549
+
550
+ ``world`` names the free current-world variable threaded through the
551
+ translation (default ``"w"``); the result is a plain first-order formula in
552
+ which propositional atoms ``A`` become ``A(world)`` and each modality becomes
553
+ a quantification over a fresh world variable bounded by an accessibility
554
+ predicate (see the module docstring for the exact scheme and the fixed
555
+ predicate names). The returned Node uses only classical FOL constructs, so it
556
+ can be handed to ``to_z3`` / ``to_prover9`` / ``to_tptp`` / the Tarski
557
+ evaluator.
558
+
559
+ Hybrid constructs translate too: ``Nominal i`` becomes the world-equality
560
+ ``world = nom_i`` and ``At(i, φ)`` becomes ``ST(φ)`` anchored at the constant
561
+ ``nom_i`` (the ``"nom_"`` prefix keeps nominal constants disjoint from user
562
+ constants — see the module docstring).
563
+
564
+ The variables of an atom (``P(w0)``) are the caller's own, and so are the names
565
+ the translation mints. The world variables it binds (``w0``, ``w1``, …) never have
566
+ the spelling of a name of ``formula`` (of any kind: variable, constant, predicate,
567
+ nominal, …), of ``world`` or of a name in ``avoid``, compared exactly and with the
568
+ case folded, so ``□P(w0)`` is ``∀w1 (R(w, w1) → P(w0, w1))`` and not a statement
569
+ about the bound world. A variable of ``formula`` that is spelled like ``world``
570
+ would be read as the current world itself, so it is renamed to a name of its own
571
+ (``x0``, …, clear of every name of ``formula``, of ``world`` and of ``avoid``) in
572
+ the image: it stays one free variable, the same in every atom. To translate several
573
+ formulas of one problem separately and keep that name the same in all of them, pass
574
+ the names of the others as ``avoid``.
575
+
576
+ Raises:
577
+ NotImplementedError: on ``Until`` (not first-order definable), any
578
+ object-level quantifier (first-order modal logic is out of scope for
579
+ v1), a Łukasiewicz node, or a lambda node; and on two different agent
580
+ terms that are named alike (the numeral ``1`` and a constant named ``1``):
581
+ the relation of an agent's operator is named after the agent, so the two
582
+ would be ONE relation and the image would say another thing than ``formula``.
583
+ """
584
+ _check_nominal_collision(formula)
585
+ refuse_alike_agents([formula], "standard_translation")
586
+ variables = {node.name for node in formula.walk() if isinstance(node, Variable)}
587
+ fresh = _FreshWorlds(world, symbol_names(formula), variables, avoid)
588
+ return _translate(formula, Variable(world), fresh)
589
+
590
+
591
+ # =========================
592
+ # Hybrid validity via the standard translation + the Z3 oracle
593
+ # =========================
594
+
595
+ # Frame classes hybrid_is_valid understands, as conditions on the ALETHIC
596
+ # accessibility predicate R (the other modal families keep their minimal K
597
+ # reading — no axioms are asserted for Rk_a / Rb_a / T / N / D here). The
598
+ # table is the shared registry (unicode_logic_kit.fol.frames), so this route
599
+ # understands the same systems as every other one; conditions with no
600
+ # first-order form are refused by name.
601
+ _HYBRID_FRAMES = _SHARED_FRAMES
602
+
603
+
604
+ # Which accessibility relation each modal family reads, mirroring _st above.
605
+ _AGENT_FAMILY_PREFIX: Dict[str, str] = {
606
+ "epistemic": _R_KNOWS_PREFIX,
607
+ "doxastic": _R_BELIEVES_PREFIX,
608
+ "assertive": _R_SAYS_PREFIX,
609
+ "bouletic": _R_WANTS_PREFIX,
610
+ }
611
+
612
+
613
+ def relations_used(formula: Node) -> FrozenSet[str]:
614
+ """Every accessibility predicate :func:`standard_translation` emits for
615
+ ``formula`` (``"R"``, ``"T"``, ``"N"``, ``"D"``, ``"Rk_alice"``, …).
616
+
617
+ This is what gates :func:`frame_axioms`: a condition on a relation the
618
+ formula never mentions is noise, and — on a route that reports a bare
619
+ "not valid" — noise that can change the answer.
620
+ """
621
+ used: set = set()
622
+ for node in formula.walk():
623
+ if isinstance(node, (Box, Diamond)):
624
+ used.add(_R_ALETHIC)
625
+ elif isinstance(node, Knows):
626
+ used.add(_R_KNOWS_PREFIX + _agent_key(node.agent))
627
+ elif isinstance(node, (EverybodyKnows, DistributedKnowledge)):
628
+ used.update(_R_KNOWS_PREFIX + _agent_key(a) for a in node.group)
629
+ elif isinstance(node, Believes):
630
+ used.add(_R_BELIEVES_PREFIX + _agent_key(node.agent))
631
+ elif isinstance(node, Says):
632
+ used.add(_R_SAYS_PREFIX + _agent_key(node.agent))
633
+ elif isinstance(node, Wants):
634
+ used.add(_R_WANTS_PREFIX + _agent_key(node.agent))
635
+ elif isinstance(node, (Obligatory, Permitted)):
636
+ used.add(_R_DEONTIC)
637
+ elif isinstance(node, (Always, Eventually, Historically, Once)):
638
+ used.add(_R_TEMPORAL)
639
+ elif isinstance(node, (Next, Previous)):
640
+ used.add(_R_NEXT)
641
+ return frozenset(used)
642
+
643
+
644
+ def _link(antecedent_relation: str, consequent_relation: str) -> Node:
645
+ """``∀v0 ∀v1 (A(v0,v1) → B(v0,v1))`` over two relation names."""
646
+ w, v = Variable("v0"), Variable("v1")
647
+ return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
648
+ Atom(antecedent_relation, (w, v)), Atom(consequent_relation, (w, v)))))
649
+
650
+
651
+ def _temporal_first_step() -> Node:
652
+ """``∀v0 ∀v1 (T(v0,v1) → v0 = v1 ∨ ∃v2 (N(v0,v2) ∧ T(v2,v1)))``.
653
+
654
+ The first-order half of ``T ⊆ N*``: the witnessing path of a henceforth-step
655
+ either stands still or starts with one ``N``-step. ``T ⊆ N*`` itself demands
656
+ a FINITE path and is not first-order definable, but every model with
657
+ ``T = N*`` satisfies this, which is what makes asserting it sound. The
658
+ unguarded twin of :func:`unicode_logic_kit.fol.qml._temporal_first_step_axiom`
659
+ — the two routes must agree, so they assert the same thing.
660
+ """
661
+ w, v, u = Variable("v0"), Variable("v1"), Variable("v2")
662
+ return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
663
+ Atom(_R_TEMPORAL, (w, v)),
664
+ Or(Atom(_EQUALITY, (w, v)),
665
+ Quantifier(_EXISTS, u, And(Atom(_R_NEXT, (w, u)),
666
+ Atom(_R_TEMPORAL, (u, v))))))))
667
+
668
+
669
+ def frame_axioms(formula: Node, frame: str = "K", systems=None,
670
+ temporal_closure: bool = True) -> List[Node]:
671
+ """The first-order frame axioms for EVERY relation ``formula``'s translation
672
+ emits — the side conditions of the standard translation.
673
+
674
+ ``frame`` constrains the ALETHIC relation ``R`` and is any system in the
675
+ shared registry (:mod:`unicode_logic_kit.fol.frames`) or a Scott–Lemmon spec
676
+ like ``"G(1,1,1,1)"``; a system whose condition has no first-order form
677
+ (GL, S4.1, Grz) is refused by name, because this route is first-order.
678
+
679
+ The other families get the conventions :mod:`unicode_logic_kit.fol.qml` and
680
+ the Isabelle/THF exporters already use, so the routes agree instead of
681
+ contradicting each other:
682
+
683
+ - temporal: ``T`` reflexive and transitive (``temporal_closure=True``,
684
+ the default), ``N ⊆ T``, and :func:`_temporal_first_step` when both
685
+ occur — this is what makes ``Ⓖφ → φ``, ``Ⓖφ → Ⓝφ`` and the past mirrors
686
+ come out valid, as the Kripke evaluator reads them (it evaluates
687
+ ``Always``/``Eventually`` over the reflexive-transitive CLOSURE of the
688
+ one-step relation). Until 0.28.1 NOTHING asserted these on this route,
689
+ so ``hybrid_is_valid(Ⓖ P → P, "S5")`` answered False while
690
+ ``qml_is_valid`` on the same formula answered True — a bare "not valid"
691
+ about a formula the kit's own modal semantics validates.
692
+ - deontic: ``D`` serial (Standard Deontic Logic), so ``Ⓞφ → Ⓟφ`` is valid.
693
+ - agent-indexed: nothing unless ``systems`` asks, e.g.
694
+ ``systems={"epistemic": "S5", "doxastic": "KD45"}`` — the families are
695
+ ``epistemic`` / ``doxastic`` / ``assertive`` / ``bouletic`` and an unknown
696
+ one raises. A system for a family the formula never mentions contributes
697
+ nothing (same as ``qml_axioms``).
698
+ - sorted constants: ``c:S`` is an element of ``S`` at EVERY world (a constant is
699
+ a rigid designator), so each distinct ``c:S`` of the formula adds
700
+ ``∀v0 S(c, v0)`` — the sort guard in the translation's own vocabulary (a
701
+ world as last argument), unguarded by any existence predicate, as in
702
+ ``qml_axioms``. Without it ``Human(carl:Human)`` has a countermodel in
703
+ which ``carl`` is no ``Human``, and a route that reads Z3's ``sat`` as
704
+ "refuted" answers wrongly. A sort that occurs only through a constant needs
705
+ no non-emptiness axiom: the constant is its member.
706
+
707
+ Every axiom is closed over its own bound variables (``v0``, ``v1``, …), so
708
+ it can never capture anything in the translated formula, and they are
709
+ returned for the caller to pass as SEPARATE premises — never conjoined onto
710
+ the translation itself. The names are deliberately ones the kit's own
711
+ parser reads back: an axiom that prints as ``∀_hw0 R(_hw0, _hw0)`` is text
712
+ :func:`unicode_logic_kit.api.parse_any` rejects.
713
+
714
+ Raises:
715
+ ValueError: unknown frame system, or an unknown ``systems`` family.
716
+ UnsupportedFrameCondition: a condition with no first-order frame form.
717
+ """
718
+ used = relations_used(formula)
719
+ axioms: List[Node] = []
720
+ # Resolve the frame FIRST, whatever the formula mentions: a typo in a frame
721
+ # name must fail loudly even for a formula with no alethic operator, or the
722
+ # caller would believe a system was applied that nothing ever looked at.
723
+ try:
724
+ alethic = resolve_frame(frame)
725
+ except ValueError as exc:
726
+ raise ValueError(f"frame_axioms: {exc}") from None
727
+ # ... and refuse a condition with no first-order form here too, rather than
728
+ # only when the formula happens to mention R: asking this route for GL is a
729
+ # request it cannot honour either way.
730
+ for cond in alethic:
731
+ if not is_first_order(cond):
732
+ raise UnsupportedFrameCondition(
733
+ f"frame_axioms: the frame condition {cond!r} has no "
734
+ f"first-order frame condition, so the standard translation "
735
+ f"cannot express {frame!r} (Löb, S4.1 and Grz need the "
736
+ f"higher-order routes: hol.isabelle_modal / hol.thf_modal, or "
737
+ f"the finite-frame enumerator atp.kripke_enum)")
738
+ if _R_ALETHIC in used:
739
+ axioms += [unguarded_frame_axiom(cond, _R_ALETHIC, prefix="v")
740
+ for cond in alethic]
741
+ if _R_TEMPORAL in used and temporal_closure:
742
+ axioms += [unguarded_frame_axiom("refl", _R_TEMPORAL, prefix="v"),
743
+ unguarded_frame_axiom("trans", _R_TEMPORAL, prefix="v")]
744
+ if _R_TEMPORAL in used and _R_NEXT in used:
745
+ # Vacuous — and misleading — unless BOTH relations occur.
746
+ axioms.append(_link(_R_NEXT, _R_TEMPORAL))
747
+ if temporal_closure:
748
+ axioms.append(_temporal_first_step())
749
+ if _R_DEONTIC in used:
750
+ axioms.append(unguarded_frame_axiom("serial", _R_DEONTIC, prefix="v"))
751
+ for family, system in dict(systems or {}).items():
752
+ if family not in _AGENT_FAMILY_PREFIX:
753
+ raise ValueError(
754
+ f"frame_axioms: unknown modal family {family!r} in systems "
755
+ f"(known: {sorted(_AGENT_FAMILY_PREFIX)})")
756
+ prefix = _AGENT_FAMILY_PREFIX[family]
757
+ try:
758
+ conds = resolve_frame(system)
759
+ except ValueError as exc:
760
+ raise ValueError(f"frame_axioms: {exc}") from None
761
+ for relation in sorted(r for r in used if r.startswith(prefix)):
762
+ axioms += [unguarded_frame_axiom(cond, relation, prefix="v")
763
+ for cond in conds]
764
+ for member in sort_membership_axioms(formula):
765
+ assert isinstance(member, Atom) # sort_membership_axioms yields atoms ``S(c)`` only
766
+ axioms.append(Quantifier(_FORALL, Variable("v0"), Atom(
767
+ _user_predicate(member.predicate), (member.args[0], Variable("v0")))))
768
+ return axioms
769
+
770
+
771
+ def _frame_axioms(frame: str) -> List[Node]:
772
+ """Deprecated alias: :func:`frame_axioms` for an alethic-only formula.
773
+
774
+ Kept because the name was imported inside the kit; it cannot see which
775
+ relations a formula uses, so it only ever constrained ``R``.
776
+ """
777
+ return frame_axioms(Box(Atom("P", ())), frame=frame)
778
+
779
+
780
+ def hybrid_is_valid(formula: Node, frame: str = "K", timeout: int = 10000,
781
+ systems=None, temporal_closure: bool = True) -> bool:
782
+ """Return True iff the hybrid-modal ``formula`` is valid over ``frame`` (via Z3).
783
+
784
+ Validity of H(@) over a frame class: true at EVERY world of EVERY Kripke
785
+ model whose alethic relation satisfies the frame conditions, under EVERY
786
+ nominal assignment. The check is the standard translation closed over the
787
+ current world under the frame axioms::
788
+
789
+ frame_axioms → ∀w ST(formula)(w)
790
+
791
+ handed to the Z3 validity oracle. The nominal constants ``nom_i`` are left
792
+ FREE in that implication — first-order validity quantifies free constants
793
+ universally, which is exactly "for every nominal assignment" (each constant
794
+ denotes exactly one domain element = one world, matching a nominal's
795
+ name-exactly-one-world semantics).
796
+
797
+ ``frame`` constrains the ALETHIC relation and is any system of the shared
798
+ registry (:mod:`unicode_logic_kit.fol.frames`) or a Scott–Lemmon spec; a
799
+ system with no first-order condition (GL, S4.1, Grz) is refused by name.
800
+ The OTHER relations the translation emits get the conventions
801
+ :func:`frame_axioms` documents — temporal ``T`` reflexive-transitive with
802
+ ``N ⊆ T`` (``temporal_closure=True``) and deontic ``D`` serial, both ON by
803
+ default, so this route agrees with ``fol.qml`` and with the Kripke
804
+ evaluator instead of reporting "not valid" for ``Ⓖφ → φ``; the
805
+ agent-indexed epistemic / doxastic / assertive / bouletic relations stay K
806
+ unless ``systems={"epistemic": "S5", …}`` asks for more.
807
+
808
+ Soundness/completeness: first-order validity is only semi-decidable in
809
+ general, so ``is_valid`` may time out (returning False) on hard instances —
810
+ but hybrid logic H(@) over K is DECIDABLE, and the ST images of H(@)
811
+ formulas (two-variable-like, tiny) are well within Z3's reach in practice;
812
+ the frame-axiom variants used here (T/S4/S5) behave the same on these
813
+ inputs. ``True`` is always a real proof; treat ``False`` as
814
+ "not proven valid" (for these small hybrid instances: a genuine
815
+ countermodel).
816
+
817
+ Raises:
818
+ NotImplementedError: ``formula`` contains a ``Down`` node (the ↓
819
+ binder, N1). Adding ↓ makes hybrid validity UNDECIDABLE, so this
820
+ function's bare-``bool`` contract — where ``False`` is safe to
821
+ read as "a genuine countermodel" precisely BECAUSE H(@) over K is
822
+ decidable and Z3 reliably closes these small instances — no
823
+ longer holds: a bare ``False`` on a ↓-formula could equally be a
824
+ countermodel or an honest Z3 timeout on an undecidable query, and
825
+ this function has no second field to tell them apart. Use
826
+ :func:`down_is_valid` instead (a :class:`~unicode_logic_kit.atp.protocol.Verdict`,
827
+ PROVED-only — never claims REFUTED) or
828
+ :func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search` /
829
+ :class:`~unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend` for a
830
+ bounded-search REFUTED verdict.
831
+ """
832
+ for n in formula.walk():
833
+ if isinstance(n, Down):
834
+ raise NotImplementedError(
835
+ "hybrid_is_valid: the ↓ binder (Down) makes hybrid validity "
836
+ "undecidable, so this bare-bool, PROVED-and-REFUTED-conflating "
837
+ "check cannot honestly answer for it. Use down_is_valid "
838
+ "(PROVED-only, never REFUTED) or "
839
+ "unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend / "
840
+ "modal_enum_search (bounded search, REFUTED-only) instead."
841
+ )
842
+ from ..atp.z3_models import is_valid # local import (as in fol.qml): keeps fol importable without z3
843
+ w = Variable("w")
844
+ closed = Quantifier(_FORALL, w, standard_translation(formula, world="w"))
845
+ hyp = None
846
+ for axiom in frame_axioms(formula, frame, systems=systems,
847
+ temporal_closure=temporal_closure):
848
+ hyp = axiom if hyp is None else And(hyp, axiom)
849
+ goal = closed if hyp is None else Implies(hyp, closed)
850
+ return is_valid(goal, timeout=timeout)
851
+
852
+
853
+ # =========================
854
+ # ↓ validity via the standard translation + a direct Z3 solver call (N1)
855
+ # =========================
856
+ #
857
+ # down_is_valid is hybrid_is_valid's PROVED-only sibling for the FULL hybrid
858
+ # language H(@,↓). It exists as a SEPARATE function, not a keyword flag on
859
+ # hybrid_is_valid, precisely because the two make different promises:
860
+ # hybrid_is_valid's bare bool is safe only because H(@) over K is decidable
861
+ # (so "not proved" IS "refuted" there); down_is_valid must call the Z3
862
+ # Solver() directly — never the bare-bool is_valid() wrapper, which collapses
863
+ # Z3's 'sat' (a genuine countermodel) and 'unknown' (timeout / incompleteness
864
+ # on an undecidable query) into the same False (unicode_logic_kit.atp.z3_models
865
+ # .is_valid, confirmed by direct reading) — so it can tell the two apart and
866
+ # report them honestly as UNKNOWN, never smuggling a REFUTED claim out of a
867
+ # SAT witness this route never verified against a presentable finite
868
+ # KripkeModel (that verification is KripkeEnumBackend's job — see
869
+ # fol._hybrid_nodes' module docstring and atp.hybrid_down.down_decide, which
870
+ # combines the two into one call).
871
+
872
+ def down_is_valid(formula: Node, frame: str = "K", timeout: int = 10000,
873
+ systems=None, temporal_closure: bool = True) -> "Verdict":
874
+ """Return a :class:`~unicode_logic_kit.atp.protocol.Verdict` for the FULL
875
+ hybrid-modal ``formula`` (H(@,↓), including ``Down``/↓) over ``frame``.
876
+
877
+ Builds the exact same goal ``hybrid_is_valid`` does — the standard
878
+ translation, closed over the current world, under the frame axioms
879
+ (``standard_translation`` now threads ``Down``'s local rebinding through,
880
+ see the module docstring) — but decides it with a Z3 ``Solver()`` called
881
+ directly, so ``unsat`` (of the negated goal) and ``sat``/``unknown`` are
882
+ told apart instead of collapsed:
883
+
884
+ - Z3 ``unsat`` → ``PROVED`` — sound unconditionally: soundness of a
885
+ Z3-``unsat`` verdict depends only on Z3's own soundness, never on
886
+ whether Z3 is a COMPLETE decision procedure for this (undecidable)
887
+ fragment (see ``fol._hybrid_nodes``' module docstring for the
888
+ co-r.e. argument this rests on: ST is meaning-preserving for H(@,↓)
889
+ — Areces/Blackburn/Marx 1999 — so FO-``unsat``-of-the-negation IS
890
+ H(@,↓)-validity, exactly).
891
+ - Z3 ``sat`` (of the negated goal) → ``UNKNOWN`` / ``reason="incomplete"``
892
+ — NEVER ``REFUTED``. A SAT witness here would, ON INSPECTION, also be a
893
+ sound Kripke countermodel in principle (same correspondence as above,
894
+ run in the other direction) — this is a deliberate COMPLETENESS
895
+ sacrifice, not a soundness requirement, made to sidestep turning an
896
+ arbitrary Z3 model back into a presentable, inspectable finite
897
+ :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`. Use
898
+ :func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search` /
899
+ :class:`~unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend` for an
900
+ actual REFUTED verdict, with a countermodel independently
901
+ re-verified by :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`
902
+ — or :func:`~unicode_logic_kit.atp.hybrid_down.down_decide`, which runs
903
+ both routes and combines them.
904
+ - Z3 times out → ``UNKNOWN`` / ``reason="timeout"``.
905
+
906
+ This function decides ANY formula ``standard_translation`` accepts
907
+ (``Down`` or not) — it is not restricted to ↓-containing input; a plain
908
+ H(@) formula is handled identically, just more conservatively than
909
+ ``hybrid_is_valid`` (which, for THAT decidable fragment, is entitled to —
910
+ and does — read Z3 ``sat``/``unknown`` as a genuine countermodel).
911
+ """
912
+ import time
913
+ from ..atp.protocol import PROVED, UNKNOWN, Verdict # local: keeps fol importable without atp
914
+ from z3 import Not as _ZNot, Solver, sat, unsat
915
+
916
+ w = Variable("w")
917
+ closed = Quantifier(_FORALL, w, standard_translation(formula, world="w"))
918
+ hyp = None
919
+ for axiom in frame_axioms(formula, frame, systems=systems,
920
+ temporal_closure=temporal_closure):
921
+ hyp = axiom if hyp is None else And(hyp, axiom)
922
+ goal = closed if hyp is None else Implies(hyp, closed)
923
+
924
+ solver = Solver()
925
+ solver.set("timeout", timeout)
926
+ solver.set("random_seed", 42)
927
+ solver.add(_ZNot(goal.to_z3()))
928
+ start = time.perf_counter()
929
+ result = solver.check()
930
+ elapsed = time.perf_counter() - start
931
+
932
+ if result == unsat:
933
+ return Verdict(PROVED, "down_is_valid", logic="hybrid", wall_time=elapsed)
934
+ if result == sat:
935
+ return Verdict(
936
+ UNKNOWN, "down_is_valid", logic="hybrid", reason="incomplete", wall_time=elapsed,
937
+ detail=("Z3 found a model of the negated goal; down_is_valid never "
938
+ "reads this as REFUTED (see its own docstring) — use "
939
+ "atp.kripke_enum.KripkeEnumBackend / modal_enum_search, or "
940
+ "atp.hybrid_down.down_decide, for a genuine countermodel."))
941
+ why = solver.reason_unknown()
942
+ reason = "timeout" if ("timeout" in why or "cancel" in why) else "incomplete"
943
+ return Verdict(UNKNOWN, "down_is_valid", logic="hybrid", reason=reason,
944
+ wall_time=elapsed, detail=why)