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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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
|
+
)
|