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,741 @@
1
+ """Bounded enumeration of finite Kripke models — a semantic refutation oracle for
2
+ the modal fragment the labelled tableau cannot decide.
3
+
4
+ :mod:`unicode_logic_kit.atp.modal_tableau` has no proof rule for the temporal
5
+ *closure* operators (Always ``Ⓖ`` / Eventually ``Ⓕ`` / Until ``Ⓤ`` /
6
+ Historically ``⒣`` / Once ``⒫`` / Previous ``⒴`` / Since ``⒮`` — see
7
+ ``modal_tableau._TEMPORAL_CLOSURE``): it marks a labelled formula built from one
8
+ of them INERT and reports ``"unknown"`` for any branch that only stays open
9
+ because of it. The quantified-modal-logic route (``fol.qml``) is sound but
10
+ proof-only (Z3 over the standard translation never REFUTES). Net effect: an
11
+ INVALID temporal formula such as ``Ⓕ P → P`` stays "unknown" — nothing in the
12
+ kit ever refutes it.
13
+
14
+ This module closes that gap for **refutation only**, the same way
15
+ :mod:`unicode_logic_kit.semantics.modelfinder` closes the analogous gap for plain
16
+ FOL: it exhaustively enumerates finite :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`\\ s
17
+ of increasing world-count and hands each one to the EXISTING, already-tested
18
+ evaluator, :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`, as an
19
+ oracle. Soundness is by construction: a returned countermodel is one
20
+ ``satisfies_modal`` itself confirms falsifies the formula at world 0, so this
21
+ module contains no new semantic rule that could disagree with the one every
22
+ other Kripke-based route in the kit already relies on.
23
+
24
+ **What is enumerated.** For each relation family the formula actually uses
25
+ (``"alethic"`` for Box/Diamond, ``"K:"+agent`` / ``"B:"+agent`` /
26
+ ``"Say:"+agent`` / ``"Want:"+agent`` for Knows/Believes/Says/Wants,
27
+ ``"deontic"`` for Obligatory/Permitted, ``"temporal"`` for every temporal
28
+ operator — the exact naming convention documented in
29
+ :mod:`unicode_logic_kit.semantics.kripke`), every relation over ``{0, …, n-1}``
30
+ satisfying that family's frame condition is enumerated (the SAME frame-name
31
+ vocabulary as :mod:`unicode_logic_kit.atp.modal_tableau`: K/T/D/KD/B/KB/K4/K45/
32
+ S4/S5/KD45 for ``frame=``, plus per-family overrides via ``systems=``), crossed
33
+ with every valuation of the formula's ground atoms over those worlds. ``n``
34
+ ranges over ``1 .. max_worlds``. Each candidate model is checked with
35
+ ``satisfies_modal(formula, model, 0)``; the first candidate where it comes back
36
+ ``False`` is returned as the countermodel.
37
+
38
+ **Many-sorted ground formulas.** ``satisfies_modal`` relativizes a formula with a
39
+ sorted constant (``Mortal(socrates:Human)``) before it reads an atom, so the atom
40
+ it looks up is ``Mortal(socrates)``; the enumerator relativizes first too, and
41
+ varies THAT key. A sorted constant ``c:S`` is an element of ``S`` at every world
42
+ (``semantics.kripke``'s module docstring), so the guard atom ``S(c)`` is not
43
+ varied: it is true at every world of every candidate, and a countermodel this
44
+ search returns is therefore a model the many-sorted routes (``qml_is_valid``,
45
+ ``api.prove``) would consider. A sorted QUANTIFIER still makes ``satisfies_modal``
46
+ ask for object domains, which these propositional models do not carry, and is
47
+ reported ``unsupported``.
48
+
49
+ **Three-way honesty, not two.** A search that finds no countermodel does NOT
50
+ mean the formula is valid — it means one of two different things, and
51
+ :class:`EnumSearchResult` keeps them apart instead of collapsing them into a
52
+ single ``None``:
53
+
54
+ - ``exhausted=True`` — EVERY candidate up to ``max_worlds`` was checked and
55
+ none refuted the formula. This is a genuine, documentable statement ("no
56
+ countermodel with ≤ ``max_worlds`` worlds, under this frame, exists"), but it
57
+ is still **not a validity proof**: modal logic has no small-model property
58
+ bounding countermodels to 3 worlds in general, so a larger, unexplored model
59
+ might still refute the formula.
60
+ - ``exhausted=False`` (with ``model=None``) — the ``max_models`` candidate
61
+ budget ran out before the ``max_worlds`` search space was fully covered, or
62
+ ``max_atoms`` rejected the formula up front. Weaker than "exhausted": the
63
+ small worlds were not even fully explored.
64
+ - ``timed_out=True`` (with ``model=None``) — the wall-clock ``timeout`` ran out
65
+ first. The clock is read before every candidate model, while the relations
66
+ and valuations of a world count are built, and while the next world count's
67
+ relations are enumerated, so a search ends within the cost of one candidate
68
+ (or of 256 edge sets of the relation enumeration) of its deadline however
69
+ large the space is; a single evaluation of the formula in one model is not
70
+ interrupted.
71
+
72
+ A formula outside the propositional/ground modal fragment ``satisfies_modal``
73
+ itself understands (a first-order quantifier with no per-world domain, an
74
+ unassigned hybrid nominal, a Lewis counterfactual, …) makes the evaluator raise
75
+ — caught here and reported as ``unsupported`` rather than silently skipped or
76
+ misreported as a bound. So is a formula in which two different atoms print alike (the
77
+ numeral ``1`` and a constant named ``1``, a free variable ``x`` and a constant named ``x``)
78
+ or two different agents are named alike: a valuation and a relation family are keyed by the
79
+ written form, so the pair would be ONE key of every candidate and "no countermodel" would be
80
+ said of a formula that has one.
81
+
82
+ **Determinism.** Every enumeration order (relations by bitmask over a
83
+ fixed, sorted pair list; valuations by bitmask over a fixed, sorted atom list;
84
+ worlds by increasing count) is fixed and free of hashing/set-iteration
85
+ nondeterminism, so two calls with identical arguments return bit-for-bit the
86
+ same model.
87
+
88
+ **Cost.** The candidate space is
89
+ ``sum_{n=1..max_worlds} (worlds^worlds-per-family-relation-count) * 2^(atoms * n)``
90
+ — it grows fast in both the number of distinct relation families the formula
91
+ mixes and its atom count. ``max_atoms`` and ``max_models`` are the two knobs
92
+ that keep a pathological formula from hanging; both give an honest
93
+ ``exhausted=False`` rather than either blocking forever or answering wrong. The
94
+ third, ``timeout`` (milliseconds), is the only one that bounds the TIME rather
95
+ than the size of the search; it gives ``timed_out=True``.
96
+
97
+ Public API: :class:`EnumSearchResult`, :func:`modal_enum_search`,
98
+ :func:`modal_enum_countermodel`, :class:`KripkeEnumBackend`, plus the witness
99
+ converters :func:`kripke_model_to_dict` / :func:`kripke_model_from_dict`
100
+ (the ``"data"`` payload of every ``"kripke"`` witness dict).
101
+ """
102
+
103
+ import itertools
104
+ from dataclasses import dataclass
105
+ from functools import lru_cache
106
+ from typing import Dict, FrozenSet, Optional, Sequence, Tuple
107
+
108
+ from .._deadline import instant as _instant, passed as _passed
109
+ from ..fol.nodes import (
110
+ Node, Atom,
111
+ Box, Diamond, Knows, Believes, Says, Wants, Obligatory, Permitted,
112
+ Next, Always, Eventually, Until, Historically, Once, Previous, Since,
113
+ EverybodyKnows, DistributedKnowledge, CommonKnowledge,
114
+ sort_axioms, sort_membership_axioms,
115
+ )
116
+ from ..fol._atom_keys import AtomKeys, refuse_alike_agents
117
+ from ..fol._msfl_nodes import key_text
118
+ from ..fol._truth_constants import truth_value as _truth_value
119
+ from ..semantics.kripke import KripkeModel, satisfies_modal
120
+ from ..fol.frames import (
121
+ FRAMES as _FRAMES, resolve_frame, holds_on_finite_frame,
122
+ )
123
+ from .protocol import ProverBackend, Verdict, REFUTED, UNKNOWN
124
+
125
+ # Relation-name convention — must match semantics.kripke.KripkeModel exactly
126
+ # (see that module's docstring, "Relation-name convention").
127
+ _ALETHIC = "alethic"
128
+ _DEONTIC = "deontic"
129
+ _TEMPORAL = "temporal"
130
+ _KNOWS_PREFIX = "K:"
131
+ _BELIEVES_PREFIX = "B:"
132
+ _SAYS_PREFIX = "Say:"
133
+ _WANTS_PREFIX = "Want:"
134
+
135
+ #: Node types whose satisfaction is defined over the "temporal" relation.
136
+ _TEMPORAL_TYPES = (Next, Always, Eventually, Until, Historically, Once, Previous, Since)
137
+
138
+ #: Frame-condition-catching exceptions from satisfies_modal — anything here on
139
+ #: a candidate means the FORMULA (not the candidate model) is out of scope for
140
+ #: this propositional/ground enumerator; see the module docstring's "A formula
141
+ #: outside the propositional/ground modal fragment" paragraph.
142
+ _UNSUPPORTED_EXC = (NotImplementedError, ValueError, TypeError, KeyError)
143
+
144
+
145
+ def _agent_key(agent: Node) -> str:
146
+ """Relation-key suffix for an epistemic/doxastic/assertive/bouletic agent term.
147
+
148
+ Mirrors the identically-named private helper in ``semantics.kripke`` and
149
+ ``atp.modal_tableau`` (each module keeps its own copy rather than share a
150
+ private cross-module import for a two-line function): the agent's
151
+ ``.name`` if it has one (a Constant or Variable), else its rendered form.
152
+ """
153
+ return getattr(agent, "name", None) or key_text(agent)
154
+
155
+
156
+ def _collect(formula: Node) -> Tuple[Tuple[str, ...], Tuple[str, ...]]:
157
+ """Scan ``formula`` for its ground-atom keys and the relation families it uses.
158
+
159
+ Returns ``(atoms, families)``, both sorted tuples (deterministic order):
160
+ ``atoms`` are the ``key_text`` keys (every constant written by its bare name) the
161
+ valuation must cover;
162
+ ``families`` are the relation NAMES (in the ``semantics.kripke`` convention)
163
+ the enumerator must build a relation for — one entry per distinct alethic /
164
+ epistemic-per-agent / doxastic-per-agent / assertive-per-agent /
165
+ bouletic-per-agent / deontic / temporal operator family actually present.
166
+ A formula with no modal operators at all yields an empty ``families``
167
+ tuple (the propositional case: only world 0's valuation matters).
168
+ """
169
+ atoms: set = set()
170
+ families: set = set()
171
+ for node in formula.walk():
172
+ if isinstance(node, Atom):
173
+ if _truth_value(node) is None: # `$true` / `$false` are not varied
174
+ atoms.add(key_text(node))
175
+ elif isinstance(node, (Box, Diamond)):
176
+ families.add(_ALETHIC)
177
+ elif isinstance(node, Knows):
178
+ families.add(_KNOWS_PREFIX + _agent_key(node.agent))
179
+ elif isinstance(node, (EverybodyKnows, DistributedKnowledge, CommonKnowledge)):
180
+ # Every group member's "K:"+agent relation is searched and frame-
181
+ # checked. Dropping it would fix the relation empty on every
182
+ # candidate, which under e.g. S5 is not even a legal frame and
183
+ # "refutes" the valid E_{a} P → P.
184
+ families.update(_KNOWS_PREFIX + _agent_key(member) for member in node.group)
185
+ elif isinstance(node, Believes):
186
+ families.add(_BELIEVES_PREFIX + _agent_key(node.agent))
187
+ elif isinstance(node, Says):
188
+ families.add(_SAYS_PREFIX + _agent_key(node.agent))
189
+ elif isinstance(node, Wants):
190
+ families.add(_WANTS_PREFIX + _agent_key(node.agent))
191
+ elif isinstance(node, (Obligatory, Permitted)):
192
+ families.add(_DEONTIC)
193
+ elif isinstance(node, _TEMPORAL_TYPES):
194
+ families.add(_TEMPORAL)
195
+ return tuple(sorted(atoms)), tuple(sorted(families))
196
+
197
+
198
+ def _check_frame(frame: str, systems: Optional[Dict[str, str]]) -> None:
199
+ """Resolve every frame name this search will use, raising if any is unknown.
200
+
201
+ The enumerator carries every condition in the shared registry that a
202
+ FINITE frame check can decide — every first-order condition (a condition
203
+ on a finite relation is directly checkable), and, via each one's finite
204
+ structural characterisation (:func:`~unicode_logic_kit.fol.frames.holds_on_finite_frame`),
205
+ Löb, McKinsey and Grz too — which is more than the labelled tableau's
206
+ rule set, so it validates frames itself rather than borrowing the
207
+ tableau's stricter check. There is nothing left for this function to
208
+ refuse by condition: it only resolves each name, which still raises
209
+ ``ValueError`` for one that names no known system or Geach spec.
210
+ """
211
+ names = [frame]
212
+ for fam, sys in (systems or {}).items():
213
+ if fam not in ("epistemic", "doxastic", "deontic", "temporal"):
214
+ raise ValueError(
215
+ f"kripke_enum: unknown system family {fam!r} (use epistemic / "
216
+ "doxastic / deontic / temporal).")
217
+ names.append(sys)
218
+ for name in names:
219
+ try:
220
+ resolve_frame(name)
221
+ except ValueError as exc:
222
+ raise ValueError(f"kripke_enum: {exc}") from None
223
+
224
+
225
+ def _conditions_for(relname: str, frame: str, systems: Optional[Dict[str, str]]) -> Tuple[str, ...]:
226
+ """Frame conditions for a relation name, mirroring ``modal_tableau._Ctx.conds``.
227
+
228
+ ``"alethic"`` uses ``frame`` directly; ``"deontic"``/``"temporal"`` and the
229
+ ``"K:"``/``"B:"`` families use their ``systems[...]`` override (default
230
+ ``"KD"`` for deontic, ``"K"`` for the rest) — the exact same defaulting
231
+ ``atp.modal_tableau`` applies, so a formula decided by both routes is
232
+ decided over the SAME frame. ``"Say:"``/``"Want:"`` agents and any other
233
+ family get no frame condition (plain K): Says/Wants are documented as
234
+ non-factive/non-veridical K-modalities with no ``systems`` entry in
235
+ ``modal_tableau`` either.
236
+ """
237
+ systems = systems or {}
238
+ if relname == _ALETHIC:
239
+ return resolve_frame(frame)
240
+ if relname == _DEONTIC:
241
+ return resolve_frame(systems.get("deontic", "KD"))
242
+ if relname == _TEMPORAL:
243
+ return resolve_frame(systems.get("temporal", "K"))
244
+ if relname.startswith(_KNOWS_PREFIX):
245
+ return resolve_frame(systems.get("epistemic", "K"))
246
+ if relname.startswith(_BELIEVES_PREFIX):
247
+ return resolve_frame(systems.get("doxastic", "K"))
248
+ return ()
249
+
250
+
251
+ def _holds_conditions(edges: FrozenSet[Tuple[int, int]], n: int, conditions: Tuple[str, ...]) -> bool:
252
+ """True iff the edge set ``edges`` over ``range(n)`` satisfies every condition.
253
+
254
+ Delegates to :func:`unicode_logic_kit.fol.frames.holds_on_finite_frame`, the
255
+ checker the correspondence tests use as well. Delegating matters for
256
+ SOUNDNESS, not tidiness: this function used to test the five conditions it
257
+ knew and IGNORE any other, so a frame class it did not recognise silently
258
+ widened to the ones it did — and a countermodel the named system excludes
259
+ would have been reported as if it refuted the formula. An unknown
260
+ condition now raises instead — Löb/Grz/McKinsey are no exception: each
261
+ is decided by its own finite structural characterisation, not raised.
262
+ """
263
+ return all(holds_on_finite_frame(cond, edges, n) for cond in conditions)
264
+
265
+
266
+ @lru_cache(maxsize=None)
267
+ def _valid_relations(n: int, conditions: Tuple[str, ...]) -> Tuple[FrozenSet[Tuple[int, int]], ...]:
268
+ """Every edge set over ``range(n) x range(n)`` satisfying ``conditions``, in bitmask order.
269
+
270
+ Deterministic and cached (families sharing the same conditions — e.g. two
271
+ K-system epistemic agents — reuse the same enumeration and the same cache
272
+ entry). Brute-force over ``2**(n*n)`` subsets, which is why callers keep
273
+ ``max_worlds`` small (n=3 already means 512 subsets to filter per family).
274
+ """
275
+ pairs = [(a, b) for a in range(n) for b in range(n)]
276
+ out = []
277
+ for mask in range(1 << len(pairs)):
278
+ edges = frozenset(p for i, p in enumerate(pairs) if (mask >> i) & 1)
279
+ if _holds_conditions(edges, n, conditions):
280
+ out.append(edges)
281
+ return tuple(out)
282
+
283
+
284
+ #: The world counts up to which :func:`_valid_relations_until` uses :func:`_valid_relations`
285
+ #: even under a deadline: ``2 ** (n * n)`` masks are a few hundred, so it cannot overrun.
286
+ _UNTIMED_RELATION_WORLDS = 3
287
+
288
+ #: The relation sets that :func:`_valid_relations_until` completed under a deadline.
289
+ _TIMED_RELATIONS: Dict[Tuple[int, Tuple[str, ...]], Tuple[FrozenSet[Tuple[int, int]], ...]] = {}
290
+
291
+
292
+ def _valid_relations_until(n: int, conditions: Tuple[str, ...],
293
+ deadline: Optional[float]) -> Optional[Tuple[FrozenSet[Tuple[int, int]], ...]]:
294
+ """:func:`_valid_relations`, or ``None`` if ``deadline`` (a ``perf_counter`` instant) passes first.
295
+
296
+ Without a deadline, and for a world count too small to matter, this is
297
+ :func:`_valid_relations` itself. Past that the ``2 ** (n * n)`` masks are enumerated here,
298
+ reading the clock every 256 of them; a completed enumeration is kept, so a later call with
299
+ the same ``n`` and ``conditions`` does not repeat it, and an interrupted one leaves nothing
300
+ behind.
301
+ """
302
+ if deadline is None or n <= _UNTIMED_RELATION_WORLDS:
303
+ return _valid_relations(n, conditions)
304
+ key = (n, conditions)
305
+ if key in _TIMED_RELATIONS:
306
+ return _TIMED_RELATIONS[key]
307
+ pairs = [(a, b) for a in range(n) for b in range(n)]
308
+ out = []
309
+ for mask in range(1 << len(pairs)):
310
+ if not mask & 0xFF and _passed(deadline):
311
+ return None
312
+ edges = frozenset(p for i, p in enumerate(pairs) if (mask >> i) & 1)
313
+ if _holds_conditions(edges, n, conditions):
314
+ out.append(edges)
315
+ _TIMED_RELATIONS[key] = tuple(out)
316
+ return _TIMED_RELATIONS[key]
317
+
318
+
319
+ def _scan_sorted(formula: Node) -> Tuple[Node, Tuple[str, ...]]:
320
+ """The formula the evaluator will read, and the atom keys a legal model fixes true.
321
+
322
+ ``satisfies_modal`` relativizes a many-sorted formula before it reads an atom,
323
+ so ``Mortal(socrates:Human)`` is looked up under the key ``Mortal(socrates)``
324
+ — NOT under the key the unrelativized formula prints. The atoms the
325
+ enumerator varies must be the atoms the evaluator reads, so a sorted formula
326
+ is relativized HERE, once, before :func:`_collect` scans it.
327
+
328
+ A sorted constant ``c:S`` also denotes an element of ``S`` at every world
329
+ (it is a rigid designator; see the ``semantics.kripke`` module docstring), so
330
+ the guard atom ``S(c)`` is not a free atom to enumerate: it is true in every
331
+ world of every candidate. The second result lists those keys, sorted.
332
+
333
+ A formula without any many-sorted node is returned UNCHANGED, with no fixed
334
+ keys, so every unsorted caller sees exactly the search it always did.
335
+ """
336
+ if not sort_axioms(formula):
337
+ return formula, ()
338
+ fixed = tuple(sorted({key_text(atom)
339
+ for atom in sort_membership_axioms(formula)}))
340
+ return formula._relativize([]), fixed
341
+
342
+
343
+ #: The number of valuations up to which one world count's valuations are held in a list
344
+ #: and reused for every relation choice (see :func:`modal_enum_search`).
345
+ _CACHED_VALUATIONS = 1 << 14
346
+
347
+
348
+ def _bitmask_tuples(count: int, n: int):
349
+ """Yield every tuple of ``n`` integers of ``range(count)``, the first slowest.
350
+
351
+ The order of ``itertools.product(range(count), repeat=n)``, produced one tuple at a time:
352
+ ``product`` first copies its input into a tuple, which for the ``2 ** m`` bitmasks of ``m``
353
+ atoms is more memory than a machine has long before ``m`` is large enough to matter.
354
+ """
355
+ if n == 0:
356
+ yield ()
357
+ return
358
+ for head in range(count):
359
+ for tail in _bitmask_tuples(count, n - 1):
360
+ yield (head,) + tail
361
+
362
+
363
+ def _valuations(atoms: Tuple[str, ...], n: int, fixed: Tuple[str, ...] = ()):
364
+ """Yield every valuation ``{world: frozenset(atoms true there)}`` over ``range(n)``.
365
+
366
+ Deterministic bitmask order: for ``n`` worlds and ``m`` atoms, each of the
367
+ ``(2**m)**n`` combinations is produced by ``itertools.product`` over a
368
+ per-world bitmask in ``range(2**m)``, world 0 varying slowest — the same
369
+ fixed order every call, so re-running a search reproduces the same model.
370
+ The atom keys in ``fixed`` are true at every world of every valuation and are
371
+ not enumerated.
372
+ """
373
+ m = len(atoms)
374
+ for combo in _bitmask_tuples(1 << m, n):
375
+ yield {
376
+ w: frozenset(atoms[i] for i in range(m) if (bits >> i) & 1) | frozenset(fixed)
377
+ for w, bits in enumerate(combo)
378
+ }
379
+
380
+
381
+ @dataclass(frozen=True)
382
+ class EnumSearchResult:
383
+ """The outcome of one :func:`modal_enum_search` call.
384
+
385
+ Fields:
386
+
387
+ ``model``
388
+ a verified :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`
389
+ falsifying the searched formula at world 0, or ``None`` if none was
390
+ found (whether because the space was exhausted, the budget ran out,
391
+ or the formula is unsupported — see ``exhausted``/``unsupported``).
392
+ ``exhausted``
393
+ ``True`` iff ``model is None`` because EVERY candidate up to
394
+ ``max_worlds`` was checked and none refuted the formula — a genuine
395
+ "no countermodel with ≤ max_worlds worlds" statement, but NEVER a
396
+ validity proof (see the module docstring). Always ``False`` when
397
+ ``model`` is not ``None`` or ``unsupported`` is not ``None``.
398
+ ``checked``
399
+ the number of distinct candidate models actually run through
400
+ ``satisfies_modal`` (informational; bounded by ``max_models``).
401
+ ``unsupported``
402
+ ``None`` if the formula is within the propositional/ground modal
403
+ fragment ``satisfies_modal`` understands; otherwise the
404
+ ``"ExceptionType: message"`` the evaluator raised on the formula,
405
+ naming exactly why it is out of scope.
406
+ ``detail``
407
+ a short free-text explanation of which of the above happened.
408
+ ``timed_out``
409
+ ``True`` iff ``model is None`` because the ``timeout`` of the call
410
+ passed before the search was complete. Always ``False`` for a search
411
+ without a ``timeout``, for a model that was found and for
412
+ ``exhausted=True``.
413
+ """
414
+
415
+ model: Optional[KripkeModel]
416
+ exhausted: bool
417
+ checked: int
418
+ unsupported: Optional[str] = None
419
+ detail: str = ""
420
+ timed_out: bool = False
421
+
422
+ def to_dict(self) -> dict:
423
+ """Serialise to a JSON-compatible dict (the model, if any, as worlds/relations/valuation)."""
424
+ return {
425
+ "found": self.model is not None,
426
+ "exhausted": self.exhausted,
427
+ "checked": self.checked,
428
+ "unsupported": self.unsupported,
429
+ "detail": self.detail,
430
+ "timed_out": self.timed_out,
431
+ "model": kripke_model_to_dict(self.model) if self.model is not None else None,
432
+ }
433
+
434
+
435
+ def kripke_model_to_dict(model: KripkeModel) -> dict:
436
+ """Render a :class:`KripkeModel` as a JSON-compatible dict.
437
+
438
+ The shape is the structured ``"data"`` payload of every ``"kripke"``
439
+ witness dict a :class:`~unicode_logic_kit.atp.protocol.Verdict` carries:
440
+ ``{"worlds": [...], "relations": {name: [[w, w'], ...]}, "valuation":
441
+ {str(world): [atom_key, ...]}}``, plus ``"nominals"`` / ``"domains"``
442
+ keys ONLY when the model actually has them (the purely propositional
443
+ models this module enumerates never do). Valuation/domain keys are
444
+ ``str(world)`` because JSON object keys must be strings;
445
+ :func:`kripke_model_from_dict` resolves them back against the typed
446
+ ``"worlds"`` list.
447
+ """
448
+ data = {
449
+ "worlds": sorted(model.worlds),
450
+ "relations": {
451
+ name: sorted([list(edge) for edge in edges])
452
+ for name, edges in model.relations.items()
453
+ },
454
+ "valuation": {
455
+ str(world): sorted(atoms) for world, atoms in model.valuation.items()
456
+ },
457
+ }
458
+ if model.nominals:
459
+ data["nominals"] = dict(sorted(model.nominals.items()))
460
+ if model.domains is not None:
461
+ data["domains"] = {
462
+ str(world): sorted(individuals, key=repr)
463
+ for world, individuals in model.domains.items()
464
+ }
465
+ return data
466
+
467
+
468
+ def kripke_model_from_dict(data: dict) -> KripkeModel:
469
+ """Rebuild a :class:`KripkeModel` from :func:`kripke_model_to_dict` output.
470
+
471
+ The inverse of :func:`kripke_model_to_dict` up to the container copying
472
+ :class:`KripkeModel`'s constructor performs anyway: worlds, relations,
473
+ valuation, and (when present) nominals and per-world domains all round
474
+ trip. Valuation/domain keys arrive as ``str(world)`` and are mapped back
475
+ to the typed world via the ``"worlds"`` list; a key matching no world is
476
+ kept verbatim rather than guessed at (``KripkeModel`` treats an unknown
477
+ valuation world as simply never queried).
478
+ """
479
+ worlds = list(data["worlds"])
480
+ by_str = {str(world): world for world in worlds}
481
+ relations = {
482
+ name: [tuple(edge) for edge in edges]
483
+ for name, edges in data.get("relations", {}).items()
484
+ }
485
+ valuation = {
486
+ by_str.get(key, key): set(atoms)
487
+ for key, atoms in data.get("valuation", {}).items()
488
+ }
489
+ domains = None
490
+ if "domains" in data:
491
+ domains = {
492
+ by_str.get(key, key): set(individuals)
493
+ for key, individuals in data["domains"].items()
494
+ }
495
+ return KripkeModel(worlds, relations, valuation,
496
+ domains=domains, nominals=data.get("nominals"))
497
+
498
+
499
+ def modal_enum_search(formula: Node, *, frame: str = "K",
500
+ systems: Optional[Dict[str, str]] = None,
501
+ max_worlds: int = 3, max_atoms: Optional[int] = None,
502
+ max_models: int = 200000,
503
+ timeout: Optional[float] = None) -> EnumSearchResult:
504
+ """Exhaustively search finite Kripke models of increasing size for a countermodel.
505
+
506
+ Enumerates every relation (per family, per the ``frame``/``systems`` frame
507
+ conditions — see :func:`_conditions_for`) and every atom valuation over
508
+ ``n = 1 .. max_worlds`` worlds, in a fixed deterministic order, and checks
509
+ each candidate with :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`
510
+ (the existing, already-tested evaluator — this function adds no new
511
+ semantic rule, only the search). The first candidate that falsifies
512
+ ``formula`` at world 0 is returned immediately.
513
+
514
+ Args:
515
+ formula: the (already premise-folded, if applicable — see
516
+ :class:`KripkeEnumBackend`) formula to search for a countermodel of.
517
+ frame: the alethic frame name for ``"alethic"`` relations (Box/Diamond)
518
+ — any name in :data:`unicode_logic_kit.fol.frames.FRAMES`, including
519
+ ``"GL"``/``"S4.1"``/``"Grz"`` (decided via their finite structural
520
+ characterisation — see the module docstring) and a Scott–Lemmon
521
+ spec like ``"G(1,1,1,1)"``.
522
+ systems: per-family frame overrides for deontic/temporal/epistemic/
523
+ doxastic relations (``{"epistemic": "S5", ...}``), same convention
524
+ and same defaults (KD for deontic, K for the rest) as
525
+ ``modal_tableau``.
526
+ max_worlds: the largest world-count searched (inclusive). Every ``n``
527
+ from 1 up to this is tried in order, smallest countermodels first.
528
+ max_atoms: if given, a formula using more than this many distinct
529
+ ground atoms is rejected up front (``exhausted=False``,
530
+ ``model=None``) rather than attempting a valuation space of
531
+ ``2**(atoms*n)`` per world-count.
532
+ max_models: the total number of candidate models actually evaluated
533
+ (across every ``n``) before giving up. Bounds the worst case
534
+ combinatorially, at the cost of an honest ``exhausted=False``
535
+ instead of a complete search.
536
+ timeout: a wall-clock limit in milliseconds, counted from the start of
537
+ the call (default ``None``: no limit, the search is exactly the one
538
+ it was without the argument). The clock is read before every
539
+ candidate model and while the relations and valuations of a world
540
+ count are built, so the search returns within the cost of one
541
+ candidate of its deadline; the result has ``timed_out=True``,
542
+ ``exhausted=False`` and no model.
543
+
544
+ Returns:
545
+ An :class:`EnumSearchResult` — see its docstring for how to read
546
+ ``model``/``exhausted``/``unsupported`` together.
547
+
548
+ Raises:
549
+ ValueError: if ``frame`` or a ``systems`` entry names an unknown frame
550
+ or system family. ``"GL"``/``"S4.1"``/``"Grz"`` are NOT refused —
551
+ each has a finite structural characterisation
552
+ (:func:`unicode_logic_kit.fol.frames.holds_on_finite_frame`) that
553
+ this bounded enumerator decides directly, unlike the routes that
554
+ emit a single first-order sentence over frames of every
555
+ cardinality (``fol.qml``, ``fol.modal_translation``, ``atp.fitch``,
556
+ the labelled tableau), which still refuse them.
557
+ """
558
+ _check_frame(frame, systems)
559
+ try:
560
+ scanned, fixed = _scan_sorted(formula)
561
+ except _UNSUPPORTED_EXC + (RuntimeError,) as exc:
562
+ # relativizing walks the whole tree and meets a node it has no rule for
563
+ # (a Łukasiewicz operator under a sorted binder): the evaluator refuses
564
+ # that formula too, so report it the way an evaluator refusal is reported.
565
+ return EnumSearchResult(
566
+ model=None, exhausted=False, checked=0,
567
+ unsupported=f"{type(exc).__name__}: {exc}",
568
+ detail=("a many-sorted formula could not be relativized — it is "
569
+ "outside the propositional/ground modal fragment this "
570
+ "enumerator supports"),
571
+ )
572
+ try:
573
+ AtomKeys("modal_enum_search").letters([scanned])
574
+ refuse_alike_agents([scanned], "modal_enum_search")
575
+ except NotImplementedError as exc:
576
+ # A valuation and a relation family are named by text: two different atoms (or two
577
+ # different agents) that print alike would be ONE key of every candidate model, and a
578
+ # search that read them as one would report "no countermodel" for a formula that has one.
579
+ return EnumSearchResult(
580
+ model=None, exhausted=False, checked=0,
581
+ unsupported=f"{type(exc).__name__}: {exc}",
582
+ detail=("two different atoms (or agents) of the formula are written alike, so a "
583
+ "model keyed by the written form could not tell them apart — the formula "
584
+ "is outside the fragment this enumerator supports"),
585
+ )
586
+ atoms, families = _collect(scanned)
587
+ # the membership keys are fixed true, not varied: they cost no search space
588
+ atoms = tuple(a for a in atoms if a not in fixed)
589
+
590
+ if max_atoms is not None and len(atoms) > max_atoms:
591
+ return EnumSearchResult(
592
+ model=None, exhausted=False, checked=0, unsupported=None,
593
+ detail=(f"formula uses {len(atoms)} ground atoms, exceeding "
594
+ f"max_atoms={max_atoms}; search skipped rather than build "
595
+ "an oversized valuation space"),
596
+ )
597
+
598
+ conditions = {fam: _conditions_for(fam, frame, systems) for fam in families}
599
+ checked = 0
600
+ deadline = _instant(timeout)
601
+
602
+ def out_of_time() -> EnumSearchResult:
603
+ return EnumSearchResult(
604
+ model=None, exhausted=False, checked=checked, unsupported=None,
605
+ detail=(f"the {timeout} ms limit passed after {checked} models; the search "
606
+ f"up to max_worlds={max_worlds} did not complete"),
607
+ timed_out=True)
608
+
609
+ for n in range(1, max_worlds + 1):
610
+ rel_lists = []
611
+ for fam in families:
612
+ family_relations = _valid_relations_until(n, conditions[fam], deadline)
613
+ if family_relations is None:
614
+ return out_of_time()
615
+ rel_lists.append(family_relations)
616
+ # A valuation list that is small is built once and reused for every relation
617
+ # choice; a large one is generated again for each, instead of held in memory.
618
+ valuations = (list(_valuations(atoms, n, fixed))
619
+ if (1 << len(atoms)) ** n <= _CACHED_VALUATIONS else None)
620
+ rel_choices = itertools.product(*rel_lists) if rel_lists else [()]
621
+ for rel_choice in rel_choices:
622
+ relations = dict(zip(families, rel_choice))
623
+ for valuation in (valuations if valuations is not None
624
+ else _valuations(atoms, n, fixed)):
625
+ if _passed(deadline):
626
+ return out_of_time()
627
+ if checked >= max_models:
628
+ return EnumSearchResult(
629
+ model=None, exhausted=False, checked=checked, unsupported=None,
630
+ detail=(f"candidate budget exhausted after {checked} models "
631
+ f"(max_models={max_models}); the search up to "
632
+ f"max_worlds={max_worlds} did not complete"),
633
+ )
634
+ model = KripkeModel(worlds=range(n), relations=relations, valuation=valuation)
635
+ try:
636
+ holds = satisfies_modal(formula, model, 0)
637
+ except _UNSUPPORTED_EXC as exc:
638
+ return EnumSearchResult(
639
+ model=None, exhausted=False, checked=checked,
640
+ unsupported=f"{type(exc).__name__}: {exc}",
641
+ detail=("satisfies_modal has no rule for a construct in "
642
+ "this formula — it is outside the propositional/"
643
+ "ground modal fragment this enumerator supports"),
644
+ )
645
+ checked += 1
646
+ if not holds:
647
+ return EnumSearchResult(
648
+ model=model, exhausted=False, checked=checked, unsupported=None,
649
+ detail=(f"countermodel found with {n} world(s) after "
650
+ f"checking {checked} candidate(s)"),
651
+ )
652
+
653
+ return EnumSearchResult(
654
+ model=None, exhausted=True, checked=checked, unsupported=None,
655
+ detail=(f"search space fully enumerated up to max_worlds={max_worlds} "
656
+ f"({checked} candidates checked); no countermodel exists at or "
657
+ "below that size — NOT a validity proof, larger models are "
658
+ "unexplored"),
659
+ )
660
+
661
+
662
+ def modal_enum_countermodel(formula: Node, *, frame: str = "K",
663
+ systems: Optional[Dict[str, str]] = None,
664
+ max_worlds: int = 3, max_atoms: Optional[int] = None,
665
+ max_models: int = 200000,
666
+ timeout: Optional[float] = None) -> Optional[KripkeModel]:
667
+ """Return a verified Kripke countermodel for ``formula``, or ``None``.
668
+
669
+ Thin convenience wrapper over :func:`modal_enum_search` for callers who
670
+ only want the model (or its absence) and not the exhausted/unsupported/
671
+ checked detail — e.g. a differential test against
672
+ :func:`unicode_logic_kit.atp.modal_tableau.modal_countermodel`. ``None`` here
673
+ conflates "exhausted" and "budget hit" and "timed out" and "unsupported";
674
+ use :func:`modal_enum_search` directly to tell them apart (this is exactly
675
+ what :class:`KripkeEnumBackend` does for its ``reason``/``detail`` fields).
676
+ """
677
+ return modal_enum_search(formula, frame=frame, systems=systems,
678
+ max_worlds=max_worlds, max_atoms=max_atoms,
679
+ max_models=max_models, timeout=timeout).model
680
+
681
+
682
+ class KripkeEnumBackend(ProverBackend):
683
+ """Refutation-only semantic backend: bounded finite-Kripke-model enumeration.
684
+
685
+ Fills exactly the gap ``modal-tableau`` and ``qml`` leave open: neither can
686
+ REFUTE a temporal-closure formula (``modal-tableau`` has no rule for
687
+ Ⓖ/Ⓕ/Ⓤ/⒣/⒫/⒴/⒮ and reports it ``UNKNOWN/unsupported``; ``qml`` is sound but
688
+ proof-only). This backend decides such a formula by exhaustive finite-model
689
+ search (:func:`modal_enum_search`) using the SAME evaluator
690
+ (``satisfies_modal``) both of those routes ultimately answer to, so a
691
+ REFUTED verdict here can never contradict a PROVED verdict from either.
692
+
693
+ Like :class:`unicode_logic_kit.atp.protocol.ModelFinderBackend`, this is
694
+ ONE-SIDED: finding a countermodel REFUTES; exhausting the search space
695
+ proves NOTHING about validity (modal logic has no small-model property
696
+ bounding countermodels to ``max_worlds``), so that case is reported
697
+ ``UNKNOWN`` with ``reason="bound_hit"`` — the same reason
698
+ ``ModelFinderBackend`` uses for "no countermodel up to the size bound",
699
+ since ``max_worlds`` is exactly such a size bound (see
700
+ ``atp.protocol``'s own reason-vocabulary docstring, which lists
701
+ ``max_worlds`` as a ``bound_hit`` example). The ``detail`` field still says
702
+ whether the bound was hit by a COMPLETE search (``exhausted``) or by the
703
+ ``max_models``/``max_atoms`` budget cutting a search short — see
704
+ :class:`EnumSearchResult`. The call's ``timeout`` is one more bound: a search
705
+ it ended is ``UNKNOWN`` with ``reason="timeout"``.
706
+
707
+ ``premises`` are folded into the LOCAL consequence goal
708
+ ``(∧ premises) → formula``, exactly like ``ModalTableauBackend`` and
709
+ ``QmlBackend``.
710
+ """
711
+
712
+ name = "kripke-enum"
713
+ logics = frozenset({"modal"})
714
+ external = False
715
+
716
+ def available(self) -> bool:
717
+ return True
718
+
719
+ def decide(self, formula: Node, premises: Sequence[Node] = (),
720
+ timeout: int = 10000, **options) -> Verdict:
721
+ import time
722
+ from .protocol import _implication
723
+
724
+ goal = _implication(formula, premises)
725
+ start = time.perf_counter()
726
+ result = modal_enum_search(goal, timeout=timeout, **options)
727
+ elapsed = time.perf_counter() - start
728
+
729
+ if result.unsupported is not None:
730
+ return Verdict(UNKNOWN, self.name, logic="modal", reason="unsupported",
731
+ wall_time=elapsed,
732
+ detail=f"{result.detail} ({result.unsupported})")
733
+ if result.model is not None:
734
+ return Verdict(REFUTED, self.name, logic="modal", wall_time=elapsed,
735
+ countermodel={"kind": "kripke", "repr": repr(result.model),
736
+ "data": kripke_model_to_dict(result.model)})
737
+ if result.timed_out:
738
+ return Verdict(UNKNOWN, self.name, logic="modal", reason="timeout",
739
+ wall_time=elapsed, detail=result.detail)
740
+ return Verdict(UNKNOWN, self.name, logic="modal", reason="bound_hit",
741
+ wall_time=elapsed, detail=result.detail)