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,156 @@
1
+ """Shared "out-of-scope node" rejection helpers for the modal v1 back-ends.
2
+
3
+ Both the Kripke evaluator (:mod:`kripke`) and the standard translation
4
+ (:mod:`..fol.modal_translation`) interpret only the **propositional / ground**
5
+ modal fragment in v1. First-order quantifiers, Łukasiewicz (fuzzy) operators,
6
+ and lambda-calculus nodes are out of scope and are rejected uniformly here with
7
+ a clear NotImplementedError, so the message wording stays consistent across the
8
+ two modules.
9
+
10
+ An **equality atom** (``a = b`` / ``a ≠ b``) is out of scope for every route
11
+ that has no TERM semantics, and :func:`reject_equality` is the one place that
12
+ says so: a propositional route reads an atom as a world-relative proposition
13
+ keyed by its rendered form, so ``=`` could only be an uninterpreted relation
14
+ there, and silently treating identity as one is the kind of approximation this
15
+ kit refuses. The propositional modal tableau (:mod:`..atp.modal_tableau`) and the
16
+ intuitionistic GMT embedding (:mod:`..hol.intuitionistic`) share it; the message
17
+ is parameterised by the route it speaks for.
18
+ """
19
+
20
+ from typing import NoReturn, TypeGuard
21
+
22
+ from ..fol.nodes import Atom, Node
23
+ from ..fol._msfl_nodes import (
24
+ LukNegation, WeakConjunction, WeakDisjunction,
25
+ StrongConjunction, StrongDisjunction,
26
+ LukImplication, LukEquivalence,
27
+ LambdaVar, Lambda, Application,
28
+ )
29
+
30
+ # Łukasiewicz (fuzzy) node types — no two-valued modal interpretation in v1.
31
+ FUZZY_TYPES = (
32
+ LukNegation, WeakConjunction, WeakDisjunction,
33
+ StrongConjunction, StrongDisjunction,
34
+ LukImplication, LukEquivalence,
35
+ )
36
+
37
+ # Lambda-calculus node types — must be eliminated before any modal back-end.
38
+ LAMBDA_TYPES = (LambdaVar, Lambda, Application)
39
+
40
+
41
+ def reject_quantifier(formula: Node, caller: str) -> NoReturn:
42
+ """Reject a (sorted) quantifier: v1 modal logic is propositional / ground."""
43
+ raise NotImplementedError(
44
+ f"{caller}: {type(formula).__name__} is not supported — v1 modal logic "
45
+ "is propositional / ground; first-order (quantified) modal logic is "
46
+ "future work."
47
+ )
48
+
49
+
50
+ def reject_fuzzy(formula: Node, caller: str) -> NoReturn:
51
+ """Reject a Łukasiewicz node: modal v1 is two-valued, not fuzzy."""
52
+ raise NotImplementedError(
53
+ f"{caller}: Łukasiewicz node {type(formula).__name__} is not supported — "
54
+ "modal v1 is two-valued; fuzzy modal logic is future work."
55
+ )
56
+
57
+
58
+ def reject_lambda(formula: Node, caller: str) -> NoReturn:
59
+ """Reject a lambda node: beta-reduce / lambda-eliminate before modal work."""
60
+ raise NotImplementedError(
61
+ f"{caller}: lambda node {type(formula).__name__} is not supported — "
62
+ "beta-reduce and lambda-eliminate the formula first."
63
+ )
64
+
65
+
66
+ #: The two spellings the kit gives identity atoms (``a = b`` parses to
67
+ #: ``Atom("=", (a, b))``, ``a ≠ b`` to ``Atom("≠", (a, b))``). Refused by NAME,
68
+ #: whatever the arity: a propositional route has no term semantics to give either
69
+ #: of them a meaning, so the arity of the atom does not matter to the refusal.
70
+ EQUALITY_PREDICATES = ("=", "≠")
71
+
72
+ #: How the Kripke evaluator reads an atom, and so why it cannot read identity.
73
+ _KRIPKE_ROUTE = "the propositional Kripke evaluator"
74
+ _KRIPKE_ATOM_READING = ("an atom is looked up by its rendered key in a world's "
75
+ "valuation")
76
+
77
+ #: What goes wrong if the route reads the atom anyway. The default is the
78
+ #: propositional routes': they key an atom by its rendered form, so identity
79
+ #: becomes an unconstrained letter. A route whose failure is a DIFFERENT one
80
+ #: passes its own ``consequence`` — identity at a property type, for instance,
81
+ #: is not an unconstrained letter but a question with several answers.
82
+ _KRIPKE_CONSEQUENCE = ("so it would read the identity as an uninterpreted "
83
+ "proposition and answer wrongly, e.g. 'a = a' false")
84
+
85
+ #: Where identity IS decided: the first-order modal embedding, which reads ``=`` as
86
+ #: RIGID identity over the object domain (``fol.qml``'s "Equality is rigid").
87
+ _QML_POINTER = ("Decide equality with unicode_logic_kit.fol.qml.qml_is_valid "
88
+ "(quantified modal logic, where '=' is rigid identity over the "
89
+ "object domain) or another first-order route, not here.")
90
+
91
+
92
+ def is_equality_atom(node: Node) -> TypeGuard[Atom]:
93
+ """True iff ``node`` is an equality / disequality atom (``=`` / ``≠``).
94
+
95
+ A ``TypeGuard`` rather than a plain ``bool`` so a caller that has checked
96
+ it may read ``.predicate`` without a second ``isinstance``: the one place
97
+ that decides what counts as identity stays this function.
98
+ """
99
+ return isinstance(node, Atom) and node.predicate in EQUALITY_PREDICATES
100
+
101
+
102
+ def reject_equality(node: Node, caller: str, route: str = _KRIPKE_ROUTE, *,
103
+ atom_reading: str = _KRIPKE_ATOM_READING,
104
+ consequence: str = _KRIPKE_CONSEQUENCE,
105
+ instead: str = _QML_POINTER) -> None:
106
+ """Refuse, by name, ``node`` if it is an equality / disequality atom.
107
+
108
+ Same shape as :func:`reject_fuzzy` / :func:`reject_lambda` (a
109
+ ``NotImplementedError`` that starts ``"<caller>: …"`` and says what to use
110
+ instead), but CHECK-AND-RAISE rather than raise-only: a caller applies it to
111
+ EVERY node of the formula and it returns ``None`` on anything that is not an
112
+ identity atom. See :func:`reject_equality_in` for the whole-tree form.
113
+
114
+ It is never left to evaluation reaching an ``Atom``. A lazy check would be
115
+ skipped wherever a route short-circuits or is vacuous — a dead end under a
116
+ ``□``, a branch that closes on an unrelated contradiction, an ``∨`` whose left
117
+ side already holds — and the verdict would then not have looked at the atom at
118
+ all. Scan the whole tree at the entry point instead.
119
+
120
+ ``route`` names the route the refusal speaks for (it completes "equality is not
121
+ interpreted by …"), ``atom_reading`` says in a clause how that route reads an
122
+ atom (why it cannot read identity), ``consequence`` says what goes wrong if it
123
+ reads the atom anyway, and ``instead`` is the sentence that points elsewhere.
124
+ The defaults are the Kripke evaluator's, so a caller that passes only ``node``
125
+ and ``caller`` gets exactly its message. ``consequence`` is separate from
126
+ ``atom_reading`` because the two are not the same claim: a propositional route
127
+ reads an atom by its rendered key AND therefore gets 'a = a' false, while a
128
+ route refusing identity at a PROPERTY type would not get that wrong — it has
129
+ no single right answer to give.
130
+ """
131
+ if not is_equality_atom(node):
132
+ return
133
+ kind = "equality" if node.predicate == "=" else "disequality"
134
+ raise NotImplementedError(
135
+ f"{caller}: the {kind} atom {node.to_unicode_str()!r} "
136
+ f"({node.predicate!r}) is refused by name — equality is not "
137
+ f"interpreted by {route}. It has no term "
138
+ f"semantics ({atom_reading}), {consequence}. "
139
+ f"{instead}"
140
+ )
141
+
142
+
143
+ def reject_equality_in(formula: Node, caller: str, route: str = _KRIPKE_ROUTE, *,
144
+ atom_reading: str = _KRIPKE_ATOM_READING,
145
+ consequence: str = _KRIPKE_CONSEQUENCE,
146
+ instead: str = _QML_POINTER) -> None:
147
+ """:func:`reject_equality` on every node of ``formula`` (whole-tree scan).
148
+
149
+ ``Node.walk`` is generic over the node's fields, so the scan reaches into
150
+ every operator's sub-formula — the body of a modality, both sides of a
151
+ connective, a quantifier's matrix, an announcement and its body, the group
152
+ and agent terms — without this module naming any of them.
153
+ """
154
+ for node in formula.walk():
155
+ reject_equality(node, caller, route, atom_reading=atom_reading,
156
+ consequence=consequence, instead=instead)
@@ -0,0 +1,466 @@
1
+ """Common knowledge and BMS action models (Dynamic Epistemic Logic).
2
+
3
+ Two independent extensions of the epistemic Kripke machinery in
4
+ :mod:`unicode_logic_kit.semantics.kripke` and
5
+ :mod:`unicode_logic_kit.semantics.dynamic_epistemic`:
6
+
7
+ 1. **Group epistemic operators** — :func:`everybody_knows` (E_G φ, "everyone
8
+ in G knows φ"), :func:`common_knowledge_holds` (C_G φ, "φ is common
9
+ knowledge in G"), and :func:`distributed_knowledge_holds` (D_G φ, "φ is
10
+ DISTRIBUTED knowledge in G" — pooling every agent's information via the
11
+ INTERSECTION, rather than the union, of their relations), evaluated
12
+ directly on an existing
13
+ :class:`~unicode_logic_kit.semantics.kripke.KripkeModel` at a world, using the
14
+ model's ``"K:" + agent`` relations (the SAME naming convention
15
+ :mod:`kripke` itself uses for ``Knows`` — see that module's docstring for
16
+ the contract). All three are also reachable as first-class AST nodes
17
+ (:class:`~unicode_logic_kit.fol._modal_nodes.EverybodyKnows` /
18
+ :class:`~unicode_logic_kit.fol._modal_nodes.CommonKnowledge` /
19
+ :class:`~unicode_logic_kit.fol._modal_nodes.DistributedKnowledge`, parsed
20
+ from ``E_{a,b,…}``/``C_{a,b,…}``/``D_{a,b,…}`` surface syntax), with
21
+ :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` dispatching
22
+ straight into the three functions here — the same "thin AST wrapper"
23
+ pattern :class:`~unicode_logic_kit.fol._modal_nodes.Announce` already uses
24
+ for :func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce`.
25
+
26
+ 2. **BMS action models** (Baltag, Moss & Solecki 1998, "The Logic of Public
27
+ Announcements, Common Knowledge, and Private Suspicions", TARK; see also
28
+ van Ditmarsch, van der Hoek & Kooi, *Dynamic Epistemic Logic*, Springer
29
+ 2007, ch. 6) — :class:`ActionModel` and :func:`product_update` generalise
30
+ public announcement (:func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce`)
31
+ to arbitrary epistemic EVENTS: private announcements, lies, and any other
32
+ information-changing event that can be described by a set of events, a
33
+ precondition per event, and per-agent indistinguishability between events.
34
+ :func:`public_announcement_action` recovers plain PAL as the special case of
35
+ a single event — the product update it induces agrees EXACTLY with
36
+ :func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce` (see the
37
+ module's own cross-check in ``tests/test_action_models.py``).
38
+
39
+ Both extensions build only on the PUBLIC interface of :mod:`kripke`
40
+ (``KripkeModel``, ``satisfies_modal``, ``model.relation``/``model.successors``,
41
+ ``reflexive_transitive_closure``) — nothing here is wired into ``kripke.py``,
42
+ ``dynamic_epistemic.py``, the parser, or ``semantics/__init__.py``; that
43
+ central wiring is a separate, later step. Import directly from this module:
44
+ ``from unicode_logic_kit.semantics.action_models import ...``.
45
+
46
+ Scope: purely EPISTEMIC dynamics. BMS action models in general carry
47
+ POSTCONDITIONS (factual, non-epistemic change — e.g. an event that flips a
48
+ coin or updates a database), but :class:`ActionModel` here has no
49
+ postcondition parameter at all: an event has only a precondition and
50
+ per-agent accessibility edges to other events. :func:`product_update` always
51
+ copies the valuation of ``w`` unchanged onto ``(w, e)``. There is therefore no
52
+ postcondition argument to refuse — the omission itself is the scope boundary,
53
+ documented here rather than enforced by a runtime check.
54
+ """
55
+
56
+ from typing import Dict, FrozenSet, Hashable, Iterable, Mapping, Optional, Set, Tuple
57
+
58
+ from ..fol.nodes import Node
59
+ from .kripke import KripkeModel, World, Edge, satisfies_modal, reflexive_transitive_closure
60
+
61
+ # Relation-key prefix for epistemic accessibility — MUST match the convention
62
+ # documented and used by unicode_logic_kit.semantics.kripke.KripkeModel /
63
+ # satisfies_modal (the private ``_KNOWS_PREFIX`` there). Kept as a literal
64
+ # here (rather than imported) because this module must not edit kripke.py,
65
+ # and the string itself — not the name it is bound to — is the actual
66
+ # contract between the two modules.
67
+ _KNOWS_PREFIX = "K:"
68
+
69
+ Event = Hashable
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Common knowledge
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def _group_relation(model: KripkeModel, group: Iterable[str]) -> Set[Edge]:
77
+ """Return the UNION of the ``"K:"+agent`` edge sets for every agent in ``group``.
78
+
79
+ This is the accessibility relation ``R_G = ⋃_{a∈G} R_a`` that both
80
+ :func:`everybody_knows` and :func:`common_knowledge_holds` are built from.
81
+ An agent absent from ``model.relations`` contributes the empty relation
82
+ (``KripkeModel``'s own "a missing name is the empty relation" convention).
83
+ """
84
+ union: Set[Edge] = set()
85
+ for agent in group:
86
+ union |= model.relation(_KNOWS_PREFIX + agent)
87
+ return union
88
+
89
+
90
+ def everybody_knows(model: KripkeModel, world: World, group: Iterable[str], formula: Node) -> bool:
91
+ """Return whether E_G φ ("everyone in ``group`` knows φ") holds at ``world``.
92
+
93
+ E_G φ is, by definition, the finite conjunction ⋀_{a∈G} K_a φ (Fagin,
94
+ Halpern, Moses & Vardi, *Reasoning About Knowledge*, MIT Press 1995, ch.
95
+ 2). Since ``Knows(a, φ)`` holds at ``world`` iff φ holds at every
96
+ ``"K:"+a``-successor of ``world`` (universal quantification distributes
97
+ over a union of successor sets), this is equivalent to — and computed
98
+ here as — "φ holds at every world reachable from ``world`` by a SINGLE
99
+ edge of the union relation ``⋃_{a∈G} R_a``" (no reflexive/transitive
100
+ closure: this is the ONE-STEP operator, the first rung of the common-
101
+ knowledge ladder — see :func:`common_knowledge_holds`).
102
+
103
+ ``group`` is any iterable of agent name strings (consumed once, so a
104
+ generator is safe here — it is materialised into the relation union
105
+ immediately). An empty group makes E_∅ φ vacuously TRUE at every world
106
+ (the union relation is empty, so "holds at every successor" holds
107
+ vacuously) — the same convention an empty conjunction always gets.
108
+
109
+ The full FHMV strength ordering this module's three group notions sit in,
110
+ strongest first: ``C_G φ → E_G φ → K_a φ → D_G φ`` (``a ∈ G``, any
111
+ agent) — :func:`common_knowledge_holds` is the strongest, E_G here sits
112
+ in the middle (``E_G φ → K_a φ``: everyone knowing φ entails any ONE of
113
+ them knowing it, since the union relation is a SUPERSET of each
114
+ individual ``R_a``, so a box over it is a stronger constraint), and
115
+ :func:`distributed_knowledge_holds` (D_G) is the weakest (see that
116
+ function's docstring for why). None of the converses is a theorem.
117
+ """
118
+ successors = {w2 for (w1, w2) in _group_relation(model, group) if w1 == world}
119
+ return all(satisfies_modal(formula, model, w2) for w2 in successors)
120
+
121
+
122
+ def _group_intersection(model: KripkeModel, group: Iterable[str]) -> Set[Edge]:
123
+ """Return the INTERSECTION of the ``"K:"+agent`` edge sets for every agent in ``group``.
124
+
125
+ This is the accessibility relation ``R_G = ⋂_{a∈G} R_a`` that
126
+ :func:`distributed_knowledge_holds` is built from — the mirror of
127
+ :func:`_group_relation`'s UNION, one line different (``&=`` instead of
128
+ ``|=``). ``group`` is materialised into a list first (not consumed
129
+ lazily): unlike a union, an intersection needs to know it has SEEN every
130
+ member before it can report a final answer, and the empty-group case
131
+ below needs to inspect it before iterating relations at all.
132
+ """
133
+ agents = list(group)
134
+ if not agents:
135
+ raise ValueError(
136
+ "_group_intersection: an empty group has no well-defined "
137
+ "intersection here — see distributed_knowledge_holds's docstring "
138
+ "for why this stays a refusal rather than the 'universal relation' "
139
+ "convention an empty intersection carries in set theory."
140
+ )
141
+ inter: Optional[Set[Edge]] = None
142
+ for agent in agents:
143
+ edges = model.relation(_KNOWS_PREFIX + agent)
144
+ inter = set(edges) if inter is None else (inter & edges)
145
+ return inter
146
+
147
+
148
+ def distributed_knowledge_holds(model: KripkeModel, world: World, group: Iterable[str], formula: Node) -> bool:
149
+ """Return whether D_G φ ("φ is DISTRIBUTED knowledge in ``group``") holds at ``world``.
150
+
151
+ Distributed knowledge pools every agent's individual information: D_G φ
152
+ holds at ``world`` iff φ holds at every world reachable by a SINGLE edge
153
+ of the INTERSECTION relation ``R_G = ⋂_{a∈G} R_a`` (Fagin, Halpern, Moses
154
+ & Vardi, *Reasoning About Knowledge*, MIT Press 1995, ch. 2) — the
155
+ one-step group operator built from intersection rather than
156
+ :func:`everybody_knows`'s union, mirroring :func:`_group_relation` /
157
+ :func:`_group_intersection`'s own shared structure.
158
+
159
+ D_G is the LOGICALLY WEAKEST of the three group notions (the one every
160
+ other one entails, never the reverse) — precisely BECAUSE pooling via
161
+ intersection can only SHRINK the reachable set relative to any single
162
+ agent's own relation (``R_G ⊆ R_a`` for every ``a ∈ G``, so a universal
163
+ claim over the smaller ``R_G`` is easier to satisfy than one over the
164
+ bigger ``R_a``): ``K_a φ → D_G φ`` for every ``a ∈ G``, and transitively
165
+ (via :func:`everybody_knows`'s ``E_G φ → K_a φ``) also ``E_G φ → D_G φ``
166
+ and ``C_G φ → D_G φ`` are theorems, while none of the converses is. This
167
+ is not a contradiction of "distributed knowledge pools richer
168
+ information than any individual" — D_G φ can hold even when NO single
169
+ K_a φ does (the group's COMBINED relations can rule out a possibility no
170
+ one agent's relation alone rules out), it is simply that, as a FORMAL
171
+ CONSTRAINT on φ, quantifying over the smaller intersection relation is
172
+ the weakest of the four readings, not the strongest.
173
+
174
+ ``group`` is any iterable of agent name strings, consumed once into the
175
+ intersection.
176
+
177
+ EMPTY-GROUP CONVENTION — deliberately NOT the same as
178
+ :func:`everybody_knows` / :func:`common_knowledge_holds`: an intersection
179
+ over an empty family of relations is, by the usual set-theoretic
180
+ convention, the UNIVERSAL relation (every pair of worlds), so a
181
+ vacuously-permissive reading of ``D_∅ φ`` would demand φ true at
182
+ LITERALLY EVERY world of the model — a degenerate, almost certainly
183
+ unintended answer for what "the distributed knowledge of no one" should
184
+ mean, unlike the union operators' empty-conjunction "vacuously true"
185
+ reading (which stays a sensible, bounded claim: nothing is asserted about
186
+ any OTHER world). Rather than silently return that near-universal
187
+ verdict, this function raises ``ValueError`` for an empty group — the
188
+ kit's refuse-loudly ethos applied to a case where "the obvious extension
189
+ of the convention" would be actively misleading. (:class:`DistributedKnowledge`
190
+ in :mod:`unicode_logic_kit.fol._modal_nodes` mirrors this at CONSTRUCTION
191
+ time already, so the parsed/AST route can never reach this function with
192
+ an empty group in the first place; this check remains here too for a
193
+ caller using the function directly, per :func:`_group_intersection`.)
194
+ """
195
+ successors = {w2 for (w1, w2) in _group_intersection(model, group) if w1 == world}
196
+ return all(satisfies_modal(formula, model, w2) for w2 in successors)
197
+
198
+
199
+ def common_knowledge_holds(model: KripkeModel, world: World, group: Iterable[str], formula: Node) -> bool:
200
+ """Return whether C_G φ ("φ is common knowledge in ``group``") holds at ``world``.
201
+
202
+ Common knowledge is standardly given TWO equivalent characterisations
203
+ (Fagin, Halpern, Moses & Vardi, *Reasoning About Knowledge*, MIT Press
204
+ 1995, §2.4; also van Ditmarsch, van der Hoek & Kooi, *Dynamic Epistemic
205
+ Logic*, Springer 2007, §2.3-2.4):
206
+
207
+ 1. **Fixpoint / ladder**: C_G φ is the (infinite) conjunction
208
+ ``E_G φ ∧ E_G E_G φ ∧ E_G E_G E_G φ ∧ …`` — equivalently, the greatest
209
+ fixpoint of ``X ↔ E_G(φ ∧ X)``. Everyone knows φ, everyone knows that
210
+ everyone knows φ, and so on without end.
211
+ 2. **Reachability**: C_G φ holds at ``world`` iff φ holds at EVERY world
212
+ reachable from ``world`` by a finite path of ONE OR MORE edges in the
213
+ union relation ``R_G = ⋃_{a∈G} R_a`` (FHMV's Theorem 2.4.4-style
214
+ reachability characterisation, which holds for arbitrary — not
215
+ necessarily equivalence — relations).
216
+
217
+ THIS FUNCTION IMPLEMENTS CHARACTERISATION 2, via
218
+ :func:`~unicode_logic_kit.semantics.kripke.reflexive_transitive_closure` of
219
+ ``R_G`` from ``world`` — REFLEXIVE, i.e. "zero or more edges" (``world``
220
+ itself is always included), not FHMV's literal "one or more edges" (which
221
+ would exclude ``world`` unless a path loops back to it).
222
+
223
+ Why reflexive is the right choice here, not a deviation from FHMV: every
224
+ individual epistemic relation ``"K:"+agent`` this kit ever builds for
225
+ ``Knows`` is used with the reflexivity axiom T in mind (``K_a φ → φ``,
226
+ tested directly in ``tests/test_kripke.py::test_reflexive_frame_gives_factivity_for_knows``)
227
+ — i.e. the intended models are S5 (or at least reflexive), exactly the
228
+ case FHMV themselves note collapses "one-or-more-steps" and "zero-or-more-
229
+ steps" reachability into the same set, because a reflexive edge at
230
+ ``world`` already puts ``world`` back into the "one-or-more-steps" set for
231
+ free. Taking the reflexive closure directly (rather than requiring the
232
+ caller's relations to already carry an explicit self-loop) makes
233
+ ``common_knowledge_holds`` agree with FHMV on every model the caller is
234
+ expected to build, and additionally makes ``C_G φ → φ`` a theorem here
235
+ even on a model some caller forgot to add reflexive loops to (a
236
+ permissive, not a silently-wrong, choice — the alternative, a strict
237
+ "one-or-more-steps" closure, would make C_G φ fail to entail φ on such a
238
+ model, which is not the standard reading anyone building an epistemic
239
+ model here would expect).
240
+
241
+ ``group`` is any iterable of agent name strings, consumed once into the
242
+ union relation. An empty group makes ``common_knowledge_holds`` reduce
243
+ to ``satisfies_modal(formula, model, world)`` (the reflexive closure of
244
+ the empty relation from ``world`` is just ``{world}``) — consistent with
245
+ :func:`everybody_knows`'s empty-group convention.
246
+ """
247
+ reachable = reflexive_transitive_closure(_group_relation(model, group), [world])
248
+ return all(satisfies_modal(formula, model, w2) for w2 in reachable)
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # BMS action models
253
+ # ---------------------------------------------------------------------------
254
+
255
+ class ActionModel:
256
+ """A BMS (Baltag-Moss-Solecki) action model: events, preconditions, accessibility.
257
+
258
+ Args:
259
+ events: an iterable of hashable event tokens. Stored as a frozenset;
260
+ duplicates collapse.
261
+ pre: maps EVERY event to a kit :class:`Node` — the precondition that
262
+ must hold at a world ``w`` for the event to be executable there
263
+ (``M, w ⊨ pre(e)``). Every event named in ``events`` must have an
264
+ entry (a missing precondition is a modelling error, not an
265
+ implicit "always executable" default — this kit has no
266
+ propositional-truth constant node that :func:`satisfies_modal`
267
+ evaluates, so there is nothing sensible to default to; use an
268
+ explicit tautology, e.g. ``Or(p, Not(p))`` over an atom already in
269
+ the model's vocabulary, for an unconditionally executable event).
270
+ An extra entry for an event not in ``events`` is equally rejected
271
+ — ``pre`` and ``events`` must name exactly the same set.
272
+ relations: maps a relation NAME (str) to a set of ``(e, e')`` event
273
+ edges — the SAME naming convention :class:`KripkeModel` uses
274
+ (``"K:" + agent`` for the epistemic accessibility
275
+ :func:`product_update` combines with the base model's relation of
276
+ the same name), so the two sides of the product line up by name
277
+ without any translation step. A missing name is the empty
278
+ relation — but note that "empty" is NOT "unchanged" and NOT
279
+ "maximally informed" in any benign sense: an agent whose event
280
+ relation is empty relates NO product worlds, so after the
281
+ product `K_a φ` holds VACUOUSLY for every φ (factivity gone).
282
+ :func:`product_update` therefore REFUSES an action that omits
283
+ a relation name the base model carries; the empty default only
284
+ matters for names the base model does not have either.
285
+ Defaults to no relations at all.
286
+
287
+ Every container is copied at construction time (frozensets / a plain
288
+ dict), so later edits to the caller's structures never leak in — the same
289
+ discipline :class:`KripkeModel` follows.
290
+ """
291
+
292
+ def __init__(
293
+ self,
294
+ events: Iterable[Event],
295
+ pre: Mapping[Event, Node],
296
+ relations: Optional[Mapping[str, Iterable[Tuple[Event, Event]]]] = None,
297
+ ):
298
+ self.events: FrozenSet[Event] = frozenset(events)
299
+ pre = dict(pre)
300
+ missing = self.events - pre.keys()
301
+ if missing:
302
+ raise ValueError(
303
+ "ActionModel: no precondition given for event(s) "
304
+ f"{sorted(missing, key=repr)!r} — every event needs an explicit "
305
+ "pre[event] Node (BMS action models have no implicit "
306
+ "'always true' default; see the class docstring for the "
307
+ "tautology workaround for an unconditional event)."
308
+ )
309
+ extra = pre.keys() - self.events
310
+ if extra:
311
+ raise ValueError(
312
+ "ActionModel: pre has precondition(s) for event(s) not in "
313
+ f"events: {sorted(extra, key=repr)!r}."
314
+ )
315
+ self.pre: Dict[Event, Node] = pre
316
+ self.relations: Dict[str, FrozenSet[Tuple[Event, Event]]] = {
317
+ name: frozenset(edges) for name, edges in (relations or {}).items()
318
+ }
319
+
320
+ def __repr__(self) -> str:
321
+ """Show the event set and relation table for inspection."""
322
+ return (
323
+ f"ActionModel(events={set(self.events)!r}, "
324
+ f"pre={ {e: p.to_unicode_str() for e, p in self.pre.items()} !r}, "
325
+ f"relations={ {k: set(v) for k, v in self.relations.items()} !r})"
326
+ )
327
+
328
+ def relation(self, name: str) -> FrozenSet[Tuple[Event, Event]]:
329
+ """Return the edge set of a named relation (empty if undeclared)."""
330
+ return self.relations.get(name, frozenset())
331
+
332
+ def successors(self, name: str, event: Event) -> Set[Event]:
333
+ """Return the set of ``e'`` with ``(event, e')`` in the named relation."""
334
+ return {e2 for (e1, e2) in self.relation(name) if e1 == event}
335
+
336
+
337
+ def product_update(model: KripkeModel, action: "ActionModel") -> KripkeModel:
338
+ """Return the BMS product update ``model ⊗ action`` (Baltag-Moss-Solecki 1998).
339
+
340
+ The construction, verbatim from the standard definition (van Ditmarsch,
341
+ van der Hoek & Kooi, *Dynamic Epistemic Logic*, Springer 2007, Def. 6.9,
342
+ specialised to no postconditions — see the module docstring on scope):
343
+
344
+ - **Worlds**: ``{(w, e) : w ∈ model.worlds, e ∈ action.events,
345
+ model, w ⊨ action.pre[e]}`` — a world/event pair survives exactly when
346
+ the event is executable at that world. Worlds of the product are
347
+ LITERAL ``(w, e)`` tuples (so a caller can always recover the
348
+ originating world/event by unpacking, no separate lookup needed).
349
+ - **Accessibility**: for every relation NAME appearing in either
350
+ ``model.relations`` or ``action.relations`` (their union — a name
351
+ absent from one side is that side's empty relation, per each class's
352
+ own convention), ``(w, e) R (w', e')`` in the product iff ``w R w'``
353
+ in ``model`` AND ``e R e'`` in ``action`` — restricted to surviving
354
+ product worlds on both ends. This is symmetric in model/action exactly
355
+ because both use the identical ``"K:" + agent`` (etc.) naming
356
+ convention for the relation being combined.
357
+ - **Valuation**: purely epistemic events carry no factual change, so
358
+ ``(w, e)`` gets EXACTLY ``model``'s valuation of ``w``, copied
359
+ unchanged (no postcondition parameter exists on :class:`ActionModel` to
360
+ do otherwise — see the module docstring).
361
+ - **Domains**: if ``model`` carries per-world object domains, ``(w, e)``
362
+ inherits ``model``'s domain of ``w`` unchanged (same "no factual
363
+ change" reasoning as the valuation). ``Nominals`` are NOT propagated —
364
+ neither does :func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce`,
365
+ whose restricted model this function is a strict generalisation of, so
366
+ the two stay consistent (see ``public_announcement_action`` /
367
+ ``tests/test_action_models.py`` for the differential check against
368
+ ``announce``).
369
+
370
+ Neither ``model`` nor ``action`` is mutated.
371
+ """
372
+ worlds: FrozenSet[Tuple[World, Event]] = frozenset(
373
+ (w, e)
374
+ for w in model.worlds
375
+ for e in action.events
376
+ if satisfies_modal(action.pre[e], model, w)
377
+ )
378
+
379
+ # A relation the MODEL carries but the ACTION omits would intersect
380
+ # with the empty event relation and silently WIPE the agent's epistemic
381
+ # structure — after which K_a φ holds vacuously for every φ, breaking
382
+ # factivity (adversarial-review reproduced: forgetting one agent in
383
+ # public_announcement_action made K_c(¬P) true with P true everywhere).
384
+ # BMS action models must specify every agent's event relation; refuse
385
+ # loudly instead of manufacturing an omniscient agent.
386
+ missing = sorted(set(model.relations.keys())
387
+ - set(action.relations.keys()))
388
+ if missing:
389
+ raise ValueError(
390
+ "product_update: the action model omits relation name(s) "
391
+ f"{missing!r} that the base model carries — an omitted agent "
392
+ "would come out with an EMPTY product relation (vacuously "
393
+ "omniscient), not an unchanged one. Give every such agent an "
394
+ "explicit event relation (for a public announcement: include "
395
+ "the agent in public_announcement_action's agents).")
396
+
397
+ names = set(model.relations.keys()) | set(action.relations.keys())
398
+ relations: Dict[str, Set[Tuple[Tuple[World, Event], Tuple[World, Event]]]] = {}
399
+ for name in names:
400
+ model_edges = model.relation(name)
401
+ action_edges = action.relation(name)
402
+ relations[name] = {
403
+ ((w, e), (w2, e2))
404
+ for (w, w2) in model_edges
405
+ for (e, e2) in action_edges
406
+ if (w, e) in worlds and (w2, e2) in worlds
407
+ }
408
+
409
+ valuation = {(w, e): set(model.atoms_true_at(w)) for (w, e) in worlds}
410
+
411
+ domains = None
412
+ if model.domains is not None:
413
+ domains = {(w, e): set(model.domain_at(w)) for (w, e) in worlds}
414
+
415
+ return KripkeModel(worlds, relations, valuation, domains=domains)
416
+
417
+
418
+ # The single event public_announcement_action uses, spelled with the kit's own
419
+ # announcement glyph "!" (as in "[φ!]ψ" / "⟨φ!⟩ψ", see fol._modal_nodes.Announce)
420
+ # so a repr of the resulting ActionModel or its product worlds reads naturally.
421
+ _ANNOUNCE_EVENT = "!"
422
+
423
+
424
+ def public_announcement_action(formula: Node, agents: Iterable[str]) -> ActionModel:
425
+ """Return the 1-event :class:`ActionModel` whose product update IS a public announcement of ``formula``.
426
+
427
+ The classic BMS special case (Baltag-Moss-Solecki 1998, §1; van Ditmarsch
428
+ et al. 2007, Example 6.6): a SINGLE event ``"!"`` with precondition
429
+ ``formula`` and, for every agent in ``agents``, the identity relation
430
+ ``{("!", "!")}`` (an agent can only relate the one event to itself —
431
+ vacuously true, since there IS only one event, but stated explicitly so
432
+ the relation NAME ``"K:" + agent`` is populated and the product picks it
433
+ up for every requested agent, not just the ones the base model happens to
434
+ already have a ``"K:"+agent`` relation for).
435
+
436
+ ``product_update(model, public_announcement_action(formula, agents))`` is
437
+ then EXACTLY :func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce`
438
+ up to the obvious relabelling ``(w, "!") ↦ w`` — PROVIDED ``agents``
439
+ covers every ``"K:"+agent`` relation the base model carries (a shorter
440
+ list no longer diverges silently: ``product_update`` refuses it, see
441
+ its missing-relation check; ``announce`` itself restricts EVERY
442
+ relation and needs no agent list at all):
443
+
444
+ - worlds: ``{(w, "!") : M,w ⊨ formula}`` — the SAME survivor set as
445
+ ``announce``'s ``{w : M,w ⊨ formula}``, just each tagged with the one
446
+ event.
447
+ - ``K:agent`` edges: ``(w,"!") R (w',"!")`` iff ``w R w'`` in ``model``
448
+ (the action's identity relation on the single event never removes an
449
+ edge — so the product restricts EXACTLY to survivor-to-survivor edges
450
+ of ``model``'s own relation), matching ``announce``'s own relation
451
+ restriction verbatim.
452
+ - valuation/domains: copied from ``w`` unchanged on both sides.
453
+
454
+ ``tests/test_action_models.py`` cross-checks this agreement explicitly
455
+ (worlds, every relation, and valuation) rather than asserting it here —
456
+ see ``test_public_announcement_action_agrees_with_announce``.
457
+ """
458
+ agents = list(agents)
459
+ return ActionModel(
460
+ events=[_ANNOUNCE_EVENT],
461
+ pre={_ANNOUNCE_EVENT: formula},
462
+ relations={
463
+ _KNOWS_PREFIX + agent: {(_ANNOUNCE_EVENT, _ANNOUNCE_EVENT)}
464
+ for agent in agents
465
+ },
466
+ )