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,1139 @@
|
|
|
1
|
+
"""Kripke (possible-worlds) semantics for the propositional modal fragment.
|
|
2
|
+
|
|
3
|
+
A :class:`KripkeModel` is a possible-worlds frame: a set of worlds, a family of
|
|
4
|
+
named accessibility relations between worlds, and a per-world valuation of the
|
|
5
|
+
ground atoms. :func:`satisfies_modal` computes the truth value of a modal
|
|
6
|
+
formula *at a world*, following the standard Kripke satisfaction relation.
|
|
7
|
+
|
|
8
|
+
Only the **propositional / ground** modal fragment is interpreted here (this is
|
|
9
|
+
v1): the modal operators wrap classical connectives and ground atoms. A ground
|
|
10
|
+
atom is identified by its key (:func:`~unicode_logic_kit.fol.atom_key`: the text it prints as,
|
|
11
|
+
with every constant written by its name, e.g. ``"P"`` or ``"Likes(a, b)"``, also where the
|
|
12
|
+
formula text writes the constant in quotes, ``Likes('a', 'b')``); a world's valuation
|
|
13
|
+
is the set of atom keys true there, and an atom whose key is written either way (its key or
|
|
14
|
+
its text as a formula, ``atom.to_unicode_str()``) is found, so a missing key is false.
|
|
15
|
+
Object quantifiers
|
|
16
|
+
(plain ``Quantifier`` and sorted ``SortedQuantifier``, see "Many-sorted formulas"
|
|
17
|
+
below) ARE interpreted, over per-world domains; Łukasiewicz operators and lambda nodes are rejected
|
|
18
|
+
with NotImplementedError — fuzzy modal logic is future work.
|
|
19
|
+
|
|
20
|
+
Many-sorted formulas: ``satisfies_modal`` relativizes the WHOLE input formula
|
|
21
|
+
ONCE, up front, before any dispatch — the same "relativize once, up front"
|
|
22
|
+
choice ``fol.qml.qml_translate`` / ``semantics.intuitionistic._prepare_many_sorted``
|
|
23
|
+
make and for the identical reason: a ``SortedConstant`` (``alice:Human``) can
|
|
24
|
+
occur anywhere in the formula, not only directly under a ``SortedQuantifier``,
|
|
25
|
+
so relativizing lazily (only when the recursive descent happens to walk into a
|
|
26
|
+
``SortedQuantifier`` node) would leave a bare sorted constant's ``:Sort``
|
|
27
|
+
suffix in place, and the valuation lookup for it would then silently miss.
|
|
28
|
+
Relativizing uses ``Node._relativize`` (the same reduction ``fol.to_fol``
|
|
29
|
+
uses): a ``SortedQuantifier`` (``∀x:S φ`` / ``∃x:S φ``) becomes a guarded plain
|
|
30
|
+
``Quantifier`` — ``∀x (S(x) → φ)`` / ``∃x (S(x) ∧ φ)`` — over the current
|
|
31
|
+
world's domain ``D_w``; a bare ``SortedConstant`` becomes a plain ``Constant``.
|
|
32
|
+
Two consequences of this reduction, both deliberate design choices, not gaps:
|
|
33
|
+
|
|
34
|
+
- **Sorts are world-relative, not rigid.** The sort guard ``S(x)`` is an
|
|
35
|
+
ordinary atom, looked up in ``valuation`` exactly like any other atom, so an
|
|
36
|
+
individual can be ``S`` at one world and not at another — the same
|
|
37
|
+
"actualist" reading this module already gives the bare per-world domain
|
|
38
|
+
``D_w`` (see ``domains``/``domain_at`` above). A caller that wants a sort
|
|
39
|
+
RIGID (the same extension at every world) states that itself, as an extra
|
|
40
|
+
frame condition on the relation being evaluated over (e.g. asserting
|
|
41
|
+
``S(d) ↔ S'(d)`` between every pair of accessible worlds in the model it
|
|
42
|
+
builds) — this evaluator does not assume or enforce it.
|
|
43
|
+
- **Non-emptiness is the caller's responsibility, exactly as it already is for
|
|
44
|
+
the unsorted domain.** This evaluator never assumes a sort — or a bare
|
|
45
|
+
per-world domain — is non-empty; ``∀x:S φ → ∃x:S φ`` can come out FALSE
|
|
46
|
+
here if the model happens to make ``S`` empty at ``world``. The classical
|
|
47
|
+
many-sorted routes (``api.prove`` et al.) instead ALWAYS assume every sort
|
|
48
|
+
is non-empty, by adding ``fol.nonempty_sort_axioms`` as extra premises (see
|
|
49
|
+
that function's docstring) — so a caller who wants THIS evaluator to agree
|
|
50
|
+
with a classical MSFOL verdict on a modal-free sorted formula must build the
|
|
51
|
+
``KripkeModel`` so each mentioned sort's guard atom holds of at least one
|
|
52
|
+
individual in ``D_w`` at every world that matters, the same way domains and
|
|
53
|
+
valuations are already the caller's construction to get right. See
|
|
54
|
+
``tests/test_sorted_modal.py`` for a worked differential against
|
|
55
|
+
``api.prove`` built this way.
|
|
56
|
+
- **A sorted constant being in its sort is a property of the MODEL, too.**
|
|
57
|
+
``c:S`` denotes an element of ``S``, and a constant is a rigid designator, so
|
|
58
|
+
in a legal model the guard atom ``S(c)`` is true at EVERY world — whatever the
|
|
59
|
+
world's domain says about whether ``c`` exists there. The evaluator reads the
|
|
60
|
+
guard from the valuation like any other atom and does not check it: in a model
|
|
61
|
+
that leaves ``Human(socrates)`` out of some world's valuation,
|
|
62
|
+
``∀x:Human Mortal(x) → Mortal(socrates:Human)`` can be FALSE there, a model
|
|
63
|
+
the many-sorted routes (``qml_is_valid``, ``api.prove``) never consider. The
|
|
64
|
+
routes assert the fact as a background axiom (``fol.sort_membership_axioms``,
|
|
65
|
+
lifted per world by ``fol.qml``) and an evaluator of ONE given model cannot, so
|
|
66
|
+
a caller who compares this evaluator with them builds the model that way, or
|
|
67
|
+
asks :func:`sorted_constant_violations` which worlds of a model fall short. A
|
|
68
|
+
plain constant ``socrates`` has no sort, and nothing is asserted about it.
|
|
69
|
+
|
|
70
|
+
Equality is NOT interpreted. ``=`` and ``≠`` need a semantics of TERMS — what
|
|
71
|
+
``a`` and ``b`` denote, so that ``a = b`` can be decided as identity of those
|
|
72
|
+
denotations — and this evaluator has none: it never interprets a term, it looks
|
|
73
|
+
an atom up by its rendered key in a world's valuation set. Run on ``a = b`` that
|
|
74
|
+
lookup would silently read the identity as an uninterpreted proposition keyed
|
|
75
|
+
``"a = b"``, so ``a = a`` would come out FALSE unless a caller happened to
|
|
76
|
+
list it, and ``□(a = b) → a = b`` would be decided by the frame alone. The kit's
|
|
77
|
+
rule is that an unsupported fragment is refused loudly, never approximated, so
|
|
78
|
+
:func:`satisfies_modal` (and so :func:`ctl_ex` / :func:`ctl_af` / :func:`ctl_eg`
|
|
79
|
+
/ :func:`ctl_au`, and every evaluator built on it) raises ``NotImplementedError``
|
|
80
|
+
naming the atom as soon as ANY ``=`` / ``≠`` atom occurs ANYWHERE in the formula
|
|
81
|
+
— the check scans the whole tree before evaluating, because evaluating lazily
|
|
82
|
+
would let a short-circuit (``P ∨ a = b`` at a world where ``P`` holds), a
|
|
83
|
+
vacuous ``□`` at a dead end, or an empty domain skip the atom and return a verdict
|
|
84
|
+
that never looked at it. Decide identity with :func:`unicode_logic_kit.fol.qml.qml_is_valid`
|
|
85
|
+
(quantified modal logic, where ``=`` is rigid identity over the object domain) or
|
|
86
|
+
a first-order route; the other arithmetic comparisons (``<``, ``≤`` …) are
|
|
87
|
+
ordinary keyed atoms here, exactly as before.
|
|
88
|
+
|
|
89
|
+
Relation-name convention (keys of :attr:`KripkeModel.relations`):
|
|
90
|
+
|
|
91
|
+
- ``"alethic"`` — the accessibility relation for Box □ / Diamond ◇.
|
|
92
|
+
- ``"K:" + agent`` — the epistemic relation for ``Knows(agent, …)``, and
|
|
93
|
+
also what ``EverybodyKnows``/``DistributedKnowledge``/
|
|
94
|
+
``CommonKnowledge`` (group operators E_G/D_G/C_G, see
|
|
95
|
+
below) combine by union/intersection/reflexive-
|
|
96
|
+
transitive-closure — there is no separate relation
|
|
97
|
+
family for the group operators.
|
|
98
|
+
- ``"B:" + agent`` — the doxastic relation for ``Believes(agent, …)``.
|
|
99
|
+
- ``"Say:" + agent`` — the assertive relation for ``Says(agent, …)`` (non-factive).
|
|
100
|
+
- ``"Want:" + agent`` — the bouletic relation for ``Wants(agent, …)`` (non-veridical).
|
|
101
|
+
- ``"temporal"`` — the one-step successor relation for Next / Always /
|
|
102
|
+
Eventually / Until.
|
|
103
|
+
- ``"deontic"`` — the (serial) accessibility relation for Obligatory O /
|
|
104
|
+
Permitted P (Standard Deontic Logic, the system KD).
|
|
105
|
+
|
|
106
|
+
A missing relation denotes the empty relation; a missing world valuation denotes
|
|
107
|
+
the empty set (every atom false there). Inputs are never mutated: the closure
|
|
108
|
+
and path helpers build fresh sets.
|
|
109
|
+
|
|
110
|
+
Hybrid logic H(@) is interpreted through the optional ``nominals`` mapping
|
|
111
|
+
(name → world): ``Nominal(i)`` is true exactly at the world the assignment
|
|
112
|
+
names, and ``At(i, φ)`` evaluates φ *at* that world, wherever the evaluation
|
|
113
|
+
currently stands. A nominal without an assignment raises a ValueError naming
|
|
114
|
+
it (rather than silently defaulting), since a nominal must name exactly one
|
|
115
|
+
world for the hybrid semantics to make sense.
|
|
116
|
+
|
|
117
|
+
The ↓ binder (N1, full H(@,↓)) — ``Down(x, φ)`` (``↓x.φ``) — is interpreted
|
|
118
|
+
the same way, with no extra machinery: evaluating ``Down(x, φ)`` at world
|
|
119
|
+
``w`` locally REBINDS the nominal named ``x`` to ``w`` for the evaluation of
|
|
120
|
+
``φ`` (still at ``w``), by recursing with a model whose ``nominals`` mapping
|
|
121
|
+
is ``model.nominals`` overridden at key ``x``. Because that override is a
|
|
122
|
+
plain dict-key overwrite — not a textual rewrite of ``φ`` — the usual
|
|
123
|
+
name-scoping rules of a binder fall out automatically, with no separate
|
|
124
|
+
alpha-renaming/fresh-name step: a nested ``Down(x, …)`` inside ``φ`` overrides
|
|
125
|
+
the SAME key again for its own (deeper) scope, so it shadows the outer
|
|
126
|
+
binding exactly the way a nested ``∀x`` would (``↓x.↓x.φ`` behaves as bare
|
|
127
|
+
``φ``, the outer binding entirely inert); a DIFFERENTLY-named nested binder
|
|
128
|
+
``Down(y, …)`` with ``y ≠ x`` extends the dict at a different key and leaves
|
|
129
|
+
``x`` untouched, so it cannot capture an outer ``@x``/bare-``x`` occurrence
|
|
130
|
+
(``↓x.(P ∧ ↓y.@x Q)`` — the inner ``↓y`` cannot affect the outer ``@x``); and
|
|
131
|
+
because the rebound world ``w`` is fixed in the dict rather than tracked
|
|
132
|
+
positionally, an occurrence of ``x`` reached through a LATER modality (a
|
|
133
|
+
``Box``/``Diamond`` inside ``φ`` moving evaluation to some other world
|
|
134
|
+
``w2``) still resolves to the ORIGINAL ``w`` — the state variable, once
|
|
135
|
+
bound, is rigid for the rest of its scope, exactly like a nominal already is
|
|
136
|
+
(this is what makes ``↓x.□¬x`` the FO irreflexivity condition: for every
|
|
137
|
+
successor ``w2`` of ``w``, ``¬x`` means "``w2`` is not the world ``x``
|
|
138
|
+
names", i.e. not ``w`` itself).
|
|
139
|
+
|
|
140
|
+
H(@,↓) validity is UNDECIDABLE, but evaluation at a GIVEN finite model is not
|
|
141
|
+
touched by that — ``satisfies_modal`` decides ``Down`` exactly as it decides
|
|
142
|
+
every other construct here, so it remains the terminating, always-available
|
|
143
|
+
"route A" oracle for ↓ (hand-built models, and the brute-force battery in
|
|
144
|
+
``tests/test_hybrid_down.py``), and the oracle
|
|
145
|
+
:func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search`'s bounded search
|
|
146
|
+
re-verifies every countermodel it reports against. See
|
|
147
|
+
:mod:`unicode_logic_kit.fol._hybrid_nodes` (the ``Down`` node) and
|
|
148
|
+
:mod:`unicode_logic_kit.fol.modal_translation` (``down_is_valid``, the
|
|
149
|
+
Z3/PROVED-only "route B" half) for the rest of the architecture.
|
|
150
|
+
|
|
151
|
+
Group epistemic operators — ``EverybodyKnows`` (E_G φ), ``DistributedKnowledge``
|
|
152
|
+
(D_G φ), and ``CommonKnowledge`` (C_G φ), each carrying a ``group`` tuple of
|
|
153
|
+
agent terms (:mod:`unicode_logic_kit.fol._modal_nodes`, parsed from
|
|
154
|
+
``E_{a,b,…}``/``D_{a,b,…}``/``C_{a,b,…}`` surface syntax) — are THIN dispatches
|
|
155
|
+
into :mod:`unicode_logic_kit.semantics.action_models`'s
|
|
156
|
+
:func:`~unicode_logic_kit.semantics.action_models.everybody_knows` /
|
|
157
|
+
:func:`~unicode_logic_kit.semantics.action_models.distributed_knowledge_holds` /
|
|
158
|
+
:func:`~unicode_logic_kit.semantics.action_models.common_knowledge_holds`, which
|
|
159
|
+
implement the actual union/intersection/closure semantics over the group's
|
|
160
|
+
``"K:"+agent`` relations — this module owns none of that logic, only the
|
|
161
|
+
node-to-function wiring (see those functions' docstrings for the semantics,
|
|
162
|
+
including ``distributed_knowledge_holds``'s deliberately-different empty-group
|
|
163
|
+
convention: it RAISES rather than defaulting).
|
|
164
|
+
|
|
165
|
+
Public announcement logic (PAL) is interpreted directly — ``Announce`` (``[φ!]ψ``)
|
|
166
|
+
and ``AnnounceDiamond`` (``⟨φ!⟩ψ``) are the only two constructs here that do NOT
|
|
167
|
+
just recurse at a fixed world or along a fixed relation: they build the
|
|
168
|
+
φ-restricted model ``M|φ`` (:func:`unicode_logic_kit.semantics.dynamic_epistemic.announce`)
|
|
169
|
+
and evaluate ``ψ`` there. This is the ORACLE that
|
|
170
|
+
:func:`unicode_logic_kit.fol.pal.reduce_announcements` (a purely SYNTACTIC
|
|
171
|
+
elimination of Announce/AnnounceDiamond, sound only for the propositional-modal
|
|
172
|
+
fragment interpreted here) is differentially tested against.
|
|
173
|
+
|
|
174
|
+
Documented temporal semantics:
|
|
175
|
+
|
|
176
|
+
- ``Next φ``: φ holds at **all** immediate ``"temporal"``-successors of the
|
|
177
|
+
current world. On a deterministic / linear frame (each world has at most one
|
|
178
|
+
successor) this is exactly "φ at the unique next state"; on a branching frame
|
|
179
|
+
it is read universally (the "for all next states" reading).
|
|
180
|
+
- ``Always φ`` (G): φ holds at every world reachable from the current world via
|
|
181
|
+
the **reflexive-transitive** closure of ``"temporal"`` (the current world
|
|
182
|
+
included).
|
|
183
|
+
- ``Eventually φ`` (F): φ holds at **some** such reachable world (current world
|
|
184
|
+
included).
|
|
185
|
+
- ``Until(φ, ψ)``: there is a finite ``"temporal"`` path
|
|
186
|
+
``w0 → w1 → … → wn`` (n ≥ 0) starting at the current world with ψ true at
|
|
187
|
+
``wn`` and φ true at every earlier world ``w0 … w(n-1)``. This is the
|
|
188
|
+
finite-reachability reading of strong Until; the search is depth-first with a
|
|
189
|
+
visited guard so cycles in the frame terminate.
|
|
190
|
+
|
|
191
|
+
Next / Always / Eventually / Until above are all LINEAR-time: each has exactly
|
|
192
|
+
one path reading baked into its single AST node (no A/E path-quantifier prefix
|
|
193
|
+
exists anywhere in the AST). Branching-time CTL model checking —
|
|
194
|
+
:func:`ctl_ex`, :func:`ctl_af`, :func:`ctl_eg`, :func:`ctl_au` — adds the four
|
|
195
|
+
readings that baked-in choice leaves out (EX, the existential dual of Next's
|
|
196
|
+
universal reading; AF/EG, the forward/backward fixpoint pair that reachability
|
|
197
|
+
alone cannot compute; AU, the universal-path generalisation of Until) as plain
|
|
198
|
+
functions taking arbitrary :class:`~unicode_logic_kit.fol.nodes.Node`
|
|
199
|
+
subformulas, evaluated via :func:`satisfies_modal` — exactly the pattern
|
|
200
|
+
:func:`~unicode_logic_kit.semantics.action_models.common_knowledge_holds` and
|
|
201
|
+
:func:`~unicode_logic_kit.semantics.action_models.everybody_knows` already use,
|
|
202
|
+
not new dispatch branches on ``formula``'s type. See the CTL section near the
|
|
203
|
+
end of this module for the fixpoint algorithms and the deadlock convention.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
import shutil
|
|
207
|
+
import subprocess
|
|
208
|
+
from typing import Any, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, Tuple
|
|
209
|
+
|
|
210
|
+
from ..fol.nodes import (
|
|
211
|
+
Node,
|
|
212
|
+
Atom, Not, And, Or, Xor, Implies, Iff,
|
|
213
|
+
Quantifier,
|
|
214
|
+
Box, Diamond, Knows, Believes, Says, Wants,
|
|
215
|
+
Always, Eventually, Next, Until,
|
|
216
|
+
Historically, Once, Previous, Since,
|
|
217
|
+
Obligatory, Permitted,
|
|
218
|
+
Nominal, At, Down,
|
|
219
|
+
Constant, substitute, sort_membership_axioms,
|
|
220
|
+
)
|
|
221
|
+
from ..fol._modal_nodes import (
|
|
222
|
+
Announce, AnnounceDiamond,
|
|
223
|
+
EverybodyKnows, DistributedKnowledge, CommonKnowledge,
|
|
224
|
+
)
|
|
225
|
+
from ..fol._atom_keys import find_key
|
|
226
|
+
from ..fol._msfl_nodes import key_text
|
|
227
|
+
from ..fol._truth_constants import truth_value as _truth_value
|
|
228
|
+
from ._modal_reject import (
|
|
229
|
+
EQUALITY_PREDICATES, FUZZY_TYPES, LAMBDA_TYPES,
|
|
230
|
+
reject_equality, reject_equality_in, reject_fuzzy, reject_lambda,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Quantifier-type spellings used by the AST.
|
|
234
|
+
_FORALL = ("∀", "forall")
|
|
235
|
+
_EXISTS = ("∃", "exists")
|
|
236
|
+
|
|
237
|
+
# Relation-name prefixes / keys (kept here so the model and the standard
|
|
238
|
+
# translation stay in sync via documentation; the strings are the contract).
|
|
239
|
+
_ALETHIC = "alethic"
|
|
240
|
+
_TEMPORAL = "temporal"
|
|
241
|
+
_DEONTIC = "deontic"
|
|
242
|
+
_KNOWS_PREFIX = "K:"
|
|
243
|
+
_BELIEVES_PREFIX = "B:"
|
|
244
|
+
_SAYS_PREFIX = "Say:"
|
|
245
|
+
_WANTS_PREFIX = "Want:"
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _agent_key(agent: Node) -> str:
|
|
249
|
+
"""Relation-key suffix for an epistemic/doxastic agent term.
|
|
250
|
+
|
|
251
|
+
The agent is a term (Variable or Constant). Object quantifiers ground a bound
|
|
252
|
+
agent to a Constant before the modality is reached (``∀x (… → K_x φ)`` becomes
|
|
253
|
+
``K_<d> φ`` per individual ``d``), so this is the constant/variable name and the
|
|
254
|
+
relation key matches the model's ``"K:"+name`` / ``"B:"+name`` convention. A term
|
|
255
|
+
without a name of its own (a numeral) is keyed by its text with every constant
|
|
256
|
+
written by its name, like an atom: a relation name is a key, not formula text.
|
|
257
|
+
"""
|
|
258
|
+
return getattr(agent, "name", None) or key_text(agent)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
World = Any
|
|
262
|
+
Edge = Tuple[World, World]
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class KripkeModel:
|
|
266
|
+
"""A Kripke model: worlds, named accessibility relations, and a valuation.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
worlds: an iterable of worlds (any hashable values). Stored as a frozen
|
|
270
|
+
set; duplicates collapse.
|
|
271
|
+
relations: maps a relation NAME (str) to a set of ``(w, w')`` edges.
|
|
272
|
+
Recognised names: ``"alethic"`` (Box/Diamond), ``"K:"+agent``
|
|
273
|
+
(Knows), ``"B:"+agent`` (Believes), ``"temporal"`` (Next / Always /
|
|
274
|
+
Eventually / Until), ``"deontic"`` (Obligatory / Permitted; serial
|
|
275
|
+
in Standard Deontic Logic). A missing name is the empty relation.
|
|
276
|
+
Each edge set is copied into a frozen set.
|
|
277
|
+
valuation: maps a world to the set of GROUND-ATOM KEYS true there, where
|
|
278
|
+
a key is the text of the atom with every constant written by its name
|
|
279
|
+
(e.g. ``"P"`` or ``"Likes(a, b)"``; :func:`~unicode_logic_kit.fol.atom_key`).
|
|
280
|
+
The text of the atom as a formula, ``"Likes('a', 'b')"``, is read as the
|
|
281
|
+
same key.
|
|
282
|
+
A missing world maps to the empty set (every atom false there). Each
|
|
283
|
+
entry is copied into a frozen set.
|
|
284
|
+
nominals: maps a NOMINAL NAME (str) to the single world it names (the
|
|
285
|
+
hybrid-logic assignment interpreting ``Nominal`` / ``At``). Defaults
|
|
286
|
+
to empty. Every referenced world must be in ``worlds`` — a dangling
|
|
287
|
+
assignment raises ValueError at construction time.
|
|
288
|
+
|
|
289
|
+
All mappings default to empty, so ``KripkeModel({0, 1})`` is a valid
|
|
290
|
+
(atom-free, relation-free) frame. The constructor copies every container, so
|
|
291
|
+
later edits to the caller's structures never leak in.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
def __init__(
|
|
295
|
+
self,
|
|
296
|
+
worlds: Iterable[World],
|
|
297
|
+
relations: Optional[Mapping[str, Iterable[Edge]]] = None,
|
|
298
|
+
valuation: Optional[Mapping[World, Iterable[str]]] = None,
|
|
299
|
+
domains: Optional[Mapping[World, Iterable[Any]]] = None,
|
|
300
|
+
domain: Optional[Iterable[Any]] = None,
|
|
301
|
+
nominals: Optional[Mapping[str, World]] = None,
|
|
302
|
+
):
|
|
303
|
+
"""Build a Kripke model, copying every container so edits never leak in.
|
|
304
|
+
|
|
305
|
+
``domains`` maps each world to the set of individuals existing there (the
|
|
306
|
+
per-world object domain ``D_w`` of quantified modal logic); ``domain`` is a
|
|
307
|
+
shorthand for a **constant** domain (the same individuals at every world).
|
|
308
|
+
Supplying either lets :func:`satisfies_modal` interpret object quantifiers
|
|
309
|
+
(``∀x`` / ``∃x``) *actualistically* — at a world ``w`` they range over
|
|
310
|
+
``D_w`` — so the Barcan formulas come out valid or invalid according to how
|
|
311
|
+
the domains vary. Omit both for the purely propositional fragment.
|
|
312
|
+
|
|
313
|
+
``nominals`` maps each hybrid nominal name to the ONE world it names;
|
|
314
|
+
every referenced world must exist in ``worlds`` (checked here, so a
|
|
315
|
+
dangling nominal fails fast instead of at evaluation time).
|
|
316
|
+
"""
|
|
317
|
+
self.worlds: FrozenSet[World] = frozenset(worlds)
|
|
318
|
+
self.relations: Dict[str, FrozenSet[Edge]] = {
|
|
319
|
+
name: frozenset(edges) for name, edges in (relations or {}).items()
|
|
320
|
+
}
|
|
321
|
+
self.valuation: Dict[World, FrozenSet[str]] = {
|
|
322
|
+
world: frozenset(keys) for world, keys in (valuation or {}).items()
|
|
323
|
+
}
|
|
324
|
+
if domains is not None:
|
|
325
|
+
self.domains: Optional[Dict[World, FrozenSet[Any]]] = {
|
|
326
|
+
world: frozenset(ind) for world, ind in domains.items()
|
|
327
|
+
}
|
|
328
|
+
elif domain is not None:
|
|
329
|
+
const = frozenset(domain)
|
|
330
|
+
self.domains = {world: const for world in self.worlds}
|
|
331
|
+
else:
|
|
332
|
+
self.domains = None
|
|
333
|
+
self.nominals: Dict[str, World] = dict(nominals or {})
|
|
334
|
+
for name, named in self.nominals.items():
|
|
335
|
+
if named not in self.worlds:
|
|
336
|
+
raise ValueError(
|
|
337
|
+
f"KripkeModel: nominal {name!r} is assigned to world "
|
|
338
|
+
f"{named!r}, which is not among the model's worlds."
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def __repr__(self) -> str:
|
|
342
|
+
"""Show world count and the relation / valuation tables for inspection."""
|
|
343
|
+
return (
|
|
344
|
+
f"KripkeModel(worlds={set(self.worlds)!r}, "
|
|
345
|
+
f"relations={ {k: set(v) for k, v in self.relations.items()} !r}, "
|
|
346
|
+
f"valuation={ {k: set(v) for k, v in self.valuation.items()} !r})"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def relation(self, name: str) -> FrozenSet[Edge]:
|
|
350
|
+
"""Return the edge set of a named relation (empty if undeclared)."""
|
|
351
|
+
return self.relations.get(name, frozenset())
|
|
352
|
+
|
|
353
|
+
def successors(self, name: str, world: World) -> Set[World]:
|
|
354
|
+
"""Return the set of ``w'`` with ``(world, w')`` in the named relation."""
|
|
355
|
+
return {w2 for (w1, w2) in self.relation(name) if w1 == world}
|
|
356
|
+
|
|
357
|
+
def atoms_true_at(self, world: World) -> FrozenSet[str]:
|
|
358
|
+
"""Return the ground-atom keys true at ``world`` (empty if undeclared)."""
|
|
359
|
+
return self.valuation.get(world, frozenset())
|
|
360
|
+
|
|
361
|
+
def domain_at(self, world: World) -> FrozenSet[Any]:
|
|
362
|
+
"""Return the individuals existing at ``world`` (the object domain ``D_w``).
|
|
363
|
+
|
|
364
|
+
Raises ValueError if the model carries no domains (a purely propositional
|
|
365
|
+
model), since object quantifiers cannot then be interpreted.
|
|
366
|
+
"""
|
|
367
|
+
if self.domains is None:
|
|
368
|
+
raise ValueError(
|
|
369
|
+
"satisfies_modal: this Kripke model has no object domains, so "
|
|
370
|
+
"object quantifiers (∀x / ∃x) cannot be evaluated — build the model "
|
|
371
|
+
"with domains={world: [...]} (varying) or domain=[...] (constant)."
|
|
372
|
+
)
|
|
373
|
+
return self.domains.get(world, frozenset())
|
|
374
|
+
|
|
375
|
+
def to_dot(self, *, show_valuation: bool = True) -> str:
|
|
376
|
+
"""Render the model as a Graphviz DOT digraph string.
|
|
377
|
+
|
|
378
|
+
Pure Python, no external dependency — mirrors the convention of
|
|
379
|
+
:meth:`~unicode_logic_kit.fol._fol_nodes.Node.to_dot` (escape labels the
|
|
380
|
+
same way; return the source text, never shell out). One node per world
|
|
381
|
+
in :attr:`worlds`, declared in ``repr()`` order for determinism (worlds
|
|
382
|
+
are "any hashable value", so a world is stringified defensively via
|
|
383
|
+
``repr()`` for the DOT node id and via ``str()`` for the visible
|
|
384
|
+
label). When ``show_valuation`` is true (the default) each node's label
|
|
385
|
+
gets a second line with the atoms :meth:`atoms_true_at` returns for
|
|
386
|
+
that world plus any nominal name(s) (from :attr:`nominals`) pointing at
|
|
387
|
+
it, prefixed ``@``; if the model carries object domains (see
|
|
388
|
+
:meth:`domain_at`), a third line shows that world's domain.
|
|
389
|
+
|
|
390
|
+
Every relation in :attr:`relations` contributes one edge per ``(w, w')``
|
|
391
|
+
pair, labelled with the relation's own name — this is the one place a
|
|
392
|
+
naive per-pair rendering would lose information, since a model can
|
|
393
|
+
carry several named relations over the same world set at once (several
|
|
394
|
+
agents' ``K:``/``B:`` relations, ``alethic``, ``temporal``, ``deontic``
|
|
395
|
+
all coexisting); the relation-name label is what keeps them visually
|
|
396
|
+
distinguishable instead of collapsing into indistinguishable arrows.
|
|
397
|
+
Relations and, within each, their edges are emitted in sorted order too,
|
|
398
|
+
so the whole output is deterministic and directly string-comparable.
|
|
399
|
+
|
|
400
|
+
This is a read-only inspection method: it never touches model
|
|
401
|
+
construction or :func:`satisfies_modal`.
|
|
402
|
+
"""
|
|
403
|
+
def esc(text: str) -> str:
|
|
404
|
+
"""Escape backslash/quote/newline/CR for safe placement inside a
|
|
405
|
+
double-quoted DOT string, on a single physical source line."""
|
|
406
|
+
return (
|
|
407
|
+
text.replace("\\", "\\\\")
|
|
408
|
+
.replace('"', '\\"')
|
|
409
|
+
.replace("\n", "\\n")
|
|
410
|
+
.replace("\r", "\\r")
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def node_id(world: World) -> str:
|
|
414
|
+
"""The DOT node id for a world: its escaped ``repr()``."""
|
|
415
|
+
return esc(repr(world))
|
|
416
|
+
|
|
417
|
+
nominals_at: Dict[World, List[str]] = {}
|
|
418
|
+
for name, named in self.nominals.items():
|
|
419
|
+
nominals_at.setdefault(named, []).append(name)
|
|
420
|
+
|
|
421
|
+
lines = ["digraph Kripke {", " node [shape=box];"]
|
|
422
|
+
for world in sorted(self.worlds, key=repr):
|
|
423
|
+
label_lines = [esc(str(world))]
|
|
424
|
+
if show_valuation:
|
|
425
|
+
bits = sorted(self.atoms_true_at(world))
|
|
426
|
+
bits += [f"@{n}" for n in sorted(nominals_at.get(world, []))]
|
|
427
|
+
label_lines.append(esc(", ".join(bits)))
|
|
428
|
+
if self.domains is not None:
|
|
429
|
+
dom = ", ".join(sorted(str(d) for d in self.domain_at(world)))
|
|
430
|
+
label_lines.append(esc(f"D = {{{dom}}}"))
|
|
431
|
+
label = "\\n".join(label_lines)
|
|
432
|
+
lines.append(f' "{node_id(world)}" [label="{label}"];')
|
|
433
|
+
|
|
434
|
+
for rel_name in sorted(self.relations):
|
|
435
|
+
edges = sorted(
|
|
436
|
+
self.relations[rel_name],
|
|
437
|
+
key=lambda edge: (repr(edge[0]), repr(edge[1])),
|
|
438
|
+
)
|
|
439
|
+
rel_label = esc(rel_name)
|
|
440
|
+
for source, target in edges:
|
|
441
|
+
lines.append(
|
|
442
|
+
f' "{node_id(source)}" -> "{node_id(target)}" '
|
|
443
|
+
f'[label="{rel_label}"];'
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
lines.append("}")
|
|
447
|
+
return "\n".join(lines)
|
|
448
|
+
|
|
449
|
+
def to_svg(self, *, dot_binary: Optional[str] = None) -> str:
|
|
450
|
+
"""Render the model to SVG by piping :meth:`to_dot` through Graphviz ``dot``.
|
|
451
|
+
|
|
452
|
+
Resolves the binary via ``dot_binary`` or ``shutil.which("dot")`` and,
|
|
453
|
+
if neither finds one, raises ``RuntimeError`` naming the missing tool
|
|
454
|
+
and how to install it — the same shutil.which-gate-and-fail-loudly
|
|
455
|
+
pattern already used for the optional external provers (eprover,
|
|
456
|
+
minizinc, vampire, prover9, Isabelle) elsewhere in this kit: this never
|
|
457
|
+
falls back to an approximate or partial rendering.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
dot_binary: an explicit path to the ``dot`` executable, overriding
|
|
461
|
+
``PATH`` discovery.
|
|
462
|
+
|
|
463
|
+
Raises:
|
|
464
|
+
RuntimeError: no ``dot`` binary found, the given/discovered binary
|
|
465
|
+
could not be executed, or the ``dot`` subprocess itself failed
|
|
466
|
+
(its stderr is included in the message).
|
|
467
|
+
"""
|
|
468
|
+
binary = dot_binary or shutil.which("dot")
|
|
469
|
+
if binary is None:
|
|
470
|
+
raise RuntimeError(
|
|
471
|
+
"KripkeModel.to_svg: no Graphviz 'dot' binary found on PATH — "
|
|
472
|
+
"install Graphviz (e.g. 'apt install graphviz', 'brew install "
|
|
473
|
+
"graphviz', or see https://graphviz.org/download/) or pass "
|
|
474
|
+
"to_svg(dot_binary=...) explicitly."
|
|
475
|
+
)
|
|
476
|
+
try:
|
|
477
|
+
result = subprocess.run(
|
|
478
|
+
[binary, "-Tsvg"],
|
|
479
|
+
input=self.to_dot(),
|
|
480
|
+
capture_output=True,
|
|
481
|
+
text=True,
|
|
482
|
+
)
|
|
483
|
+
except OSError as exc:
|
|
484
|
+
raise RuntimeError(
|
|
485
|
+
f"KripkeModel.to_svg: could not run {binary!r} ({exc}) — is "
|
|
486
|
+
"this a valid Graphviz 'dot' binary?"
|
|
487
|
+
) from exc
|
|
488
|
+
if result.returncode != 0:
|
|
489
|
+
raise RuntimeError(
|
|
490
|
+
f"KripkeModel.to_svg: 'dot -Tsvg' failed (exit "
|
|
491
|
+
f"{result.returncode}): {result.stderr.strip()}"
|
|
492
|
+
)
|
|
493
|
+
return result.stdout
|
|
494
|
+
|
|
495
|
+
def _repr_svg_(self) -> Optional[str]:
|
|
496
|
+
"""IPython/Jupyter rich-display hook: render to SVG, or opt out quietly.
|
|
497
|
+
|
|
498
|
+
Returns ``None`` (so the notebook falls back to the plain ``__repr__``
|
|
499
|
+
text) instead of raising when Graphviz's ``dot`` binary — or the
|
|
500
|
+
subprocess call to it — is not available, so merely inspecting a
|
|
501
|
+
KripkeModel in a notebook without Graphviz installed never crashes the
|
|
502
|
+
display machinery.
|
|
503
|
+
"""
|
|
504
|
+
try:
|
|
505
|
+
return self.to_svg()
|
|
506
|
+
except RuntimeError:
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def reflexive_transitive_closure(
|
|
511
|
+
edges: Iterable[Edge],
|
|
512
|
+
sources: Iterable[World],
|
|
513
|
+
) -> Set[World]:
|
|
514
|
+
"""Return every world reachable from ``sources`` along ``edges``, reflexively.
|
|
515
|
+
|
|
516
|
+
The result contains each source world itself (reflexive) and every world
|
|
517
|
+
reachable from a source by following one or more edges (transitive). A
|
|
518
|
+
breadth-first walk with a visited set; the input edge collection is never
|
|
519
|
+
mutated. Used by Always / Eventually over the ``"temporal"`` relation.
|
|
520
|
+
"""
|
|
521
|
+
edge_set = set(edges)
|
|
522
|
+
reachable: Set[World] = set()
|
|
523
|
+
frontier = list(sources)
|
|
524
|
+
while frontier:
|
|
525
|
+
w = frontier.pop()
|
|
526
|
+
if w in reachable:
|
|
527
|
+
continue
|
|
528
|
+
reachable.add(w)
|
|
529
|
+
for (w1, w2) in edge_set:
|
|
530
|
+
if w1 == w and w2 not in reachable:
|
|
531
|
+
frontier.append(w2)
|
|
532
|
+
return reachable
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _until_holds(
|
|
536
|
+
left: Node,
|
|
537
|
+
right: Node,
|
|
538
|
+
model: KripkeModel,
|
|
539
|
+
world: World,
|
|
540
|
+
) -> bool:
|
|
541
|
+
"""Decide ``Until(left, right)`` at ``world`` by finite-path search.
|
|
542
|
+
|
|
543
|
+
Searches for a finite ``"temporal"`` path ``world = w0 → … → wn`` (n ≥ 0)
|
|
544
|
+
with ``right`` true at ``wn`` and ``left`` true at every earlier ``wi``. A
|
|
545
|
+
depth-first search guarded by a visited set: if ``right`` already holds we
|
|
546
|
+
succeed immediately (n = 0); otherwise ``left`` must hold here and the
|
|
547
|
+
search continues into the temporal successors. The visited guard makes the
|
|
548
|
+
search terminate on cyclic frames.
|
|
549
|
+
"""
|
|
550
|
+
edges = model.relation(_TEMPORAL)
|
|
551
|
+
|
|
552
|
+
def search(w: World, visited: FrozenSet[World]) -> bool:
|
|
553
|
+
"""Return whether some path from ``w`` witnesses the Until."""
|
|
554
|
+
if satisfies_modal(right, model, w):
|
|
555
|
+
return True
|
|
556
|
+
if not satisfies_modal(left, model, w):
|
|
557
|
+
return False
|
|
558
|
+
next_visited = visited | {w}
|
|
559
|
+
for w2 in {b for (a, b) in edges if a == w}:
|
|
560
|
+
if w2 not in next_visited and search(w2, next_visited):
|
|
561
|
+
return True
|
|
562
|
+
return False
|
|
563
|
+
|
|
564
|
+
return search(world, frozenset())
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _predecessors(model: KripkeModel, name: str, world: World) -> Set[World]:
|
|
568
|
+
"""Return the set of ``w'`` with ``(w', world)`` in the named relation (its converse successors)."""
|
|
569
|
+
return {w1 for (w1, w2) in model.relation(name) if w2 == world}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _since_holds(
|
|
573
|
+
left: Node,
|
|
574
|
+
right: Node,
|
|
575
|
+
model: KripkeModel,
|
|
576
|
+
world: World,
|
|
577
|
+
) -> bool:
|
|
578
|
+
"""Decide ``Since(left, right)`` at ``world`` — the backward mirror of Until.
|
|
579
|
+
|
|
580
|
+
Searches for a finite ``"temporal"`` path into the PAST
|
|
581
|
+
``world = w0 ← w1 ← … ← wn`` (each step ``(w(i+1), wi)`` a temporal edge, n ≥ 0)
|
|
582
|
+
with ``right`` true at ``wn`` and ``left`` true at every later ``wi`` (i < n). A
|
|
583
|
+
depth-first search guarded by a visited set, so cyclic frames terminate.
|
|
584
|
+
"""
|
|
585
|
+
edges = model.relation(_TEMPORAL)
|
|
586
|
+
|
|
587
|
+
def search(w: World, visited: FrozenSet[World]) -> bool:
|
|
588
|
+
"""Return whether some backward path from ``w`` witnesses the Since."""
|
|
589
|
+
if satisfies_modal(right, model, w):
|
|
590
|
+
return True
|
|
591
|
+
if not satisfies_modal(left, model, w):
|
|
592
|
+
return False
|
|
593
|
+
next_visited = visited | {w}
|
|
594
|
+
for w0 in {a for (a, b) in edges if b == w}:
|
|
595
|
+
if w0 not in next_visited and search(w0, next_visited):
|
|
596
|
+
return True
|
|
597
|
+
return False
|
|
598
|
+
|
|
599
|
+
return search(world, frozenset())
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _nominal_world(model: KripkeModel, name: str) -> World:
|
|
603
|
+
"""Return the world the nominal ``name`` names; raise if it is unassigned.
|
|
604
|
+
|
|
605
|
+
A nominal must name exactly one world, so an assignment-free nominal is a
|
|
606
|
+
modelling error — the ValueError names the offending nominal and shows the
|
|
607
|
+
``nominals=`` fix rather than silently picking a truth value.
|
|
608
|
+
"""
|
|
609
|
+
if name not in model.nominals:
|
|
610
|
+
raise ValueError(
|
|
611
|
+
f"satisfies_modal: the nominal {name!r} has no world assignment in "
|
|
612
|
+
f"this model — build the KripkeModel with nominals={{{name!r}: world}}."
|
|
613
|
+
)
|
|
614
|
+
return model.nominals[name]
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def satisfies_modal(formula: Node, model: KripkeModel, world: World) -> bool:
|
|
618
|
+
"""Return whether ``formula`` is true at ``world`` in the Kripke ``model``.
|
|
619
|
+
|
|
620
|
+
The Kripke satisfaction relation for the propositional / ground modal
|
|
621
|
+
fragment:
|
|
622
|
+
|
|
623
|
+
- ``Atom`` — its Unicode key is in the world's valuation.
|
|
624
|
+
- ``Nominal i`` — true iff ``world`` IS the world ``model.nominals[i]``
|
|
625
|
+
names (a nominal holds at exactly one world).
|
|
626
|
+
- ``At(i, φ)`` — φ holds at the world named ``i``, regardless of the
|
|
627
|
+
current world (the hybrid satisfaction operator ``@i φ``).
|
|
628
|
+
- ``Not / And / Or / Xor / Implies / Iff`` — the classical truth tables,
|
|
629
|
+
recursing at the **same** world.
|
|
630
|
+
- ``Box φ`` — φ holds at every ``"alethic"``-successor; ``Diamond φ`` — at
|
|
631
|
+
some ``"alethic"``-successor.
|
|
632
|
+
- ``Knows(a, φ)`` — φ holds at every ``"K:"+a``-successor (universal).
|
|
633
|
+
- ``Believes(a, φ)`` — φ holds at every ``"B:"+a``-successor (universal).
|
|
634
|
+
- ``EverybodyKnows(G, φ)`` (E_G φ) / ``DistributedKnowledge(G, φ)``
|
|
635
|
+
(D_G φ) / ``CommonKnowledge(G, φ)`` (C_G φ) — dispatched to
|
|
636
|
+
:func:`~unicode_logic_kit.semantics.action_models.everybody_knows` /
|
|
637
|
+
:func:`~unicode_logic_kit.semantics.action_models.distributed_knowledge_holds`
|
|
638
|
+
/ :func:`~unicode_logic_kit.semantics.action_models.common_knowledge_holds`
|
|
639
|
+
(see the module docstring).
|
|
640
|
+
- ``Obligatory φ`` — φ holds at every ``"deontic"``-successor (universal);
|
|
641
|
+
``Permitted φ`` — at some ``"deontic"``-successor.
|
|
642
|
+
- ``Next φ`` — φ holds at every immediate ``"temporal"``-successor.
|
|
643
|
+
- ``Always φ`` / ``Eventually φ`` — φ holds at all / some worlds in the
|
|
644
|
+
reflexive-transitive closure of ``"temporal"`` from ``world``.
|
|
645
|
+
- ``Until(φ, ψ)`` — see :func:`_until_holds` (finite-path strong Until).
|
|
646
|
+
- ``Announce(φ, ψ)`` (``[φ!]ψ``) — ``φ`` false at ``world`` makes this
|
|
647
|
+
vacuously true; otherwise ``ψ`` must hold at ``world`` in the model
|
|
648
|
+
restricted to the ``φ``-worlds (:func:`~unicode_logic_kit.semantics.dynamic_epistemic.announce`).
|
|
649
|
+
- ``AnnounceDiamond(φ, ψ)`` (``⟨φ!⟩ψ``) — the dual: ``φ`` true at ``world``
|
|
650
|
+
AND ``ψ`` holds at ``world`` in the ``φ``-restricted model.
|
|
651
|
+
|
|
652
|
+
A many-sorted ``formula`` (``SortedQuantifier`` / ``SortedConstant``, ``∀x:S φ``
|
|
653
|
+
/ ``∃x:S φ`` / a bare ``alice:Human``) is relativized ONCE, here, before
|
|
654
|
+
anything else runs — see the module docstring's "Many-sorted formulas"
|
|
655
|
+
section for what that does and does not assume (world-relative, not rigid;
|
|
656
|
+
non-empty only if the model says so; a sorted constant is in its sort only
|
|
657
|
+
if the model puts ``S(c)`` in every world's valuation — see
|
|
658
|
+
:func:`sorted_constant_violations`).
|
|
659
|
+
|
|
660
|
+
Raises:
|
|
661
|
+
NotImplementedError: on a Łukasiewicz node or a lambda node (checked
|
|
662
|
+
BEFORE relativizing, since relativizing runs a whole-tree
|
|
663
|
+
structural recursion that would otherwise reach one of these
|
|
664
|
+
first and raise a less specific error), and on an equality /
|
|
665
|
+
disequality atom (``=`` / ``≠``) anywhere in the formula — see the
|
|
666
|
+
module docstring's "Equality is NOT interpreted" section.
|
|
667
|
+
"""
|
|
668
|
+
# --- many-sorted formulas: relativize the WHOLE formula once, here, before
|
|
669
|
+
# any dispatch below — see the docstring above and the module docstring's
|
|
670
|
+
# "Many-sorted formulas" section. Reject any Łukasiewicz / lambda node
|
|
671
|
+
# FIRST, by walking the whole (pre-relativize) tree: Node._relativize is
|
|
672
|
+
# an unconditional structural descent into every child (Node.map_children),
|
|
673
|
+
# so if a fuzzy/lambda node sat anywhere in the formula -- not only at the
|
|
674
|
+
# very top -- relativizing before this check would reach it first and
|
|
675
|
+
# raise a generic RuntimeError ("call to_msfol() before _relativize")
|
|
676
|
+
# instead of this function's own documented NotImplementedError contract
|
|
677
|
+
# (see test_fuzzy_node_rejected / test_lambda_node_rejected). ---
|
|
678
|
+
for node in formula.walk():
|
|
679
|
+
if isinstance(node, FUZZY_TYPES):
|
|
680
|
+
reject_fuzzy(node, "satisfies_modal")
|
|
681
|
+
if isinstance(node, LAMBDA_TYPES):
|
|
682
|
+
reject_lambda(node, "satisfies_modal")
|
|
683
|
+
# Equality has no interpretation here (module docstring); refuse it by
|
|
684
|
+
# name from the WHOLE tree, up front, so no short-circuit or vacuous
|
|
685
|
+
# branch can let an equality atom through unexamined.
|
|
686
|
+
reject_equality(node, "satisfies_modal")
|
|
687
|
+
formula = formula._relativize([])
|
|
688
|
+
|
|
689
|
+
# --- atomic ---
|
|
690
|
+
if isinstance(formula, Atom):
|
|
691
|
+
constant = _truth_value(formula)
|
|
692
|
+
if constant is not None:
|
|
693
|
+
return constant # `$true` / `$false`: the same at every world
|
|
694
|
+
# The key of the atom first, then its formula text: an element ``a`` of a domain is
|
|
695
|
+
# substituted as ``Constant("a")``, which the formula text writes ``P('a')``, and the
|
|
696
|
+
# valuation holds the key the user typed, ``P(a)``, or the text of the atom as a
|
|
697
|
+
# formula, ``P('a')``, which the guide taught as the key of a hand-built atom.
|
|
698
|
+
return find_key(model.atoms_true_at(world), formula) is not None
|
|
699
|
+
|
|
700
|
+
# --- hybrid: a nominal is true exactly at the world it names; @ jumps there ---
|
|
701
|
+
if isinstance(formula, Nominal):
|
|
702
|
+
return world == _nominal_world(model, formula.name)
|
|
703
|
+
if isinstance(formula, At):
|
|
704
|
+
return satisfies_modal(formula.formula, model,
|
|
705
|
+
_nominal_world(model, formula.nominal.name))
|
|
706
|
+
if isinstance(formula, Down):
|
|
707
|
+
# ↓x.φ at world: locally rebind the nominal x to THIS world for φ's
|
|
708
|
+
# evaluation (still at the same world) — see the module docstring's
|
|
709
|
+
# "The ↓ binder" section for why this plain dict-override, with no
|
|
710
|
+
# separate alpha-renaming step, already gets shadowing / no-capture /
|
|
711
|
+
# rigidity right. KripkeModel's own constructor re-validates the
|
|
712
|
+
# (harmless, since world ∈ model.worlds whenever a caller reached
|
|
713
|
+
# here in the first place) new nominal assignment.
|
|
714
|
+
rebound = KripkeModel(
|
|
715
|
+
model.worlds, model.relations, model.valuation,
|
|
716
|
+
domains=model.domains,
|
|
717
|
+
nominals={**model.nominals, formula.variable.name: world},
|
|
718
|
+
)
|
|
719
|
+
return satisfies_modal(formula.formula, rebound, world)
|
|
720
|
+
|
|
721
|
+
# --- classical connectives (recurse at the same world) ---
|
|
722
|
+
if isinstance(formula, Not):
|
|
723
|
+
return not satisfies_modal(formula.formula, model, world)
|
|
724
|
+
if isinstance(formula, And):
|
|
725
|
+
return (satisfies_modal(formula.left, model, world)
|
|
726
|
+
and satisfies_modal(formula.right, model, world))
|
|
727
|
+
if isinstance(formula, Or):
|
|
728
|
+
return (satisfies_modal(formula.left, model, world)
|
|
729
|
+
or satisfies_modal(formula.right, model, world))
|
|
730
|
+
if isinstance(formula, Xor):
|
|
731
|
+
return (satisfies_modal(formula.left, model, world)
|
|
732
|
+
!= satisfies_modal(formula.right, model, world))
|
|
733
|
+
if isinstance(formula, Implies):
|
|
734
|
+
return ((not satisfies_modal(formula.left, model, world))
|
|
735
|
+
or satisfies_modal(formula.right, model, world))
|
|
736
|
+
if isinstance(formula, Iff):
|
|
737
|
+
return (satisfies_modal(formula.left, model, world)
|
|
738
|
+
== satisfies_modal(formula.right, model, world))
|
|
739
|
+
|
|
740
|
+
# --- alethic ---
|
|
741
|
+
if isinstance(formula, Box):
|
|
742
|
+
return all(
|
|
743
|
+
satisfies_modal(formula.formula, model, w2)
|
|
744
|
+
for w2 in model.successors(_ALETHIC, world)
|
|
745
|
+
)
|
|
746
|
+
if isinstance(formula, Diamond):
|
|
747
|
+
return any(
|
|
748
|
+
satisfies_modal(formula.formula, model, w2)
|
|
749
|
+
for w2 in model.successors(_ALETHIC, world)
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# --- epistemic / doxastic (both universal) ---
|
|
753
|
+
if isinstance(formula, Knows):
|
|
754
|
+
return all(
|
|
755
|
+
satisfies_modal(formula.formula, model, w2)
|
|
756
|
+
for w2 in model.successors(_KNOWS_PREFIX + _agent_key(formula.agent), world)
|
|
757
|
+
)
|
|
758
|
+
if isinstance(formula, Believes):
|
|
759
|
+
return all(
|
|
760
|
+
satisfies_modal(formula.formula, model, w2)
|
|
761
|
+
for w2 in model.successors(_BELIEVES_PREFIX + _agent_key(formula.agent), world)
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# --- group epistemic (everyone/distributed/common knowledge): thin
|
|
765
|
+
# dispatch into semantics.action_models, exactly like Announce dispatches
|
|
766
|
+
# into semantics.dynamic_epistemic.announce below. Lazy import: action_models
|
|
767
|
+
# imports THIS module at load time (KripkeModel/satisfies_modal/
|
|
768
|
+
# reflexive_transitive_closure), so a module-level import here would cycle. ---
|
|
769
|
+
if isinstance(formula, EverybodyKnows):
|
|
770
|
+
from .action_models import everybody_knows
|
|
771
|
+
agents = [_agent_key(a) for a in formula.group]
|
|
772
|
+
return everybody_knows(model, world, agents, formula.formula)
|
|
773
|
+
if isinstance(formula, DistributedKnowledge):
|
|
774
|
+
from .action_models import distributed_knowledge_holds
|
|
775
|
+
agents = [_agent_key(a) for a in formula.group]
|
|
776
|
+
return distributed_knowledge_holds(model, world, agents, formula.formula)
|
|
777
|
+
if isinstance(formula, CommonKnowledge):
|
|
778
|
+
from .action_models import common_knowledge_holds
|
|
779
|
+
agents = [_agent_key(a) for a in formula.group]
|
|
780
|
+
return common_knowledge_holds(model, world, agents, formula.formula)
|
|
781
|
+
|
|
782
|
+
# --- assertive / bouletic (both universal K-modalities, no frame conditions:
|
|
783
|
+
# Says is non-factive / non-doxastic, Wants is non-veridical) ---
|
|
784
|
+
if isinstance(formula, Says):
|
|
785
|
+
return all(
|
|
786
|
+
satisfies_modal(formula.formula, model, w2)
|
|
787
|
+
for w2 in model.successors(_SAYS_PREFIX + _agent_key(formula.agent), world)
|
|
788
|
+
)
|
|
789
|
+
if isinstance(formula, Wants):
|
|
790
|
+
return all(
|
|
791
|
+
satisfies_modal(formula.formula, model, w2)
|
|
792
|
+
for w2 in model.successors(_WANTS_PREFIX + _agent_key(formula.agent), world)
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# --- deontic (Standard Deontic Logic / KD over a serial "deontic" relation) ---
|
|
796
|
+
if isinstance(formula, Obligatory):
|
|
797
|
+
return all(
|
|
798
|
+
satisfies_modal(formula.formula, model, w2)
|
|
799
|
+
for w2 in model.successors(_DEONTIC, world)
|
|
800
|
+
)
|
|
801
|
+
if isinstance(formula, Permitted):
|
|
802
|
+
return any(
|
|
803
|
+
satisfies_modal(formula.formula, model, w2)
|
|
804
|
+
for w2 in model.successors(_DEONTIC, world)
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# --- temporal ---
|
|
808
|
+
if isinstance(formula, Next):
|
|
809
|
+
return all(
|
|
810
|
+
satisfies_modal(formula.formula, model, w2)
|
|
811
|
+
for w2 in model.successors(_TEMPORAL, world)
|
|
812
|
+
)
|
|
813
|
+
if isinstance(formula, Always):
|
|
814
|
+
reachable = reflexive_transitive_closure(model.relation(_TEMPORAL), [world])
|
|
815
|
+
return all(
|
|
816
|
+
satisfies_modal(formula.formula, model, w2) for w2 in reachable
|
|
817
|
+
)
|
|
818
|
+
if isinstance(formula, Eventually):
|
|
819
|
+
reachable = reflexive_transitive_closure(model.relation(_TEMPORAL), [world])
|
|
820
|
+
return any(
|
|
821
|
+
satisfies_modal(formula.formula, model, w2) for w2 in reachable
|
|
822
|
+
)
|
|
823
|
+
if isinstance(formula, Until):
|
|
824
|
+
return _until_holds(formula.left, formula.right, model, world)
|
|
825
|
+
|
|
826
|
+
# --- past tense (over the CONVERSE of the one-step "temporal" relation) ---
|
|
827
|
+
if isinstance(formula, Previous):
|
|
828
|
+
return all(
|
|
829
|
+
satisfies_modal(formula.formula, model, w2)
|
|
830
|
+
for w2 in _predecessors(model, _TEMPORAL, world)
|
|
831
|
+
)
|
|
832
|
+
if isinstance(formula, Historically):
|
|
833
|
+
reverse = [(b, a) for (a, b) in model.relation(_TEMPORAL)]
|
|
834
|
+
reachable = reflexive_transitive_closure(reverse, [world])
|
|
835
|
+
return all(satisfies_modal(formula.formula, model, w2) for w2 in reachable)
|
|
836
|
+
if isinstance(formula, Once):
|
|
837
|
+
reverse = [(b, a) for (a, b) in model.relation(_TEMPORAL)]
|
|
838
|
+
reachable = reflexive_transitive_closure(reverse, [world])
|
|
839
|
+
return any(satisfies_modal(formula.formula, model, w2) for w2 in reachable)
|
|
840
|
+
if isinstance(formula, Since):
|
|
841
|
+
return _since_holds(formula.left, formula.right, model, world)
|
|
842
|
+
|
|
843
|
+
# --- public announcement logic (PAL): a genuine MODEL UPDATE, not a fixed
|
|
844
|
+
# accessibility relation — this is the ORACLE unicode_logic_kit.fol.pal
|
|
845
|
+
# .reduce_announcements is differentially tested against (see that module's
|
|
846
|
+
# docstring for the correctness argument relating the two). Both build the
|
|
847
|
+
# restricted model M|announcement via
|
|
848
|
+
# unicode_logic_kit.semantics.dynamic_epistemic.announce (imported lazily to
|
|
849
|
+
# avoid a circular import: dynamic_epistemic imports THIS module at load
|
|
850
|
+
# time) and recurse into ``formula.formula`` there, at the SAME world. ---
|
|
851
|
+
if isinstance(formula, Announce):
|
|
852
|
+
if not satisfies_modal(formula.announcement, model, world):
|
|
853
|
+
return True # untruthful announcement is not made: vacuously true
|
|
854
|
+
from .dynamic_epistemic import announce # lazy: avoid import cycle
|
|
855
|
+
return satisfies_modal(formula.formula, announce(model, formula.announcement), world)
|
|
856
|
+
if isinstance(formula, AnnounceDiamond):
|
|
857
|
+
if not satisfies_modal(formula.announcement, model, world):
|
|
858
|
+
return False # dual of Announce: false whenever the announcement is
|
|
859
|
+
from .dynamic_epistemic import announce # lazy: avoid import cycle
|
|
860
|
+
return satisfies_modal(formula.formula, announce(model, formula.announcement), world)
|
|
861
|
+
|
|
862
|
+
# --- object quantifiers (actualist: range over the CURRENT world's domain D_w) ---
|
|
863
|
+
if isinstance(formula, Quantifier):
|
|
864
|
+
individuals = model.domain_at(world)
|
|
865
|
+
instances = (
|
|
866
|
+
satisfies_modal(substitute(formula.formula, formula.variable, Constant(d)),
|
|
867
|
+
model, world)
|
|
868
|
+
for d in individuals
|
|
869
|
+
)
|
|
870
|
+
if formula.type in _FORALL:
|
|
871
|
+
return all(instances)
|
|
872
|
+
if formula.type in _EXISTS:
|
|
873
|
+
return any(instances)
|
|
874
|
+
raise ValueError(f"satisfies_modal: unknown quantifier type {formula.type!r}")
|
|
875
|
+
|
|
876
|
+
# NOTE: SortedQuantifier / SortedConstant never reach this dispatch chain --
|
|
877
|
+
# the preamble above relativizes the whole formula before any isinstance
|
|
878
|
+
# check runs, so by this point the tree contains only plain Quantifier /
|
|
879
|
+
# Constant nodes (see the module docstring's "Many-sorted formulas"
|
|
880
|
+
# section for what the guarded-Quantifier reduction does and does not
|
|
881
|
+
# assume: world-relative sort guards, non-emptiness and the membership of a
|
|
882
|
+
# sorted constant left to the caller).
|
|
883
|
+
|
|
884
|
+
# Łukasiewicz / lambda nodes were already rejected in the preamble above
|
|
885
|
+
# (deep scan, before relativizing); anything reaching here is a genuinely
|
|
886
|
+
# unhandled node type.
|
|
887
|
+
raise NotImplementedError(
|
|
888
|
+
f"satisfies_modal: unsupported node type {type(formula).__name__}."
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def models_at(formula: Node, model: KripkeModel, world: World) -> bool:
|
|
893
|
+
"""Convenience alias for :func:`satisfies_modal` reading "model, world ⊨ φ"."""
|
|
894
|
+
return satisfies_modal(formula, model, world)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def sorted_constant_violations(formula: Node,
|
|
898
|
+
model: KripkeModel) -> List[Tuple[str, str, World]]:
|
|
899
|
+
"""The worlds of ``model`` at which a sorted constant of ``formula`` is NOT in its sort.
|
|
900
|
+
|
|
901
|
+
``c:S`` denotes an element of ``S`` and a constant is a rigid designator, so a
|
|
902
|
+
model is LEGAL for ``formula`` only if the guard atom ``S(c)`` is in the
|
|
903
|
+
valuation of every world, for every distinct ``c:S`` of ``formula`` (see the
|
|
904
|
+
module docstring: :func:`satisfies_modal` evaluates one given model and does
|
|
905
|
+
not assert the fact itself). Pass the ORIGINAL formula — ``c:S`` is gone once
|
|
906
|
+
the formula is relativized.
|
|
907
|
+
|
|
908
|
+
Returns ``(constant, sort, world)`` triples, one per sorted constant and per
|
|
909
|
+
world whose valuation lacks the key ``S(c)``, constants in first-occurrence
|
|
910
|
+
order and worlds in ``repr`` order; ``[]`` means the model is legal for the
|
|
911
|
+
formula's sorted constants (and trivially so for a formula without any).
|
|
912
|
+
Non-emptiness of a sort is a different fact, left to the caller as before.
|
|
913
|
+
"""
|
|
914
|
+
violations: List[Tuple[str, str, World]] = []
|
|
915
|
+
worlds = sorted(model.worlds, key=repr)
|
|
916
|
+
for atom in sort_membership_axioms(formula):
|
|
917
|
+
# every member is the guard atom ``S(c)`` over the one constant ``c``
|
|
918
|
+
assert isinstance(atom, Atom)
|
|
919
|
+
constant = atom.args[0]
|
|
920
|
+
assert isinstance(constant, Constant)
|
|
921
|
+
for world in worlds:
|
|
922
|
+
if find_key(model.atoms_true_at(world), atom) is None:
|
|
923
|
+
violations.append((constant.name, atom.predicate, world))
|
|
924
|
+
return violations
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
# ---------------------------------------------------------------------------
|
|
928
|
+
# CTL (branching-time) model checking: EX / AF / EG / AU
|
|
929
|
+
# ---------------------------------------------------------------------------
|
|
930
|
+
#
|
|
931
|
+
# Next / Always / Eventually / Until (documented above) are LINEAR-time
|
|
932
|
+
# operators, each with exactly one path reading baked into its own AST node
|
|
933
|
+
# (fol/_modal_nodes.py): Next and Always read universally (every successor /
|
|
934
|
+
# every reachable world), Eventually and Until existentially (some reachable
|
|
935
|
+
# world / some finite witnessing path). There is no A/E path-quantifier
|
|
936
|
+
# prefix anywhere in the AST, so the four functions below are plain
|
|
937
|
+
# functions — not new isinstance branches in satisfies_modal's dispatch —
|
|
938
|
+
# exactly like common_knowledge_holds / everybody_knows in
|
|
939
|
+
# semantics/action_models.py: arbitrary Node subformulas evaluated via
|
|
940
|
+
# satisfies_modal, no new AST node type and no grammar/parser change.
|
|
941
|
+
#
|
|
942
|
+
# - ctl_ex is the existential dual of the built-in (universal) Next: a plain
|
|
943
|
+
# one-step modality, no fixpoint needed.
|
|
944
|
+
# - ctl_af/ctl_eg are the missing AF/EG dual pair. Note AF is NOT the same
|
|
945
|
+
# computation as reflexive_transitive_closure: a world can satisfy "phi
|
|
946
|
+
# holds on every path" without phi holding at every REACHABLE world (some
|
|
947
|
+
# paths from it may loop back through phi-free territory before others
|
|
948
|
+
# reach a phi-world), so a genuine forward least-fixpoint is needed, not a
|
|
949
|
+
# closure/reachability walk.
|
|
950
|
+
# - ctl_au is the universal-path generalisation of the built-in (existential)
|
|
951
|
+
# Until above.
|
|
952
|
+
#
|
|
953
|
+
# All three fixpoint functions (AF/EG/AU) quantify over the "temporal"
|
|
954
|
+
# relation RESTRICTED to model.worlds: their state space IS model.worlds, so
|
|
955
|
+
# an edge into a world outside it cannot be a step of the fixpoint — unlike
|
|
956
|
+
# Next/Always/Eventually/Until above, which follow model.successors() /
|
|
957
|
+
# model.relation() exactly as given, with no membership filter.
|
|
958
|
+
#
|
|
959
|
+
# Algorithm (standard CTL labelling via fixpoint iteration on the finite
|
|
960
|
+
# temporal relation restricted to model.worlds — Baier & Katoen, Principles of
|
|
961
|
+
# Model Checking, MIT Press 2008, §6.4; Clarke, Grumberg & Peled, Model
|
|
962
|
+
# Checking, MIT Press 1999, ch. 6): each iteration is monotone on the finite
|
|
963
|
+
# set model.worlds, so it reaches a fixpoint in at most |model.worlds| rounds.
|
|
964
|
+
# No external library and no Tarjan/SCC machinery is required for correctness
|
|
965
|
+
# (SCC detection would only be an internal optimisation, and none is used
|
|
966
|
+
# here).
|
|
967
|
+
|
|
968
|
+
def _ctl_temporal_successors(model: "KripkeModel", world: World) -> Set[World]:
|
|
969
|
+
"""Temporal successors of ``world`` that are themselves in ``model.worlds``.
|
|
970
|
+
|
|
971
|
+
The CTL fixpoints below (:func:`ctl_af`, :func:`ctl_eg`, :func:`ctl_au`)
|
|
972
|
+
iterate over ``model.worlds`` as their state space, so a temporal edge
|
|
973
|
+
into a world outside it is not a step of any of those fixpoints — unlike
|
|
974
|
+
``Next``/``Always``/``Eventually``/``Until`` above, which follow
|
|
975
|
+
``model.successors``/``model.relation`` exactly as given, with no
|
|
976
|
+
membership filter. :func:`ctl_ex` (the one CTL function below that is a
|
|
977
|
+
plain one-step modality, not a path fixpoint) intentionally does NOT use
|
|
978
|
+
this helper — see its own docstring.
|
|
979
|
+
"""
|
|
980
|
+
return {w2 for w2 in model.successors(_TEMPORAL, world) if w2 in model.worlds}
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _require_total_temporal(model: "KripkeModel") -> None:
|
|
984
|
+
"""Raise ValueError unless every world of ``model.worlds`` has a temporal
|
|
985
|
+
successor inside ``model.worlds``.
|
|
986
|
+
|
|
987
|
+
``ctl_af``/``ctl_eg``/``ctl_au`` quantify over INFINITE ``"temporal"``
|
|
988
|
+
paths. A world with no successor inside ``model.worlds`` is a dead end no
|
|
989
|
+
infinite path can pass through — treating it the way ``Next`` treats a
|
|
990
|
+
dead end (AX-anything vacuously true) would silently make AF/EG/AU wrong
|
|
991
|
+
there, exactly the kind of silent wrongness the kit's "refuse loudly,
|
|
992
|
+
never approximate" rule exists to prevent (the same discipline
|
|
993
|
+
``product_update`` follows for a missing agent relation in
|
|
994
|
+
``action_models.py``). So instead of picking a convention, this requires
|
|
995
|
+
the frame to be total on ``model.worlds`` and names the (deterministically
|
|
996
|
+
first, by ``repr()``, so the message is reproducible) offending world.
|
|
997
|
+
|
|
998
|
+
``ctl_ex`` needs no such check — it is a genuine one-step modality, as
|
|
999
|
+
vacuously-decided at a dead end as the existing universal ``Next``, see
|
|
1000
|
+
its own docstring.
|
|
1001
|
+
"""
|
|
1002
|
+
dead_ends = sorted(
|
|
1003
|
+
(w for w in model.worlds if not _ctl_temporal_successors(model, w)),
|
|
1004
|
+
key=repr,
|
|
1005
|
+
)
|
|
1006
|
+
if dead_ends:
|
|
1007
|
+
raise ValueError(
|
|
1008
|
+
"ctl_af/ctl_eg/ctl_au: the \"temporal\" relation is not total on "
|
|
1009
|
+
f"model.worlds — world {dead_ends[0]!r} has no temporal successor "
|
|
1010
|
+
"inside model.worlds, so no infinite path passes through it. Add "
|
|
1011
|
+
"a temporal successor within model.worlds (a self-loop is the "
|
|
1012
|
+
"usual fix for a terminal/sink state) to make the frame total, "
|
|
1013
|
+
"or use ctl_ex there instead, which needs no totality."
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def ctl_ex(model: KripkeModel, world: World, formula: Node) -> bool:
|
|
1018
|
+
"""Return whether EX φ ("some immediate successor satisfies φ") holds at ``world``.
|
|
1019
|
+
|
|
1020
|
+
The existential dual of the kit's built-in ``Next`` (which reads its one
|
|
1021
|
+
AST node universally, i.e. AX — see the module docstring). A plain
|
|
1022
|
+
one-step modality: no fixpoint, and no totality requirement. Exactly like
|
|
1023
|
+
``Next``, a world with no ``"temporal"``-successor is simply decided by
|
|
1024
|
+
the empty case — ``Next``'s empty ``all(...)`` is vacuously TRUE there,
|
|
1025
|
+
this function's empty ``any(...)`` is FALSE there — not an error.
|
|
1026
|
+
|
|
1027
|
+
Also follows ``Next``'s convention of reading ``model.successors``
|
|
1028
|
+
UNFILTERED: a successor outside ``model.worlds`` is still a witness here
|
|
1029
|
+
(exactly as it is for ``Next``), unlike :func:`ctl_af`/:func:`ctl_eg`/
|
|
1030
|
+
:func:`ctl_au` below, which restrict to ``model.worlds`` because they run
|
|
1031
|
+
a fixpoint over it.
|
|
1032
|
+
|
|
1033
|
+
Raises:
|
|
1034
|
+
NotImplementedError: an equality / disequality atom (``=`` / ``≠``)
|
|
1035
|
+
anywhere in ``formula`` — refused up front, so a world with no
|
|
1036
|
+
successor (where the ``any(...)`` below would never look at
|
|
1037
|
+
``formula``) refuses it too; see the module docstring.
|
|
1038
|
+
"""
|
|
1039
|
+
reject_equality_in(formula, "ctl_ex")
|
|
1040
|
+
return any(
|
|
1041
|
+
satisfies_modal(formula, model, w2)
|
|
1042
|
+
for w2 in model.successors(_TEMPORAL, world)
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
def ctl_af(model: KripkeModel, world: World, formula: Node) -> bool:
|
|
1047
|
+
"""Return whether AF φ ("φ eventually holds, on every path") holds at ``world``.
|
|
1048
|
+
|
|
1049
|
+
The least fixpoint μZ. φ ∨ AX Z (Baier & Katoen §6.4), computed by
|
|
1050
|
+
forward iteration from Z = ∅: a world enters Z as soon as φ holds there,
|
|
1051
|
+
or every one of its ``model.worlds``-internal temporal successors is
|
|
1052
|
+
already in Z. Z grows monotonically on the finite set ``model.worlds``,
|
|
1053
|
+
so the loop reaches a fixpoint in at most ``|model.worlds|`` rounds.
|
|
1054
|
+
|
|
1055
|
+
Raises:
|
|
1056
|
+
NotImplementedError: an equality / disequality atom (``=`` / ``≠``)
|
|
1057
|
+
anywhere in ``formula`` — see the module docstring.
|
|
1058
|
+
ValueError: the ``"temporal"`` relation is not total on
|
|
1059
|
+
``model.worlds`` — see :func:`_require_total_temporal`. Without
|
|
1060
|
+
totality, "every successor of w is in Z" would hold vacuously at
|
|
1061
|
+
a dead end and silently pull dead ends into AF's least fixpoint
|
|
1062
|
+
for the wrong reason.
|
|
1063
|
+
"""
|
|
1064
|
+
reject_equality_in(formula, "ctl_af")
|
|
1065
|
+
_require_total_temporal(model)
|
|
1066
|
+
phi_worlds = {w for w in model.worlds if satisfies_modal(formula, model, w)}
|
|
1067
|
+
z: Set[World] = set()
|
|
1068
|
+
while True:
|
|
1069
|
+
new_z = set(phi_worlds)
|
|
1070
|
+
for w in model.worlds:
|
|
1071
|
+
if w not in new_z and _ctl_temporal_successors(model, w) <= z:
|
|
1072
|
+
new_z.add(w)
|
|
1073
|
+
if new_z == z:
|
|
1074
|
+
return world in z
|
|
1075
|
+
z = new_z
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def ctl_eg(model: KripkeModel, world: World, formula: Node) -> bool:
|
|
1079
|
+
"""Return whether EG φ ("φ holds forever, on some path") holds at ``world``.
|
|
1080
|
+
|
|
1081
|
+
The greatest fixpoint νZ. φ ∧ EX Z (Baier & Katoen §6.4), computed by
|
|
1082
|
+
shrinking from Z = {w : φ holds at w}: a world leaves Z as soon as NONE
|
|
1083
|
+
of its ``model.worlds``-internal temporal successors is still in Z. Z
|
|
1084
|
+
shrinks monotonically on the finite set ``model.worlds``, so the loop
|
|
1085
|
+
reaches a fixpoint in at most ``|model.worlds|`` rounds.
|
|
1086
|
+
|
|
1087
|
+
Raises:
|
|
1088
|
+
ValueError: the ``"temporal"`` relation is not total on
|
|
1089
|
+
``model.worlds`` — see :func:`_require_total_temporal`. This
|
|
1090
|
+
batch's deadlock convention requires totality uniformly across
|
|
1091
|
+
``ctl_af``/``ctl_eg``/``ctl_au`` (even though EG's own greatest
|
|
1092
|
+
fixpoint would, left alone, correctly drop a dead end out of Z on
|
|
1093
|
+
its own on the first iteration): a dead end is a modelling error
|
|
1094
|
+
to be reported the same way by all three, not something EG alone
|
|
1095
|
+
quietly special-cases while its siblings refuse it.
|
|
1096
|
+
NotImplementedError: an equality / disequality atom (``=`` / ``≠``)
|
|
1097
|
+
anywhere in ``formula`` — see the module docstring.
|
|
1098
|
+
"""
|
|
1099
|
+
reject_equality_in(formula, "ctl_eg")
|
|
1100
|
+
_require_total_temporal(model)
|
|
1101
|
+
z = {w for w in model.worlds if satisfies_modal(formula, model, w)}
|
|
1102
|
+
while True:
|
|
1103
|
+
new_z = {w for w in z if _ctl_temporal_successors(model, w) & z}
|
|
1104
|
+
if new_z == z:
|
|
1105
|
+
return world in z
|
|
1106
|
+
z = new_z
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def ctl_au(model: KripkeModel, world: World, phi: Node, psi: Node) -> bool:
|
|
1110
|
+
"""Return whether A[φ U ψ] ("φ holds until ψ, on every path") holds at ``world``.
|
|
1111
|
+
|
|
1112
|
+
The universal-path generalisation of the kit's built-in (existential)
|
|
1113
|
+
``Until`` above. The least fixpoint μZ. ψ ∨ (φ ∧ AX Z) (Baier & Katoen
|
|
1114
|
+
§6.4), computed by forward iteration from Z = ∅ exactly like
|
|
1115
|
+
:func:`ctl_af`: a world enters Z as soon as ψ holds there, or φ holds
|
|
1116
|
+
there AND every one of its ``model.worlds``-internal temporal successors
|
|
1117
|
+
is already in Z.
|
|
1118
|
+
|
|
1119
|
+
Raises:
|
|
1120
|
+
NotImplementedError: an equality / disequality atom (``=`` / ``≠``)
|
|
1121
|
+
anywhere in ``phi`` or ``psi`` — see the module docstring.
|
|
1122
|
+
ValueError: the ``"temporal"`` relation is not total on
|
|
1123
|
+
``model.worlds`` — see :func:`_require_total_temporal`.
|
|
1124
|
+
"""
|
|
1125
|
+
reject_equality_in(phi, "ctl_au")
|
|
1126
|
+
reject_equality_in(psi, "ctl_au")
|
|
1127
|
+
_require_total_temporal(model)
|
|
1128
|
+
phi_worlds = {w for w in model.worlds if satisfies_modal(phi, model, w)}
|
|
1129
|
+
psi_worlds = {w for w in model.worlds if satisfies_modal(psi, model, w)}
|
|
1130
|
+
z: Set[World] = set()
|
|
1131
|
+
while True:
|
|
1132
|
+
new_z = set(psi_worlds)
|
|
1133
|
+
for w in model.worlds:
|
|
1134
|
+
if (w not in new_z and w in phi_worlds
|
|
1135
|
+
and _ctl_temporal_successors(model, w) <= z):
|
|
1136
|
+
new_z.add(w)
|
|
1137
|
+
if new_z == z:
|
|
1138
|
+
return world in z
|
|
1139
|
+
z = new_z
|