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,580 @@
1
+ """Counterfactual conditionals — Lewis / Stalnaker sphere semantics.
2
+
3
+ The material conditional gets counterfactuals wrong: "if kangaroos had no tails they
4
+ would topple over" is not made true by kangaroos having tails. **Counterfactuals**
5
+ ``A □→ B`` ("if A were the case, B would be") are evaluated over a *similarity*
6
+ ordering of worlds: from the actual world ``w`` you look at the **closest** worlds
7
+ where ``A`` holds and check that ``B`` holds throughout them.
8
+
9
+ This module uses Lewis's **system of spheres**: each world ``w`` carries a nested
10
+ sequence of sets of worlds ``$_w$`` (innermost first, ``w`` in the innermost), read as
11
+ "increasingly distant neighbourhoods". The truth condition (finite version)::
12
+
13
+ w ⊨ A □→ B iff no sphere of w contains an A-world (vacuously true),
14
+ or, for the smallest sphere S that does, every A-world in S is a B-world.
15
+
16
+ The "might" counterfactual is the dual ``A ◇→ B ≡ ¬(A □→ ¬B)``. Antecedents and
17
+ consequents are ordinary propositional formulas (atoms, ¬ ∧ ∨ → ↔). With a single
18
+ innermost sphere ``{closest A-world}`` this is Stalnaker's semantics; with ties it is
19
+ Lewis's.
20
+
21
+ ``□→`` and ``◇→`` parse in modal mode (``MSFLParser(modal=True)``) as the
22
+ :class:`~unicode_logic_kit.fol.nodes.Would` / :class:`~unicode_logic_kit.fol.nodes.Might`
23
+ nodes, so a whole formula can be handed to :func:`cf_satisfies` directly; the
24
+ sphere ordering is not an accessibility relation, so these nodes have no
25
+ first-order export and the Kripke evaluator rejects them.
26
+
27
+ CENTERING — which sphere systems count
28
+ --------------------------------------
29
+ "Nested" alone does not pin down a logic. Lewis names three systems, and
30
+ :data:`CENTERING_LEVELS` is exactly that list:
31
+
32
+ ``"none"`` (Lewis's **V**)
33
+ Nesting and nothing else. A world's spheres need not contain the world, and a
34
+ world may have no sphere at all.
35
+ ``"weak"`` (Lewis's **VW**) — the DEFAULT
36
+ Some sphere of ``w`` contains ``w``, and every non-empty sphere of ``w``
37
+ contains ``w``.
38
+ ``"strong"`` (Lewis's **VC**)
39
+ ``{w}`` is itself a sphere of ``w`` — with nesting, the innermost non-empty one.
40
+
41
+ ``"weak"`` is the default because it is what makes ``□→`` behave like a
42
+ *conditional*. Under ``"none"`` a sphere system that does not contain the
43
+ evaluation world makes ``A □→ B`` vacuously true even where ``A`` actually holds,
44
+ so **modus ponens fails**: ``(P ∧ (P □→ Q)) → Q`` and ``(P □→ Q) → (P → Q)`` are
45
+ both refutable in V, by the one-world model whose sphere system is empty. V is a
46
+ real logic and is kept reachable (``centering="none"``), but it is not what a
47
+ reader who writes ``P ∧ (P □→ Q)`` means by "counterfactual".
48
+
49
+ The three classes are strictly nested — ``strong ⊆ weak ⊆ none`` as sets of
50
+ models — so validity propagates ``none ⇒ weak ⇒ strong`` and refutation
51
+ propagates the other way. Strong centering ``(P ∧ Q) → (P □→ Q)`` separates VC
52
+ from VW; modus ponens and weak centering separate VW from V; antecedent
53
+ strengthening, contraposition and transitivity stay invalid in all three, which
54
+ is the whole point of the connective.
55
+
56
+ Centering is a property of a *class of models*, not of a single model: it is an
57
+ argument of :func:`cf_valid` / :func:`cf_countermodel` (which quantify over a
58
+ class), never of :func:`cf_satisfies` / :func:`would` / :func:`might` (which
59
+ evaluate in one given model).
60
+
61
+ HOW MANY WORLDS — and why the default is not one number
62
+ -------------------------------------------------------
63
+ The search is bounded, so a ``True`` verdict is only ever "no countermodel with at
64
+ most ``max_worlds`` worlds". Two worlds is **systematically too few for the
65
+ centered levels**, and that is a structural fact about the classes rather than bad
66
+ luck with a few schemas:
67
+
68
+ * At ``"strong"`` (VC) the innermost sphere is pinned to ``{w}``, so refuting a
69
+ disjunction of two counterfactuals needs ``w`` to falsify the antecedent *plus*
70
+ two antecedent-worlds that disagree — three worlds minimum. Conditional excluded
71
+ middle ``(A □→ B) ∨ (A □→ ¬B)``, Stalnaker's principle and one of the schemas
72
+ VC is supposed to leave open, is reported VALID at ``|W| ≤ 2`` and refuted at
73
+ ``|W| ≤ 3``.
74
+ * At ``"weak"`` (VW) the plain schemas are already settled at two worlds, but a
75
+ NESTED counterfactual is not: importation
76
+ ``(A □→ (B □→ C)) → ((A ∧ B) □→ C)`` is reported VALID at ``|W| ≤ 2`` and
77
+ refuted at ``|W| ≤ 3``.
78
+
79
+ :data:`DEFAULT_MAX_WORLDS` therefore sets the default per level — 3 for ``"weak"``
80
+ and ``"strong"``, 2 for ``"none"``, whose enumeration is an order of magnitude
81
+ larger per world (see the chain-count table in :func:`_sphere_chains`; measured, a
82
+ three-atom schema is seconds at VW and minutes at V). One consequence is worth
83
+ stating rather than discovering: because the levels are searched to different
84
+ depths by default, the class inclusion ``strong ⊆ weak ⊆ none`` need not show up
85
+ in the *default* verdicts — a schema first refuted at three worlds can read
86
+ "invalid at weak, valid at none". Pass the same explicit ``max_worlds`` to every
87
+ call whenever you are comparing levels.
88
+
89
+ Public API: :class:`CounterfactualModel`, :func:`cf_satisfies`, :func:`would`,
90
+ :func:`might`, :func:`cf_countermodel`, :func:`cf_valid`, :data:`CENTERING_LEVELS`,
91
+ :data:`DEFAULT_MAX_WORLDS`.
92
+ """
93
+
94
+ from dataclasses import dataclass
95
+ from itertools import product
96
+ from typing import Any, Dict, FrozenSet, Iterable, Iterator, List, Optional, Tuple
97
+
98
+ from ..fol.nodes import Node, Atom, Not, And, Or, Xor, Implies, Iff, Would, Might
99
+ from ..fol._atom_keys import AtomKeys, find_key, refuse_sorted_constant
100
+ from ..fol._truth_constants import truth_value as _truth_value
101
+ from ._modal_reject import reject_equality, reject_equality_in
102
+
103
+
104
+ #: The three Lewis sphere systems :func:`cf_valid` / :func:`cf_countermodel` can
105
+ #: quantify over — ``"none"`` = V, ``"weak"`` = VW, ``"strong"`` = VC. See the
106
+ #: module docstring for what each one is and why ``"weak"`` is the default.
107
+ CENTERING_LEVELS: Tuple[str, ...] = ("none", "weak", "strong")
108
+
109
+ #: The ``max_worlds`` bound :func:`cf_valid` / :func:`cf_countermodel` use when the
110
+ #: caller does not give one, keyed by centering level. Three for the centered levels
111
+ #: because two is *structurally* too few there (module docstring: conditional excluded
112
+ #: middle at VC, importation at VW, both spuriously VALID at ``|W| ≤ 2``); two for
113
+ #: ``"none"``, whose per-world chain count is 26 against VW's 11 and VC's 6, so the same
114
+ #: schema costs minutes instead of seconds. Deciding a level at another bound is one
115
+ #: positional argument away — and is what to do when comparing levels against each other.
116
+ DEFAULT_MAX_WORLDS: Dict[str, int] = {"none": 2, "weak": 3, "strong": 3}
117
+
118
+
119
+ def check_centering(level: str) -> str:
120
+ """Validate a centering level name and return it unchanged.
121
+
122
+ The single source of truth for the level names:
123
+ :mod:`unicode_logic_kit.hol.isabelle_conditional` imports this rather than
124
+ keeping its own tuple, so the Python enumeration and the Isabelle premise
125
+ cannot drift on what a level is called (or on which levels exist).
126
+
127
+ Raises:
128
+ ValueError: naming all three legal levels, on anything else.
129
+ """
130
+ if level not in CENTERING_LEVELS:
131
+ raise ValueError(
132
+ f"centering must be one of {CENTERING_LEVELS} "
133
+ f"(Lewis V / VW / VC), got {level!r}.")
134
+ return level
135
+
136
+
137
+ #: The sphere semantics reads an atom as a PROPOSITION keyed by its rendered
138
+ #: form and interprets no term, so it can give identity no meaning. Until
139
+ #: 0.30.0 it read ``a = b`` as such a key, which made ``a = a`` come back not
140
+ #: valid (measured) while ``a = b ∨ ¬(a = b)`` came back valid without the
141
+ #: atom having been read as identity at all — a verdict about an unconstrained
142
+ #: letter. The refusal is the shared one, so the evaluator and the exporters
143
+ #: (:mod:`unicode_logic_kit.hol.isabelle_conditional`) refuse the same input with
144
+ #: the same words.
145
+ _EQUALITY_ROUTE = "the propositional sphere (counterfactual) semantics"
146
+ _EQUALITY_ATOM_READING = ("an atom is a proposition keyed by its rendered "
147
+ "form in a world's valuation, and no term is "
148
+ "interpreted")
149
+ _EQUALITY_INSTEAD = (
150
+ "Counterfactuals over identity are outside this semantics; decide identity "
151
+ "with unicode_logic_kit.fol.qml.qml_is_valid (rigid identity over the object "
152
+ "domain) on the modal fragment instead."
153
+ )
154
+
155
+
156
+ def _reject_equality_atom(node: Node, caller: str) -> None:
157
+ """:func:`._modal_reject.reject_equality` with this route's wording."""
158
+ reject_equality(node, caller, _EQUALITY_ROUTE,
159
+ atom_reading=_EQUALITY_ATOM_READING,
160
+ instead=_EQUALITY_INSTEAD)
161
+
162
+
163
+ def _reject_equality_everywhere(formula: Node, caller: str) -> None:
164
+ """The whole-tree scan the search entry points run before searching.
165
+
166
+ Not left to :func:`cf_satisfies` reaching the atom: the search short-circuits
167
+ (``a = b ∨ ¬(a = b)`` is decided by the valuation of one key, whichever way it
168
+ goes), and a formula whose countermodel is found before the identity atom is
169
+ ever evaluated would come back with a verdict that never looked at it.
170
+ """
171
+ reject_equality_in(formula, caller, _EQUALITY_ROUTE,
172
+ atom_reading=_EQUALITY_ATOM_READING,
173
+ instead=_EQUALITY_INSTEAD)
174
+
175
+
176
+ def _reject_free_variable_atom(atom: Atom) -> None:
177
+ """Reject an atom with a free Variable argument: the sphere semantics is
178
+ propositional/ground. Keying such an atom by its surface string would treat
179
+ ``P(x)`` as one opaque proposition and silently return a verdict for an
180
+ out-of-contract formula — the sibling ``to_isabelle_conditional`` already
181
+ rejects the identical input, and the two must agree."""
182
+ from ..fol.nodes import Variable
183
+ for arg in atom.args:
184
+ for sub in arg.walk():
185
+ if isinstance(sub, Variable):
186
+ raise TypeError(
187
+ f"conditional: atom {atom.to_unicode_str()!r} has a free "
188
+ "variable — the Lewis sphere semantics here is "
189
+ "propositional/ground. Use ground atoms (constants are "
190
+ "fine), matching hol.isabelle_conditional.")
191
+
192
+
193
+ @dataclass(frozen=True)
194
+ class CounterfactualModel:
195
+ """A Lewis sphere model over a set of worlds.
196
+
197
+ ``valuation`` maps each world to the set of atom keys (the text of the atom with every
198
+ constant written by its name, ``'P(a)'``; the text of the atom as a formula, ``"P('a')"``,
199
+ is read as the same key) true there. ``spheres`` maps each world ``w``
200
+ to its nested system of spheres — a
201
+ list of frozensets ordered **innermost (closest) first**, each a superset of the
202
+ previous. A world omitted from ``spheres`` is taken to have the single sphere
203
+ ``{w}``.
204
+
205
+ NO CENTERING IS IMPOSED HERE. The dataclass deliberately does not require ``w``
206
+ to belong to any of its own spheres, and accepts the empty system ``[]``,
207
+ because it has to be able to represent all three Lewis classes (see
208
+ :data:`CENTERING_LEVELS`) — a centered-by-construction dataclass would make
209
+ ``centering="none"`` models unconstructible and so unreachable for
210
+ :func:`cf_countermodel`. Centering is therefore a property of the model *class*
211
+ that :func:`cf_valid` / :func:`cf_countermodel` quantify over, selected by their
212
+ ``centering=`` argument.
213
+
214
+ The trap that follows is worth naming: a hand-built model may perfectly well
215
+ falsify ``(P ∧ (P □→ Q)) → Q`` under :func:`cf_satisfies` while
216
+ ``cf_valid`` reports that formula valid. Those are not in conflict — one is
217
+ model-relative, the other class-relative, and the hand-built model simply is
218
+ not weakly centered. :func:`_centering_ok` is the predicate that decides which
219
+ class a given sphere system belongs to.
220
+
221
+ Note that :meth:`sphere_system`'s fallback ``[{world}]`` for an omitted world is
222
+ a **strongly centered** default (it satisfies VC, hence VW too), which is what
223
+ makes a partially specified ``spheres`` dict behave the way a reader expects.
224
+ """
225
+
226
+ worlds: Tuple[Any, ...]
227
+ valuation: Dict[Any, FrozenSet[str]]
228
+ spheres: Dict[Any, List[FrozenSet[Any]]]
229
+
230
+ def sphere_system(self, world: Any) -> List[FrozenSet[Any]]:
231
+ """The nested spheres around ``world`` (default ``[{world}]``)."""
232
+ return self.spheres.get(world, [frozenset({world})])
233
+
234
+
235
+ def cf_satisfies(formula: Node, model: CounterfactualModel, world: Any) -> bool:
236
+ """Return whether ``world`` satisfies ``formula`` in the sphere ``model``.
237
+
238
+ Handles the propositional connectives plus the counterfactual conditionals
239
+ :class:`~unicode_logic_kit.fol.nodes.Would` (``□→``) and
240
+ :class:`~unicode_logic_kit.fol.nodes.Might` (``◇→``), so a parsed formula can be
241
+ evaluated directly and counterfactuals may NEST — a conditional in an antecedent
242
+ or consequent is evaluated at the world under consideration, with that world's own
243
+ spheres, which is what Lewis's semantics prescribes.
244
+
245
+ Raises:
246
+ TypeError: on any other node (quantifiers, modal operators, …), and on an
247
+ atom with a FREE VARIABLE argument (``P(x)``) — the sphere semantics is
248
+ propositional/ground here; a Kripke accessibility relation is a
249
+ different structure, so mixing ``□`` with ``□→`` is rejected rather than
250
+ silently reinterpreted, and a first-order atom is rejected rather than
251
+ silently read as one opaque proposition (matching
252
+ ``hol.isabelle_conditional``).
253
+ """
254
+ if isinstance(formula, Atom):
255
+ constant = _truth_value(formula)
256
+ if constant is not None:
257
+ return constant # `$true` / `$false`: the same at every world
258
+ _reject_equality_atom(formula, "cf_satisfies")
259
+ _reject_free_variable_atom(formula)
260
+ refuse_sorted_constant(formula, "cf_satisfies")
261
+ # The key first, then the text of the atom as a formula, so a valuation keyed either
262
+ # way is read.
263
+ return find_key(model.valuation.get(world, frozenset()), formula) is not None
264
+ if isinstance(formula, Not):
265
+ return not cf_satisfies(formula.formula, model, world)
266
+ if isinstance(formula, And):
267
+ return (cf_satisfies(formula.left, model, world)
268
+ and cf_satisfies(formula.right, model, world))
269
+ if isinstance(formula, Or):
270
+ return (cf_satisfies(formula.left, model, world)
271
+ or cf_satisfies(formula.right, model, world))
272
+ if isinstance(formula, Xor):
273
+ return (cf_satisfies(formula.left, model, world)
274
+ != cf_satisfies(formula.right, model, world))
275
+ if isinstance(formula, Implies):
276
+ return ((not cf_satisfies(formula.left, model, world))
277
+ or cf_satisfies(formula.right, model, world))
278
+ if isinstance(formula, Iff):
279
+ return (cf_satisfies(formula.left, model, world)
280
+ == cf_satisfies(formula.right, model, world))
281
+ if isinstance(formula, Would):
282
+ return _would_holds(model, world, formula.left, formula.right)
283
+ if isinstance(formula, Might):
284
+ # The dual ¬(A □→ ¬B). Note this makes ◇→ vacuously FALSE exactly where
285
+ # □→ is vacuously true (no antecedent-world in any sphere).
286
+ return not _would_holds(model, world, formula.left, Not(formula.right))
287
+ raise TypeError(
288
+ f"conditional: formula must be propositional or a counterfactual, got "
289
+ f"{type(formula).__name__}.")
290
+
291
+
292
+ def _would_holds(model: CounterfactualModel, world: Any,
293
+ antecedent: Node, consequent: Node) -> bool:
294
+ """The Lewis sphere condition for ``antecedent □→ consequent`` at ``world``."""
295
+ for sphere in model.sphere_system(world):
296
+ a_worlds = [w for w in sphere if cf_satisfies(antecedent, model, w)]
297
+ if a_worlds:
298
+ return all(cf_satisfies(consequent, model, w) for w in a_worlds)
299
+ return True # no antecedent-world anywhere → vacuous
300
+
301
+
302
+ def would(model: CounterfactualModel, world: Any,
303
+ antecedent: Node, consequent: Node) -> bool:
304
+ """Return whether ``world ⊨ antecedent □→ consequent`` (Lewis "would" counterfactual).
305
+
306
+ Vacuously true if no sphere of ``world`` holds an antecedent-world; otherwise the
307
+ consequent must hold at every antecedent-world of the smallest antecedent-permitting
308
+ sphere. Equivalent to ``cf_satisfies(Would(antecedent, consequent), model, world)``.
309
+
310
+ The vacuous case can arise **even when the antecedent holds at ``world``
311
+ itself**, if ``model`` is not (weakly) centered — ``world`` may belong to none
312
+ of its own spheres, or have no sphere at all. That is not a bug in the clause
313
+ but the defining behaviour of Lewis's bare system V, and it is exactly what a
314
+ ``centering="none"`` countermodel from :func:`cf_countermodel` exhibits; the
315
+ ``centering="weak"`` default of :func:`cf_valid` excludes it.
316
+ """
317
+ return _would_holds(model, world, antecedent, consequent)
318
+
319
+
320
+ def might(model: CounterfactualModel, world: Any,
321
+ antecedent: Node, consequent: Node) -> bool:
322
+ """Return whether ``world ⊨ antecedent ◇→ consequent`` — the dual ``¬(A □→ ¬B)``."""
323
+ return not _would_holds(model, world, antecedent, Not(consequent))
324
+
325
+
326
+ # --------------------------------------------------------------------------- #
327
+ # Bounded countermodel search / validity over Lewis sphere models.
328
+ # --------------------------------------------------------------------------- #
329
+
330
+ def _atom_keys(formula: Node) -> Tuple[str, ...]:
331
+ """The distinct atom keys (the text of each atom with every constant written by its
332
+ name) of ``formula``, sorted.
333
+
334
+ Rejects identity atoms and free-variable atoms upfront (same contract as
335
+ :func:`cf_satisfies`), so ``cf_countermodel`` / ``cf_valid`` fail fast instead
336
+ of mid-enumeration — and, for identity, instead of returning a verdict the
337
+ search reached without reading the atom as identity at all.
338
+
339
+ A letter is named by the text its atom prints as, so two different atoms that print
340
+ alike (the numeral ``1`` and a constant named ``1``) would be one letter and the
341
+ formula another problem: such a pair is refused by name (``NotImplementedError``),
342
+ and so is a sorted constant, whose sort is a fact the sphere models have no
343
+ statement of.
344
+ """
345
+ atom_keys = AtomKeys("cf_countermodel", "refuse")
346
+ keys = set()
347
+ for n in formula.walk():
348
+ if isinstance(n, Atom):
349
+ if _truth_value(n) is not None:
350
+ continue # constants are not varied
351
+ _reject_equality_atom(n, "cf_countermodel")
352
+ _reject_free_variable_atom(n)
353
+ keys.add(atom_keys.key(n))
354
+ return tuple(sorted(keys))
355
+
356
+
357
+ def _centering_ok(spheres: Iterable[FrozenSet[Any]], world: Any,
358
+ centering: str) -> bool:
359
+ """Whether ``spheres`` is an admissible sphere system for ``world`` at ``centering``.
360
+
361
+ Written independently of :func:`_sphere_chains`, so the generator can be
362
+ cross-checked against it (see ``tests/test_counterfactual_centering.py``) in both
363
+ directions instead of being trusted twice over.
364
+
365
+ It denotes the same class as the Isabelle predicates ``weakly_centered`` /
366
+ ``strongly_centered``, but it is not a literal transcription of them, and the
367
+ difference is worth naming for anyone auditing the two side by side: HOL's
368
+ ``strongly_centered`` asserts only that ``{x}`` is a sphere, whereas ``"strong"``
369
+ below re-imposes the two weak clauses on top. On a NESTED family the two agree
370
+ (``{world}`` being a member forces every non-empty member to contain ``world``),
371
+ and ``isabelle_conditional`` always emits ``nested Sel`` as a co-premise, so the
372
+ denoted classes are identical; off nested families they differ — smallest witness
373
+ ``{{0}, {1}}`` at ``world=0``, which HOL's predicate accepts and this one rejects.
374
+
375
+ * ``"none"``: no condition — nesting is the caller's business.
376
+ * ``"weak"``: some sphere contains ``world``, AND every *non-empty* sphere
377
+ contains ``world``. The non-emptiness guard is not slack: Lewis's ``$_w`` is
378
+ closed under unions and so contains ``∅``, and ``∅`` is truth-value inert
379
+ anyway (see :func:`_sphere_chains`).
380
+ * ``"strong"``: weakly centered and ``{world}`` is itself a sphere. Under
381
+ nesting that makes it the ⊆-least non-empty sphere, which is the usual
382
+ phrasing of VC.
383
+ """
384
+ check_centering(centering)
385
+ if centering == "none":
386
+ return True
387
+ if not any(world in s for s in spheres):
388
+ return False
389
+ if any(s and world not in s for s in spheres):
390
+ return False
391
+ if centering == "strong":
392
+ return any(set(s) == {world} for s in spheres)
393
+ return True
394
+
395
+
396
+ def _sphere_chains(world: int, worlds: Tuple[int, ...],
397
+ centering: str = "weak") -> Iterator[List[FrozenSet[int]]]:
398
+ """Yield every admissible nested sphere system around ``world`` over ``worlds``.
399
+
400
+ A system is a strictly increasing chain ``S₁ ⊂ S₂ ⊂ … ⊂ Sₖ`` of frozensets,
401
+ innermost first. What ``centering`` selects (see :data:`CENTERING_LEVELS`):
402
+
403
+ * ``"none"`` (V): members are arbitrary non-empty subsets — they need not
404
+ contain ``world`` — and the EMPTY chain ``[]`` is emitted too (no sphere at
405
+ all, so every ``□→`` is vacuously true at ``world``). That chain is the sole
406
+ difference between V and VW at one world, and it is the one that refutes
407
+ modus ponens.
408
+ * ``"weak"`` (VW, default): every member contains ``world``, and the chain is
409
+ non-empty. Both weak-centering clauses follow immediately.
410
+ * ``"strong"`` (VC): as ``"weak"``, plus the innermost member is pinned to
411
+ ``{world}``.
412
+
413
+ ``∅`` is never a *member* of an emitted chain at any level. That loses no
414
+ generality, because ``∅`` is truth-value inert: :func:`_would_holds` can never
415
+ select it (it holds no antecedent-world) and it satisfies the vacuous clause
416
+ outright, so adding or deleting it changes no ``□→`` anywhere. This is the same
417
+ "strictness loses no generality" argument that justifies excluding repeated
418
+ spheres, and it is what lets the Isabelle premises in
419
+ :mod:`unicode_logic_kit.hol.isabelle_conditional` — which are ``∅``-tolerant, as
420
+ Lewis is — denote exactly the class enumerated here.
421
+
422
+ Chains per world, measured — this is the enumeration cost (``c(n)ⁿ`` chain
423
+ assignments per valuation), and it is the whole reason
424
+ :data:`DEFAULT_MAX_WORLDS` is 3 at the centered levels and 2 at ``"none"``::
425
+
426
+ |W|: 1 2 3
427
+ none 2 6 26
428
+ weak 1 3 11
429
+ strong 1 2 6
430
+ """
431
+ check_centering(centering)
432
+ others = [w for w in worlds if w != world]
433
+ pool: List[FrozenSet[int]] = []
434
+ for k in range(len(others) + 1):
435
+ for bits in product((False, True), repeat=len(others)):
436
+ if sum(bits) != k:
437
+ continue
438
+ rest = {o for o, b in zip(others, bits) if b}
439
+ if centering == "none":
440
+ # V does not require `world` in its own spheres, so the pool is
441
+ # every non-empty subset of `worlds`, with and without `world`.
442
+ if rest:
443
+ pool.append(frozenset(rest))
444
+ pool.append(frozenset({world} | rest))
445
+ else:
446
+ pool.append(frozenset({world} | rest))
447
+ pool.sort(key=len)
448
+ # pool is now ordered by size, so chains can be built left-to-right; for the
449
+ # centered levels pool[0] is the unique smallest member, {world}.
450
+
451
+ def extend(chain: List[FrozenSet[int]], start: int):
452
+ if chain: # [] is emitted only for "none"
453
+ yield list(chain)
454
+ for i in range(start, len(pool)):
455
+ if not chain or chain[-1] < pool[i]: # strict superset
456
+ yield from extend(chain + [pool[i]], i + 1)
457
+
458
+ if centering == "strong":
459
+ yield from extend([frozenset({world})], 1)
460
+ else:
461
+ if centering == "none":
462
+ yield [] # only V admits the empty system
463
+ yield from extend([], 0)
464
+
465
+
466
+ def cf_countermodel(formula: Node, max_worlds: Optional[int] = None, *,
467
+ centering: str = "weak"
468
+ ) -> Optional[Tuple[CounterfactualModel, Any]]:
469
+ """Return ``(model, world)`` where ``formula`` fails, or None.
470
+
471
+ EXHAUSTIVE search over every Lewis sphere model with ``|W| ≤ max_worlds`` that
472
+ satisfies ``centering``: every valuation of the formula's atoms and every
473
+ admissible nested sphere system around every world. The failure check *is*
474
+ :func:`cf_satisfies`, so a non-None result definitively refutes validity **over
475
+ that class** — the countermodel is verified by construction.
476
+
477
+ Args:
478
+ formula: the AST node to refute (propositional connectives plus ``□→``/``◇→``).
479
+ max_worlds: the world-count bound; every ``|W|`` from 1 up to it is tried.
480
+ ``None`` (the default) means :data:`DEFAULT_MAX_WORLDS` for the chosen
481
+ level — 3 at ``"weak"`` / ``"strong"``, 2 at ``"none"``.
482
+ centering: which class to search — ``"none"`` (V), ``"weak"`` (VW, the
483
+ default) or ``"strong"`` (VC). Keyword-only, so existing positional
484
+ ``cf_countermodel(f, 3)`` calls keep working. See
485
+ :data:`CENTERING_LEVELS`.
486
+
487
+ Raises:
488
+ ValueError: on an unknown ``centering`` level.
489
+ TypeError: on a free-variable atom (via :func:`_atom_keys`).
490
+
491
+ The space is exponential: ``2^(n·a)`` valuations times ``c(n)ⁿ`` sphere
492
+ assignments for ``n`` worlds and ``a`` atoms, where ``c(n)`` is the per-world
493
+ chain count tabulated in :func:`_sphere_chains`. The level matters a lot to the
494
+ cost, which is why the default bound is level-dependent: measured on the
495
+ toolkit's ten-schema Lewis battery at ``max_worlds=3``, ``"strong"`` takes 9 s
496
+ and ``"weak"`` 55 s in total (worst single schema 27 s, three atoms), whereas
497
+ ``"none"`` is ``26³`` chain assignments per valuation instead of ``11³`` and the
498
+ same three-atom schema runs for minutes. ``centering="none"`` together with
499
+ ``max_worlds ≥ 3`` is therefore a deliberate choice, not a default.
500
+ """
501
+ check_centering(centering)
502
+ if max_worlds is None:
503
+ max_worlds = DEFAULT_MAX_WORLDS[centering]
504
+ atoms = _atom_keys(formula)
505
+ for n in range(1, max_worlds + 1):
506
+ worlds = tuple(range(n))
507
+ chain_options = [list(_sphere_chains(w, worlds, centering)) for w in worlds]
508
+ for bits in product((False, True), repeat=n * max(1, len(atoms))):
509
+ valuation = {
510
+ w: frozenset(a for j, a in enumerate(atoms)
511
+ if bits[w * len(atoms) + j])
512
+ for w in worlds
513
+ } if atoms else {w: frozenset() for w in worlds}
514
+ for chains in product(*chain_options):
515
+ model = CounterfactualModel(
516
+ worlds, valuation,
517
+ {w: chains[w] for w in worlds})
518
+ for w in worlds:
519
+ if not cf_satisfies(formula, model, w):
520
+ return (model, w)
521
+ return None
522
+
523
+
524
+ def cf_valid(formula: Node, max_worlds: Optional[int] = None, *,
525
+ centering: str = "weak") -> bool:
526
+ """Return True iff no sphere countermodel to ``formula`` is found within the bound.
527
+
528
+ HONEST CONTRACT (mirroring :func:`~unicode_logic_kit.semantics.relevant.rel_valid`):
529
+ ``False`` is *definitive* — it is backed by an explicit,
530
+ :func:`cf_satisfies`-verified countermodel from :func:`cf_countermodel`, so
531
+ the formula is certainly not valid over Lewis sphere models. ``True`` means
532
+ only "no countermodel with at most ``max_worlds`` worlds"; a non-theorem
533
+ whose smallest refuting model needs more worlds is (spuriously) reported
534
+ valid — for a certified positive verdict use
535
+ :func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_counterfactual`,
536
+ whose proof battery is a real validity proof over ALL nested sphere systems
537
+ of the requested level. Raising ``max_worlds`` never turns a ``False`` into a
538
+ ``True``.
539
+
540
+ THE BOUND IS NOT ONE NUMBER, and the reason is structural rather than
541
+ performance tuning: at VC two worlds cannot host a countermodel to a
542
+ disjunction of two counterfactuals at all (``{w}`` is pinned as the innermost
543
+ sphere), and at VW two worlds cannot host one to a NESTED counterfactual. The
544
+ default is :data:`DEFAULT_MAX_WORLDS` for the level — 3 for ``"weak"`` and
545
+ ``"strong"``, 2 for ``"none"`` — so ``(A □→ B) ∨ (A □→ ¬B)`` at ``"strong"``
546
+ and ``(A □→ (B □→ C)) → ((A ∧ B) □→ C)`` at ``"weak"`` come back ``False``,
547
+ as they should, instead of the spurious ``True`` a two-world bound reports.
548
+ The flip side, stated so it is not discovered: default verdicts at DIFFERENT
549
+ levels are not directly comparable, since they are searched to different
550
+ depths. Pass an explicit ``max_worlds`` when comparing levels.
551
+
552
+ EVERY VERDICT IS RELATIVE TO ``centering``, and the relation between levels is
553
+ DIRECTIONAL. Because ``strong ⊆ weak ⊆ none`` as classes of models, a
554
+ countermodel found at ``"strong"`` is automatically a countermodel at ``"weak"``
555
+ and ``"none"``, and validity found at ``"none"`` automatically holds at
556
+ ``"weak"`` and ``"strong"``. Neither implication runs backwards:
557
+ ``cf_valid(φ, centering="none") is False`` does **not** refute ``φ`` over VW —
558
+ the witness may be a model VW excludes, which is precisely what happens to
559
+ modus ponens ``(P ∧ (P □→ Q)) → Q``. Compare like with like: the
560
+ certified-positive counterpart is
561
+ :func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_counterfactual`,
562
+ and it answers *at the level of the theory it emits* — which is
563
+ :func:`~unicode_logic_kit.hol.isabelle_conditional.isabelle_conditional_theory`'s
564
+ default, ``"weak"``, the same default as here.
565
+
566
+ Args:
567
+ formula: the AST node to decide.
568
+ max_worlds: the world-count bound handed to :func:`cf_countermodel`;
569
+ ``None`` (the default) means :data:`DEFAULT_MAX_WORLDS` for the level.
570
+ centering: ``"none"`` (V) / ``"weak"`` (VW, default) / ``"strong"`` (VC);
571
+ keyword-only, so ``cf_valid(f, 3)`` keeps working. The default matches
572
+ :func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_counterfactual`'s,
573
+ so the two routes decide the same question unless told otherwise — and
574
+ at the centered levels the default *bounds* now match too, three worlds
575
+ here against that route's ``card="1-3"``.
576
+
577
+ Raises:
578
+ ValueError: on an unknown ``centering`` level.
579
+ """
580
+ return cf_countermodel(formula, max_worlds, centering=centering) is None
@@ -0,0 +1,95 @@
1
+ """Public announcement logic (PAL) — dynamic epistemic model update.
2
+
3
+ Static epistemic logic (``Knows`` over a Kripke model) describes *what agents know*;
4
+ **public announcement logic** adds the dynamics: a truthful public announcement of
5
+ ``φ`` removes every world where ``φ`` is false, so the agents' knowledge changes. The
6
+ announcement operator ``[φ!]ψ`` ("after announcing φ, ψ holds") has the truth
7
+ condition::
8
+
9
+ M, w ⊨ [φ!] ψ iff M, w ⊨ φ implies M|φ, w ⊨ ψ
10
+
11
+ where ``M|φ`` is :func:`announce` — ``M`` restricted to its ``φ``-worlds (relations and
12
+ valuation cut down to the survivors). The dual ``⟨φ!⟩ψ`` ("φ is true and after
13
+ announcing it ψ holds") is ``φ ∧ M|φ,w ⊨ ψ``. Announcements compose, so iterated
14
+ updates and the Moore-sentence phenomenon (announcing ``p ∧ ¬K_a p`` makes it false)
15
+ fall straight out.
16
+
17
+ Built on :func:`unicode_logic_kit.semantics.kripke.satisfies_modal`; the announced and
18
+ post formulas are ordinary (propositional, epistemic, …) modal formulas.
19
+
20
+ ``[φ!]ψ`` / ``⟨φ!⟩ψ`` ALSO exist as first-class AST nodes —
21
+ :class:`~unicode_logic_kit.fol._modal_nodes.Announce` /
22
+ :class:`~unicode_logic_kit.fol._modal_nodes.AnnounceDiamond` (parsed by
23
+ ``MSFLParser(modal=True)`` from exactly this surface syntax). Two independent, and
24
+ independently useful, routes evaluate them:
25
+
26
+ * :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` interprets an
27
+ ``Announce``/``AnnounceDiamond`` node DIRECTLY, by calling :func:`announce` (this
28
+ module) internally to build ``M|φ`` — i.e. this module is satisfies_modal's own
29
+ implementation of the PAL case, not a separate parallel semantics.
30
+ * :func:`unicode_logic_kit.fol.pal.reduce_announcements` ELIMINATES an
31
+ ``Announce``/``AnnounceDiamond`` node SYNTACTICALLY, rewriting it to an
32
+ announcement-free modal formula via the standard PAL reduction axioms — the
33
+ route into every other reasoning tool in the kit (:mod:`unicode_logic_kit.atp.modal_tableau`,
34
+ the Isabelle/THF embeddings, …), none of which know about Announce/AnnounceDiamond
35
+ directly. ``satisfies_modal`` (hence this module's :func:`announce` /
36
+ :func:`box_announce` / :func:`diamond_announce`) is the ORACLE
37
+ ``fol.pal.reduce_announcements`` is differentially tested against — see that
38
+ module's docstring for the correctness argument connecting the two.
39
+
40
+ :func:`box_announce` / :func:`diamond_announce` remain useful on their own for
41
+ MODEL-level (rather than AST-level) PAL reasoning — e.g. stepping an existing
42
+ :class:`~unicode_logic_kit.semantics.kripke.KripkeModel` through a sequence of
43
+ announcements programmatically, with no ``Announce`` node ever constructed.
44
+
45
+ Public API: :func:`announce`, :func:`box_announce`, :func:`diamond_announce`.
46
+ """
47
+
48
+ from typing import Any
49
+
50
+ from ..fol.nodes import Node
51
+ from .kripke import KripkeModel, satisfies_modal
52
+
53
+
54
+ def announce(model: KripkeModel, formula: Node) -> KripkeModel:
55
+ """Return ``model`` updated by a truthful public announcement of ``formula``.
56
+
57
+ The result keeps exactly the worlds where ``formula`` is true (under
58
+ :func:`satisfies_modal`); every relation is restricted to those survivors and the
59
+ valuation (and per-world object domains, if any) likewise. Inputs are not mutated.
60
+ """
61
+ survivors = frozenset(w for w in model.worlds if satisfies_modal(formula, model, w))
62
+ relations = {
63
+ name: {(a, b) for (a, b) in edges if a in survivors and b in survivors}
64
+ for name, edges in model.relations.items()
65
+ }
66
+ valuation = {w: set(model.atoms_true_at(w)) for w in survivors}
67
+ domains = None
68
+ if model.domains is not None:
69
+ domains = {w: set(model.domains.get(w, frozenset())) for w in survivors}
70
+ return KripkeModel(survivors, relations, valuation, domains=domains)
71
+
72
+
73
+ def box_announce(model: KripkeModel, world: Any, announcement: Node, post: Node) -> bool:
74
+ """Return whether ``model, world ⊨ [announcement!] post`` (PAL box).
75
+
76
+ Vacuously true when the announcement is false at ``world`` (an untruthful
77
+ announcement is not made); otherwise ``post`` must hold at ``world`` in the
78
+ updated model :func:`announce`.
79
+ """
80
+ if not satisfies_modal(announcement, model, world):
81
+ return True
82
+ updated = announce(model, announcement)
83
+ return satisfies_modal(post, updated, world)
84
+
85
+
86
+ def diamond_announce(model: KripkeModel, world: Any, announcement: Node, post: Node) -> bool:
87
+ """Return whether ``model, world ⊨ ⟨announcement!⟩ post`` (PAL diamond).
88
+
89
+ True iff the announcement is truthful at ``world`` **and** ``post`` then holds at
90
+ ``world`` in the updated model — the dual of :func:`box_announce`.
91
+ """
92
+ if not satisfies_modal(announcement, model, world):
93
+ return False
94
+ updated = announce(model, announcement)
95
+ return satisfies_modal(post, updated, world)