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,1382 @@
|
|
|
1
|
+
"""Labelled analytic tableaux for the propositional modal family.
|
|
2
|
+
|
|
3
|
+
The classical :mod:`unicode_logic_kit.atp.tableau` engine has no rule for a modal
|
|
4
|
+
operator — a ``Box`` / ``Knows`` / ``Obligatory`` node makes it raise. This module
|
|
5
|
+
fills that gap with a **labelled** (world-prefixed) tableau: a branch is a set of
|
|
6
|
+
*labelled* formulas ``w: φ`` (worlds are integers) together with the accessibility
|
|
7
|
+
edges generated along the way. The propositional / connective rules act at a fixed
|
|
8
|
+
world; the modal rules move between worlds:
|
|
9
|
+
|
|
10
|
+
- ``w: □φ`` (a *box* over its relation) asserts ``v: φ`` at every successor ``v`` of
|
|
11
|
+
``w`` — and is re-applied whenever a new successor appears;
|
|
12
|
+
- ``w: ◇φ`` (a *diamond*) creates a **fresh** successor ``v`` with ``v: φ``;
|
|
13
|
+
- a negated box becomes a diamond of the negation and vice versa (``¬□φ ≡ ◇¬φ``).
|
|
14
|
+
|
|
15
|
+
The box/diamond family handled here is exactly the one with a single accessibility
|
|
16
|
+
relation: alethic ``□``/``◇``, epistemic ``K_a``, doxastic ``B_a``, deontic
|
|
17
|
+
``O``/``P``, and the one-step temporal ``X`` (``Next``). Two of the three
|
|
18
|
+
GROUP-epistemic operators join that family too: ``EverybodyKnows`` (E_G, alpha-
|
|
19
|
+
reduced into one ``Knows`` box per agent) and ``DistributedKnowledge`` (D_G, a box
|
|
20
|
+
over the intersection of the group's per-agent relations, POSITIVE occurrences
|
|
21
|
+
only — see the comment above ``_distributed_relname``). ``CommonKnowledge`` (C_G)
|
|
22
|
+
needs the reflexive-transitive closure of a union relation, the same
|
|
23
|
+
least/greatest-fixpoint machinery ``Always``/``Eventually``/``Until`` need, so it
|
|
24
|
+
is rejected the same way they are (below). The relation names match the
|
|
25
|
+
:class:`~unicode_logic_kit.semantics.kripke.KripkeModel` convention
|
|
26
|
+
(``"alethic"`` / ``"K:"+a`` / ``"B:"+a`` / ``"deontic"`` / ``"temporal"``), so an open
|
|
27
|
+
branch is read off directly as a Kripke counter-model. The temporal *closure*
|
|
28
|
+
operators ``Always`` (G), ``Eventually`` (F) and ``Until`` need least/greatest-fixpoint
|
|
29
|
+
(eventuality) machinery beyond a basic labelled tableau and are rejected with a pointer
|
|
30
|
+
to :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` /
|
|
31
|
+
:func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_modal`. Hybrid constructs
|
|
32
|
+
(``Nominal`` / ``At``) are likewise rejected — a nominal's name-exactly-one-world
|
|
33
|
+
constraint has no rule here; use ``hybrid_is_valid`` (the standard translation + Z3)
|
|
34
|
+
or evaluate in a ``KripkeModel`` with a ``nominals=`` assignment.
|
|
35
|
+
|
|
36
|
+
**Public announcement logic (PAL)** — the ``Announce``/``AnnounceDiamond`` nodes
|
|
37
|
+
(``[φ!]ψ`` / ``⟨φ!⟩ψ``, :mod:`unicode_logic_kit.fol._modal_nodes`) — is decided too,
|
|
38
|
+
via a PRE-PASS: every public entry point below runs its formula(s) through
|
|
39
|
+
:func:`unicode_logic_kit.fol.pal.reduce_announcements` first (see :func:`_run`),
|
|
40
|
+
which eliminates every announcement using the standard PAL reduction axioms
|
|
41
|
+
before this tableau ever sees the formula. So ``modal_decide``/``modal_prove``/
|
|
42
|
+
``is_modal_valid``/``modal_countermodel`` all decide genuine PAL formulas — e.g.
|
|
43
|
+
the reduction axiom ``[φ!]K_aψ ↔ (φ → K_a[φ!]ψ)`` is K-valid, while the famous
|
|
44
|
+
NON-theorem ``[φ!]K_aψ → K_a[φ!]ψ`` is not (see ``tests/test_pal.py``) — with no
|
|
45
|
+
PAL-specific rule in this module at all.
|
|
46
|
+
|
|
47
|
+
**Sorted constants.** ``c:S`` denotes an element of ``S`` at every world (a constant is a
|
|
48
|
+
rigid designator; ``semantics.kripke``'s module docstring). The annotation does not make a
|
|
49
|
+
second symbol, so ``Mortal(c:S)`` and ``Mortal(c)`` are one letter, and the guard atom
|
|
50
|
+
``S(c)`` is a letter that is true at EVERY world: a world holding ``¬S(c)`` closes its
|
|
51
|
+
branch, and a model read off an open branch makes ``S(c)`` true everywhere. Without that a
|
|
52
|
+
valid formula such as ``S(c:S)`` had an open branch whose model left ``c`` out of ``S``, and
|
|
53
|
+
:func:`modal_decide` called it invalid. A sorted QUANTIFIER stays an opaque literal.
|
|
54
|
+
|
|
55
|
+
**Frame conditions** are realised as structural rules over the edge set: reflexivity
|
|
56
|
+
adds ``w → w`` for every world, symmetry mirrors each edge, transitivity takes the
|
|
57
|
+
closure, the euclidean rule closes ``w→v, w→u ⊢ v→u``, and seriality manufactures a
|
|
58
|
+
successor for a world that has a box obligation and lacks one. The named systems are K, T,
|
|
59
|
+
D/KD, B/KB, K4, K45, S4, S5, KD45.
|
|
60
|
+
|
|
61
|
+
**The model is a model of the frame.** The branch the search leaves open is not yet a
|
|
62
|
+
structure of the frame class that was asked for: a world that has no box obligation needs no
|
|
63
|
+
successor for the formula, but a serial relation gives it one. The model that is read off
|
|
64
|
+
therefore lets every such dead end of a serial relation see itself. A self-loop at a world
|
|
65
|
+
without a successor adds no obligation (the world has no box of that relation to satisfy) and
|
|
66
|
+
keeps a transitive, symmetric or euclidean relation closed. A relation the formula reads
|
|
67
|
+
but the branch never used (its operator sits in a disjunct the branch does not take) has no
|
|
68
|
+
edge on the branch, and a relation the model does not list is the empty relation, which is
|
|
69
|
+
neither reflexive nor serial: it is given the loops its system asks for as well, so every
|
|
70
|
+
relation the formula reads is a relation of the frame class. A relation the formula does not
|
|
71
|
+
read is not made up: nothing the formula says could tell the difference. The loops can change
|
|
72
|
+
the value of one construct: a distributed-knowledge box ``D_G`` reads the intersection of
|
|
73
|
+
several relations, and a world that is a dead end in each of them then gains an edge in the
|
|
74
|
+
intersection. A model is therefore handed out only when it falsifies the formula
|
|
75
|
+
(:func:`satisfies_modal`, which evaluates the relations as they are and does not know the
|
|
76
|
+
frame) AND every relation of it, and every relation the formula reads, satisfies the frame
|
|
77
|
+
conditions of its system; if either check fails, no model is handed out and the answer is
|
|
78
|
+
``"unknown"``. The first check is made at each open branch already, so a branch whose model
|
|
79
|
+
does not falsify the formula (it holds a construct this tableau has no rule for, such as a
|
|
80
|
+
negated ``D_G``) does not end the search while another branch is left.
|
|
81
|
+
|
|
82
|
+
**Equality is NOT interpreted here.** ``a = b`` / ``a ≠ b`` need a semantics of TERMS —
|
|
83
|
+
what ``a`` and ``b`` denote, so that the atom can be decided as identity of those
|
|
84
|
+
denotations — and this tableau has none: an atom is a propositional letter, a branch
|
|
85
|
+
closes on a syntactic complement, and an open branch is read off as a valuation of
|
|
86
|
+
rendered atom keys. Run on ``a = a`` it would leave the branch for ``¬(a = a)`` open
|
|
87
|
+
(``is_modal_valid`` False, ``modal_decide`` "unknown") although identity is reflexive,
|
|
88
|
+
and on ``a = b → □(a = b)`` it would build a counter-model in which identity varies
|
|
89
|
+
from world to world — both against :func:`unicode_logic_kit.fol.qml.qml_is_valid`, where
|
|
90
|
+
``=`` is RIGID identity over the object domain. Reading identity as an uninterpreted
|
|
91
|
+
relation is an approximation this kit refuses, so every public entry point raises
|
|
92
|
+
``NotImplementedError`` naming the atom (the shared
|
|
93
|
+
:func:`~unicode_logic_kit.semantics._modal_reject.reject_equality`, the same refusal
|
|
94
|
+
:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` gives). The refusal is a
|
|
95
|
+
whole-formula scan at entry (:func:`_run`), BEFORE any search, and never left to the
|
|
96
|
+
search reaching an ``Atom``: a branch that closes on an unrelated contradiction, a
|
|
97
|
+
vacuous ``□`` at a dead end, or a tautologous disjunct would
|
|
98
|
+
otherwise return a verdict that never looked at the atom. Decide identity with
|
|
99
|
+
``fol.qml.qml_is_valid`` or another first-order route.
|
|
100
|
+
|
|
101
|
+
**Cross-family bridges are NOT supported here.** A frame condition relating two
|
|
102
|
+
DIFFERENT relations — ``rb ⊆ rk`` (``K_a φ → B_a φ``), ``rb ⊆ rs``
|
|
103
|
+
(``Say_a φ → B_a φ``), ``∀w ∃v. d w v ∧ r w v`` (``Oφ → ◇φ``) — would need edge
|
|
104
|
+
rules that copy or jointly witness across relations, and this tableau has none:
|
|
105
|
+
every structural rule above acts within a single relation. Rather than accept a
|
|
106
|
+
``bridges=`` request and quietly decide a *different* (weaker) logic, every public
|
|
107
|
+
entry point takes ``bridges=None`` and **raises** ``NotImplementedError`` naming the
|
|
108
|
+
routes that do implement the option — ``fol.qml.qml_is_valid`` /
|
|
109
|
+
``fol.qml.qml_axioms`` and ``hol.isabelle_modal.to_isabelle_modal`` /
|
|
110
|
+
``hol.thf_modal.to_thf_modal_full``. Without the guard a caller that threaded
|
|
111
|
+
``bridges=`` through the toolkit would get ``invalid`` here and ``valid`` there for
|
|
112
|
+
the same formula, which is precisely the cross-route disagreement the option was
|
|
113
|
+
introduced to remove.
|
|
114
|
+
|
|
115
|
+
**Soundness vs. completeness.** Every rule preserves satisfiability over its frame
|
|
116
|
+
class, so a *closed* tableau is a real proof — ``is_modal_valid`` only returns ``True``
|
|
117
|
+
when the tableau closes. Termination on the transitive logics relies on subset
|
|
118
|
+
*blocking*, and the whole search is bounded (``max_worlds`` / ``max_steps`` and an optional
|
|
119
|
+
wall-clock ``timeout`` in milliseconds, which every public entry point takes and the search
|
|
120
|
+
checks at every step; a bound hit is ``"unknown"``, never an exception); to keep
|
|
121
|
+
the *invalid* verdict trustworthy regardless of any blocking/bound effect, an open
|
|
122
|
+
branch's model is **verified** with :func:`satisfies_modal` before it is reported, and
|
|
123
|
+
a model that fails to falsify the formula downgrades the answer to ``"unknown"`` rather
|
|
124
|
+
than risk a wrong ``"invalid"``. The result is the same valid / invalid / unknown
|
|
125
|
+
contract as the local-Isabelle runner, but in-process and install-free.
|
|
126
|
+
|
|
127
|
+
Public API: :func:`modal_tableau_closed`, :func:`is_modal_valid`, :func:`modal_prove`,
|
|
128
|
+
:func:`modal_decide`, :func:`modal_countermodel`.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
import time
|
|
132
|
+
from typing import List, Optional, Tuple
|
|
133
|
+
|
|
134
|
+
from .._deadline import DeadlineReached
|
|
135
|
+
from ..fol.nodes import (
|
|
136
|
+
Node, Atom, Not, And, Or, Xor, Implies, Iff, Contrast,
|
|
137
|
+
Box, Diamond, Knows, Believes, Says, Wants, Obligatory, Permitted,
|
|
138
|
+
EverybodyKnows, DistributedKnowledge, CommonKnowledge,
|
|
139
|
+
Next, Always, Eventually, Until,
|
|
140
|
+
Historically, Once, Previous, Since,
|
|
141
|
+
Nominal, At, Would, Might,
|
|
142
|
+
Quantifier, SortedQuantifier, Count, SortedCount,
|
|
143
|
+
Cardinality, SortedCardinality, SecondOrderQuantifier,
|
|
144
|
+
)
|
|
145
|
+
from ..fol._modal_nodes import Announce, AnnounceDiamond
|
|
146
|
+
# Down (the ↓ binder, N1) is not yet re-exported through fol.nodes / fol's
|
|
147
|
+
# public __init__ / the top-level unicode_logic_kit package (that three-file
|
|
148
|
+
# edit is outside this change's file ownership — see the change's own
|
|
149
|
+
# report); imported directly from its defining module in the meantime, the
|
|
150
|
+
# same class object either import path would give.
|
|
151
|
+
from ..fol._hybrid_nodes import Down
|
|
152
|
+
from ..fol.pal import reduce_announcements
|
|
153
|
+
from ..semantics.kripke import KripkeModel, satisfies_modal
|
|
154
|
+
from ..semantics._modal_reject import reject_equality_in
|
|
155
|
+
from ..fol.frames import (
|
|
156
|
+
FRAME_CONDITIONS, FRAMES as _SHARED_FRAMES,
|
|
157
|
+
UnsupportedFrameCondition, resolve_frame, require_supported,
|
|
158
|
+
)
|
|
159
|
+
from ..fol._atom_keys import refuse_alike_agents
|
|
160
|
+
from ..fol._msfl_nodes import key_text
|
|
161
|
+
from ..fol._truth_constants import is_true_constant, is_truth_constant
|
|
162
|
+
from .fitch import is_falsum
|
|
163
|
+
from .lj import _forget_constant_sorts
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Relation names — the contract with semantics.kripke.KripkeModel.
|
|
167
|
+
_ALETHIC = "alethic"
|
|
168
|
+
_DEONTIC = "deontic"
|
|
169
|
+
_TEMPORAL = "temporal"
|
|
170
|
+
_KNOWS = "K:"
|
|
171
|
+
_BELIEVES = "B:"
|
|
172
|
+
_SAYS = "Say:"
|
|
173
|
+
_WANTS = "Want:"
|
|
174
|
+
|
|
175
|
+
#: The named modal systems, shared with every other route
|
|
176
|
+
#: (:mod:`unicode_logic_kit.fol.frames`). This tableau implements the rules for
|
|
177
|
+
#: five of the conditions in that registry — reflexivity, transitivity,
|
|
178
|
+
#: symmetry, seriality, euclideanness — and REFUSES the rest by name
|
|
179
|
+
#: (:data:`_TABLEAU_CONDITIONS` / :func:`_check_frame`): a labelled tableau
|
|
180
|
+
#: for density or partial functionality needs rules this module does not
|
|
181
|
+
#: have, and quietly dropping the condition would answer about a larger
|
|
182
|
+
#: frame class than the caller asked for.
|
|
183
|
+
_FRAMES = _SHARED_FRAMES
|
|
184
|
+
|
|
185
|
+
#: The frame conditions this tableau has sound rules for.
|
|
186
|
+
_TABLEAU_CONDITIONS = frozenset({"refl", "trans", "sym", "serial", "eucl"})
|
|
187
|
+
|
|
188
|
+
# Operators needing least/greatest-fixpoint (eventuality) or converse-relation
|
|
189
|
+
# machinery beyond this labelled tableau — routed to satisfies_modal / Isabelle.
|
|
190
|
+
_TEMPORAL_CLOSURE = (Always, Eventually, Until, Historically, Once, Previous, Since)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _agent_key(agent: Node) -> str:
|
|
194
|
+
"""Relation-key suffix for an epistemic/doxastic agent term (its name)."""
|
|
195
|
+
return getattr(agent, "name", None) or key_text(agent)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Distributed knowledge (D_G): a box over the INTERSECTION of the group's
|
|
200
|
+
# per-agent "K:"+agent relations.
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
#
|
|
203
|
+
# Every OTHER box/diamond rule here acts on ONE named relation (`_apply_boxes`
|
|
204
|
+
# just pushes a box's body to `b.rels[relname]`'s successors; `_frame_close`
|
|
205
|
+
# closes ONE relation's edges under its own frame conditions). Distributed
|
|
206
|
+
# knowledge needs a box over several relations' INTERSECTION at once, which
|
|
207
|
+
# is not a relation _frame_close (or anything else here) already builds. The
|
|
208
|
+
# fix is additive rather than new machinery in the search loop itself: a D_G
|
|
209
|
+
# box is filed under a SYNTHETIC relation name that encodes its own
|
|
210
|
+
# constituent "K:"+agent names (`_distributed_relname`), and `_close_distributed`
|
|
211
|
+
# — run to fixpoint alongside `_frame_close`/`_apply_boxes` in `_solve` — keeps
|
|
212
|
+
# that synthetic relation's edge set equal to the intersection of its
|
|
213
|
+
# constituents' CURRENT edges every round (constituents only ever GROW during
|
|
214
|
+
# search — frame closure, or a nested diamond inside one of them — so this
|
|
215
|
+
# recompute is monotonic and terminates for exactly the reason `_frame_close`'s
|
|
216
|
+
# own transitive closure does). satisfies_modal itself never reads a "D∩:…"
|
|
217
|
+
# key — semantics.action_models.distributed_knowledge_holds recomputes the
|
|
218
|
+
# SAME intersection directly from "K:"+agent — so the synthetic name is purely
|
|
219
|
+
# an internal tableau bookkeeping device; `_build_model` happening to carry it
|
|
220
|
+
# along into the returned KripkeModel is harmless (an extra relation entry
|
|
221
|
+
# nothing downstream queries).
|
|
222
|
+
#
|
|
223
|
+
# A constituent "K:"+agent relation must also be FRAME-CLOSED under the
|
|
224
|
+
# caller's requested epistemic system (S5 reflexivity, etc.) even when the
|
|
225
|
+
# agent is mentioned nowhere else in the formula — otherwise D_G's box would
|
|
226
|
+
# quantify over an intersection of relations the search never applied the
|
|
227
|
+
# requested frame conditions to, which is unsound the moment any of those
|
|
228
|
+
# conditions matters (e.g. S5 factivity: D_a P → P needs "K:a" reflexive).
|
|
229
|
+
# `_frame_close` only ever looks at `_relnames(b)` (`b.rels`'s keys plus box
|
|
230
|
+
# relation names), so `_close_distributed` REGISTERS each constituent in
|
|
231
|
+
# `b.rels` (even with no edges yet) the first time it sees a synthetic D∩ key
|
|
232
|
+
# — this alone makes the constituent "live" for the next `_frame_close` pass,
|
|
233
|
+
# which is why the registration also reports `changed`: the `_solve` fixpoint
|
|
234
|
+
# loop (`_frame_close` / `_close_distributed` / `_apply_boxes`) must run one
|
|
235
|
+
# more round for that newly-live relation to actually pick up its reflexive/
|
|
236
|
+
# transitive/etc. edges before the intersection is (re)computed from them.
|
|
237
|
+
#
|
|
238
|
+
# Only the POSITIVE (box) occurrence of D_G is given a rule: the negated form
|
|
239
|
+
# ¬D_G φ (a diamond over the intersection) would need a fresh witness world
|
|
240
|
+
# added to EVERY constituent relation at once — the generic diamond rule in
|
|
241
|
+
# `_solve` adds an edge to exactly ONE named relation, and adding it only to
|
|
242
|
+
# the synthetic key would just be erased by the next `_close_distributed`
|
|
243
|
+
# pass (it is not yet in every constituent). Rather than special-case the
|
|
244
|
+
# generic diamond-witness step for one synthetic relation family, ¬D_G is left
|
|
245
|
+
# `"unsupported"` (inert, sound, see `_expand_simple`) — this still decides
|
|
246
|
+
# every validity of the shape `D_G φ → …` (the useful direction: D_G occurs
|
|
247
|
+
# POSITIVELY once its negation-as-premise is pushed through), which is what
|
|
248
|
+
# the box rule below is for.
|
|
249
|
+
_DISTRIBUTED_PREFIX = "D∩:"
|
|
250
|
+
_DISTRIBUTED_SEP = "|"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _distributed_relname(agent_keys) -> str:
|
|
254
|
+
"""Synthetic relation name for a D_G box: encodes its own "K:"+agent
|
|
255
|
+
constituents (sorted, so the same group always yields the same key)."""
|
|
256
|
+
names = sorted(_KNOWS + a for a in agent_keys)
|
|
257
|
+
return _DISTRIBUTED_PREFIX + _DISTRIBUTED_SEP.join(names)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _distributed_constituents(relname: str):
|
|
261
|
+
"""Return the constituent relation names a synthetic D∩ key encodes, or
|
|
262
|
+
None if ``relname`` is not one (an ordinary relation name never starts
|
|
263
|
+
with "D∩:", since that prefix is not producible by any agent name — agent
|
|
264
|
+
names lex as VARIABLE/NAME, which cannot contain "∩")."""
|
|
265
|
+
if not relname.startswith(_DISTRIBUTED_PREFIX):
|
|
266
|
+
return None
|
|
267
|
+
return relname[len(_DISTRIBUTED_PREFIX):].split(_DISTRIBUTED_SEP)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _close_distributed(b: "_Branch") -> bool:
|
|
271
|
+
"""Recompute every synthetic D∩ relation as the intersection of its
|
|
272
|
+
constituents' CURRENT edges; return True iff any edge set changed.
|
|
273
|
+
|
|
274
|
+
Also REGISTERS every constituent "K:"+agent relation in ``b.rels`` (with
|
|
275
|
+
no edges, if it has none yet) the first time it is seen — this makes it
|
|
276
|
+
'live' for `_frame_close` (see the module comment above), which is what
|
|
277
|
+
lets an agent whose only mention is inside this D_G box still receive the
|
|
278
|
+
caller's requested epistemic frame conditions (S5 reflexivity and so on).
|
|
279
|
+
Registering a previously-absent constituent counts as a change so the
|
|
280
|
+
`_solve` fixpoint loop runs `_frame_close` again before the intersection
|
|
281
|
+
below is treated as final.
|
|
282
|
+
"""
|
|
283
|
+
changed = False
|
|
284
|
+
for relname in list(_relnames(b)):
|
|
285
|
+
constituents = _distributed_constituents(relname)
|
|
286
|
+
if constituents is None:
|
|
287
|
+
continue
|
|
288
|
+
for name in constituents:
|
|
289
|
+
if name not in b.rels:
|
|
290
|
+
b.rels[name] = set()
|
|
291
|
+
changed = True
|
|
292
|
+
inter = None
|
|
293
|
+
for name in constituents:
|
|
294
|
+
edges = b.rels.get(name, set())
|
|
295
|
+
inter = set(edges) if inter is None else (inter & edges)
|
|
296
|
+
inter = inter if inter is not None else set()
|
|
297
|
+
if b.rels.get(relname) != inter:
|
|
298
|
+
b.rels[relname] = inter
|
|
299
|
+
changed = True
|
|
300
|
+
return changed
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _neg(f: Node) -> Node:
|
|
304
|
+
"""Return the complementary formula of ``f`` (``¬φ`` ↔ ``φ``)."""
|
|
305
|
+
return f.formula if isinstance(f, Not) else Not(f)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def has_modal(node: Node) -> bool:
|
|
309
|
+
"""True iff ``node`` contains any modal/temporal/epistemic/deontic/hybrid
|
|
310
|
+
operator — or a counterfactual — or a public-announcement operator.
|
|
311
|
+
|
|
312
|
+
Hybrid constructs (Nominal / At), the Lewis counterfactuals (Would / Might),
|
|
313
|
+
and the PAL announcement operators (Announce / AnnounceDiamond) count as
|
|
314
|
+
modal so the classical tableau routes them here, where each gets its clean,
|
|
315
|
+
specific rejection (or, for Announce/AnnounceDiamond, its pal.reduce_announcements
|
|
316
|
+
pre-pass — see :func:`_run`) instead of a generic no-rule error.
|
|
317
|
+
"""
|
|
318
|
+
modal = (Box, Diamond, Knows, Believes, Says, Wants, Obligatory, Permitted,
|
|
319
|
+
EverybodyKnows, DistributedKnowledge, CommonKnowledge,
|
|
320
|
+
Next, Always, Eventually, Until,
|
|
321
|
+
Historically, Once, Previous, Since,
|
|
322
|
+
Nominal, At, Down, Would, Might, Announce, AnnounceDiamond)
|
|
323
|
+
return any(isinstance(n, modal) for n in node.walk())
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _contains_hybrid(node: Node) -> bool:
|
|
327
|
+
"""True iff ``node`` contains a hybrid construct (a Nominal or an At)."""
|
|
328
|
+
return any(isinstance(n, (Nominal, At)) for n in node.walk())
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _contains_down(node: Node) -> bool:
|
|
332
|
+
"""True iff ``node`` contains the ↓ binder (N1).
|
|
333
|
+
|
|
334
|
+
``Down.variable`` is itself a :class:`Nominal` (see fol._hybrid_nodes'
|
|
335
|
+
module docstring), which means ``_contains_hybrid`` above already
|
|
336
|
+
detects every ``Down``-containing formula with ZERO extra code (the
|
|
337
|
+
generic ``Node._child_nodes``/``walk`` traversal is field-VALUE-typed,
|
|
338
|
+
not field-NAME-typed, so it walks straight into ``Down.variable`` too).
|
|
339
|
+
This function exists ONLY so ``_run`` can raise a ↓-SPECIFIC,
|
|
340
|
+
undecidability-naming message ahead of that generic one — Down must
|
|
341
|
+
never fall through to a generic branch (or even a correct-but-vaguer
|
|
342
|
+
one) unnoticed.
|
|
343
|
+
"""
|
|
344
|
+
return any(isinstance(n, Down) for n in node.walk())
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _contains_counterfactual(node: Node) -> bool:
|
|
348
|
+
"""True iff ``node`` contains a Lewis counterfactual (Would / Might)."""
|
|
349
|
+
return any(isinstance(n, (Would, Might)) for n in node.walk())
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
#: Constructs that bind an object/predicate variable. The modal tableau is a
|
|
353
|
+
#: ground (propositional-modal) engine with no quantifier rules, so these are
|
|
354
|
+
#: treated as OPAQUE literals: a branch may still close on a syntactic
|
|
355
|
+
#: complement (sound — φ and ¬φ at one world are contradictory whatever φ
|
|
356
|
+
#: means), while an open branch's model must pass satisfies_modal verification
|
|
357
|
+
#: before any caller sees it, so no wrong verdict can arise from the opacity.
|
|
358
|
+
_QUANTIFIED = (Quantifier, SortedQuantifier, Count, SortedCount,
|
|
359
|
+
Cardinality, SortedCardinality, SecondOrderQuantifier)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _decompose(f: Node):
|
|
363
|
+
"""Classify a formula for the tableau.
|
|
364
|
+
|
|
365
|
+
Returns one of:
|
|
366
|
+
``("lit",)`` — atom / negated atom / ⊥ (closure only);
|
|
367
|
+
``("true",)`` — ¬⊥ (always true, discard);
|
|
368
|
+
``("alpha", [comp, …])`` — assert all components at the same world;
|
|
369
|
+
``("beta", [[…], […]])`` — branch (each list one branch's components);
|
|
370
|
+
``("box", relname, body)`` — universal modality over ``relname``;
|
|
371
|
+
``("dia", relname, body)`` — existential modality over ``relname``;
|
|
372
|
+
``("unsupported", node)`` — a temporal-closure operator (G / F / U).
|
|
373
|
+
"""
|
|
374
|
+
if is_falsum(f):
|
|
375
|
+
return ("lit",)
|
|
376
|
+
if is_true_constant(f):
|
|
377
|
+
return ("true",) # `$true` holds at every world: discard
|
|
378
|
+
if isinstance(f, Atom):
|
|
379
|
+
return ("lit",)
|
|
380
|
+
if isinstance(f, _QUANTIFIED):
|
|
381
|
+
# Opaque literal: no quantifier rules here, but syntactic-complement
|
|
382
|
+
# closure stays sound and open models are verified before release.
|
|
383
|
+
return ("lit",)
|
|
384
|
+
|
|
385
|
+
# --- positive modal operators ---
|
|
386
|
+
if isinstance(f, Box):
|
|
387
|
+
return ("box", _ALETHIC, f.formula)
|
|
388
|
+
if isinstance(f, Diamond):
|
|
389
|
+
return ("dia", _ALETHIC, f.formula)
|
|
390
|
+
if isinstance(f, Knows):
|
|
391
|
+
return ("box", _KNOWS + _agent_key(f.agent), f.formula)
|
|
392
|
+
if isinstance(f, Believes):
|
|
393
|
+
return ("box", _BELIEVES + _agent_key(f.agent), f.formula)
|
|
394
|
+
if isinstance(f, Says):
|
|
395
|
+
return ("box", _SAYS + _agent_key(f.agent), f.formula)
|
|
396
|
+
if isinstance(f, Wants):
|
|
397
|
+
return ("box", _WANTS + _agent_key(f.agent), f.formula)
|
|
398
|
+
if isinstance(f, EverybodyKnows):
|
|
399
|
+
# E_G φ ≡ ⋀_{a∈G} K_a φ: an ALPHA reduction into one Knows(a, φ) per
|
|
400
|
+
# agent, each of which the loop re-decomposes into its own box rule
|
|
401
|
+
# on the NEXT pass — no new relation-key machinery needed (see the
|
|
402
|
+
# module-level comment above _distributed_relname for why D_G, unlike
|
|
403
|
+
# E_G, does need one). An empty group alpha-reduces to [] (no
|
|
404
|
+
# components), which the caller treats as "nothing new asserted" —
|
|
405
|
+
# correctly vacuous, matching everybody_knows's own convention.
|
|
406
|
+
return ("alpha", [Knows(a, f.formula) for a in f.group])
|
|
407
|
+
if isinstance(f, DistributedKnowledge):
|
|
408
|
+
# D_G φ: a genuine box, over the SYNTHETIC intersection relation
|
|
409
|
+
# _close_distributed keeps in sync (see the module-level comment).
|
|
410
|
+
agents = tuple(_agent_key(a) for a in f.group)
|
|
411
|
+
return ("box", _distributed_relname(agents), f.formula)
|
|
412
|
+
if isinstance(f, CommonKnowledge):
|
|
413
|
+
# C_G needs the reflexive-transitive closure of a union relation —
|
|
414
|
+
# an induction/fixpoint rule this labelled tableau does not have, the
|
|
415
|
+
# same G/F/U precedent below. Inert, never a wrong verdict (see
|
|
416
|
+
# _expand_simple's "unsupported" branch).
|
|
417
|
+
return ("unsupported", f)
|
|
418
|
+
if isinstance(f, Obligatory):
|
|
419
|
+
return ("box", _DEONTIC, f.formula)
|
|
420
|
+
if isinstance(f, Permitted):
|
|
421
|
+
return ("dia", _DEONTIC, f.formula)
|
|
422
|
+
if isinstance(f, Next):
|
|
423
|
+
return ("box", _TEMPORAL, f.formula)
|
|
424
|
+
if isinstance(f, _TEMPORAL_CLOSURE):
|
|
425
|
+
return ("unsupported", f)
|
|
426
|
+
|
|
427
|
+
# --- positive connectives ---
|
|
428
|
+
if isinstance(f, And):
|
|
429
|
+
return ("alpha", [f.left, f.right])
|
|
430
|
+
if isinstance(f, Contrast):
|
|
431
|
+
# Concession is truth-functionally conjunction (Contrast's own contract).
|
|
432
|
+
return ("alpha", [f.left, f.right])
|
|
433
|
+
if isinstance(f, Or):
|
|
434
|
+
return ("beta", [[f.left], [f.right]])
|
|
435
|
+
if isinstance(f, Implies):
|
|
436
|
+
return ("beta", [[Not(f.left)], [f.right]])
|
|
437
|
+
if isinstance(f, Iff):
|
|
438
|
+
return ("beta", [[f.left, f.right], [Not(f.left), Not(f.right)]])
|
|
439
|
+
if isinstance(f, Xor):
|
|
440
|
+
return ("beta", [[f.left, Not(f.right)], [Not(f.left), f.right]])
|
|
441
|
+
|
|
442
|
+
# --- negations: push through ---
|
|
443
|
+
if isinstance(f, Not):
|
|
444
|
+
g = f.formula
|
|
445
|
+
if is_falsum(g):
|
|
446
|
+
return ("true",)
|
|
447
|
+
if isinstance(g, Atom):
|
|
448
|
+
return ("lit",)
|
|
449
|
+
if isinstance(g, _QUANTIFIED):
|
|
450
|
+
return ("lit",)
|
|
451
|
+
if isinstance(g, Not):
|
|
452
|
+
return ("alpha", [g.formula])
|
|
453
|
+
if isinstance(g, And):
|
|
454
|
+
return ("beta", [[Not(g.left)], [Not(g.right)]])
|
|
455
|
+
if isinstance(g, Contrast):
|
|
456
|
+
return ("beta", [[Not(g.left)], [Not(g.right)]])
|
|
457
|
+
if isinstance(g, Or):
|
|
458
|
+
return ("alpha", [Not(g.left), Not(g.right)])
|
|
459
|
+
if isinstance(g, Implies):
|
|
460
|
+
return ("alpha", [g.left, Not(g.right)])
|
|
461
|
+
if isinstance(g, Iff):
|
|
462
|
+
return ("beta", [[g.left, Not(g.right)], [Not(g.left), g.right]])
|
|
463
|
+
if isinstance(g, Xor):
|
|
464
|
+
return ("beta", [[g.left, g.right], [Not(g.left), Not(g.right)]])
|
|
465
|
+
if isinstance(g, Box):
|
|
466
|
+
return ("dia", _ALETHIC, Not(g.formula))
|
|
467
|
+
if isinstance(g, Diamond):
|
|
468
|
+
return ("box", _ALETHIC, Not(g.formula))
|
|
469
|
+
if isinstance(g, Knows):
|
|
470
|
+
return ("dia", _KNOWS + _agent_key(g.agent), Not(g.formula))
|
|
471
|
+
if isinstance(g, Believes):
|
|
472
|
+
return ("dia", _BELIEVES + _agent_key(g.agent), Not(g.formula))
|
|
473
|
+
if isinstance(g, Says):
|
|
474
|
+
return ("dia", _SAYS + _agent_key(g.agent), Not(g.formula))
|
|
475
|
+
if isinstance(g, Wants):
|
|
476
|
+
return ("dia", _WANTS + _agent_key(g.agent), Not(g.formula))
|
|
477
|
+
if isinstance(g, EverybodyKnows):
|
|
478
|
+
# ¬E_G φ ≡ ⋁_{a∈G} ¬K_a φ: a BETA branch, one option per agent,
|
|
479
|
+
# each a single Not(Knows(a,φ)) component — which this SAME
|
|
480
|
+
# negation section already turns into a diamond rule on its own
|
|
481
|
+
# next pass (the `if isinstance(g, Knows)` case just above,
|
|
482
|
+
# reached because `_decompose` is called again on each freshly
|
|
483
|
+
# asserted Not(Knows(...))). An empty group beta-branches into
|
|
484
|
+
# ZERO options, which `_solve`'s beta handling immediately reports
|
|
485
|
+
# "closed" (no branch to keep the tableau open) — the correct
|
|
486
|
+
# verdict, since ¬E_∅ φ negates an always-vacuously-true E_∅ φ and
|
|
487
|
+
# so is itself unsatisfiable.
|
|
488
|
+
return ("beta", [[Not(Knows(a, g.formula))] for a in g.group])
|
|
489
|
+
if isinstance(g, DistributedKnowledge):
|
|
490
|
+
# ¬D_G φ (a diamond over the intersection relation) has no rule
|
|
491
|
+
# here — see the module-level comment above _distributed_relname
|
|
492
|
+
# for why. Inert, sound.
|
|
493
|
+
return ("unsupported", f)
|
|
494
|
+
if isinstance(g, CommonKnowledge):
|
|
495
|
+
return ("unsupported", f)
|
|
496
|
+
if isinstance(g, Obligatory):
|
|
497
|
+
return ("dia", _DEONTIC, Not(g.formula))
|
|
498
|
+
if isinstance(g, Permitted):
|
|
499
|
+
return ("box", _DEONTIC, Not(g.formula))
|
|
500
|
+
if isinstance(g, Next):
|
|
501
|
+
return ("dia", _TEMPORAL, Not(g.formula))
|
|
502
|
+
if isinstance(g, _TEMPORAL_CLOSURE):
|
|
503
|
+
return ("unsupported", f)
|
|
504
|
+
|
|
505
|
+
raise NotImplementedError(
|
|
506
|
+
f"modal_tableau: no rule for {type(f).__name__} {f.to_unicode_str()}")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class _OrderedSet(dict):
|
|
510
|
+
"""A set that iterates in the order its members were first added.
|
|
511
|
+
|
|
512
|
+
The branch keeps each world's formulas and its box obligations in one. The search takes
|
|
513
|
+
"the first" unexpanded branching formula, diamond or box obligation it meets, and which
|
|
514
|
+
one it takes decides the numbering of the worlds it creates and therefore the model it
|
|
515
|
+
reads off an open branch. A ``set`` of formulas (which hash their names) or of tuples
|
|
516
|
+
holding a relation name iterates in an order that follows ``PYTHONHASHSEED``, so the
|
|
517
|
+
same input gave a different countermodel from one process to the next; here the order is
|
|
518
|
+
a function of the sequence of insertions alone, and every insertion is itself made by an
|
|
519
|
+
ordered traversal, so the model is a function of the input. The rules and the bounds are
|
|
520
|
+
those of a plain ``set``: only the order in which they are tried is fixed.
|
|
521
|
+
|
|
522
|
+
A ``dict`` subclass, so membership, iteration and ``len`` keep the speed of the built-in
|
|
523
|
+
container; only the set operations the search uses are spelled out.
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
__slots__ = ()
|
|
527
|
+
|
|
528
|
+
def add(self, member) -> None:
|
|
529
|
+
self[member] = None
|
|
530
|
+
|
|
531
|
+
def copy(self) -> "_OrderedSet":
|
|
532
|
+
duplicate = _OrderedSet()
|
|
533
|
+
dict.update(duplicate, self)
|
|
534
|
+
return duplicate
|
|
535
|
+
|
|
536
|
+
def __le__(self, other) -> bool:
|
|
537
|
+
"""Subset test, as for a ``set`` (``other`` may be any set-like)."""
|
|
538
|
+
return self.keys() <= (other.keys() if isinstance(other, dict) else other)
|
|
539
|
+
|
|
540
|
+
def __repr__(self) -> str:
|
|
541
|
+
return f"_OrderedSet({list(self)!r})"
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
class _Branch:
|
|
545
|
+
"""A single open tableau branch: labelled formulas, edges, box obligations."""
|
|
546
|
+
|
|
547
|
+
__slots__ = ("tv", "rels", "boxes", "wcount", "expanded", "facts")
|
|
548
|
+
|
|
549
|
+
def __init__(self):
|
|
550
|
+
self.tv = {0: _OrderedSet()} # world -> the labelled formulas, in insertion order
|
|
551
|
+
self.rels = {} # relname -> set of (w, v) edges
|
|
552
|
+
self.boxes = _OrderedSet() # (world, relname, body) obligations, in insertion order
|
|
553
|
+
self.wcount = 1 # next fresh world id
|
|
554
|
+
self.expanded = set() # (world, formula) already consumed
|
|
555
|
+
self.facts = frozenset() # atoms true at EVERY world (sorted constants' membership)
|
|
556
|
+
|
|
557
|
+
def copy(self) -> "_Branch":
|
|
558
|
+
b = _Branch.__new__(_Branch)
|
|
559
|
+
b.tv = {w: s.copy() for w, s in self.tv.items()}
|
|
560
|
+
b.rels = {r: set(e) for r, e in self.rels.items()}
|
|
561
|
+
b.boxes = self.boxes.copy()
|
|
562
|
+
b.wcount = self.wcount
|
|
563
|
+
b.expanded = set(self.expanded)
|
|
564
|
+
b.facts = self.facts
|
|
565
|
+
return b
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
class _Ctx:
|
|
569
|
+
"""Search budget and frame configuration shared across the branch tree."""
|
|
570
|
+
|
|
571
|
+
def __init__(self, frame: str, systems, max_worlds: int, max_steps: int,
|
|
572
|
+
timeout: Optional[int] = None, mentioned: Tuple[str, ...] = (),
|
|
573
|
+
roots: Tuple[Node, ...] = ()):
|
|
574
|
+
self.frame = frame
|
|
575
|
+
self.systems = systems or {}
|
|
576
|
+
# every relation name the formulas read, whether or not the search used it: the model
|
|
577
|
+
# that is read off has to give each one the shape its system asks for
|
|
578
|
+
self.mentioned = tuple(mentioned)
|
|
579
|
+
# the formulas asserted at the root world: a model read off an open branch has to make
|
|
580
|
+
# them true there, or the branch (which holds a construct it has no rule for) is no help
|
|
581
|
+
self.roots = tuple(roots)
|
|
582
|
+
self.max_worlds = max_worlds
|
|
583
|
+
self.steps = max_steps
|
|
584
|
+
self.exhausted = False
|
|
585
|
+
# ``timeout`` is in milliseconds, counted from the moment the search starts.
|
|
586
|
+
self.deadline = None if timeout is None else time.perf_counter() + timeout / 1000.0
|
|
587
|
+
|
|
588
|
+
def tick(self) -> bool:
|
|
589
|
+
"""Charge one step; False once the step budget is gone or the deadline has passed."""
|
|
590
|
+
self.steps -= 1
|
|
591
|
+
if self.steps <= 0 or (self.deadline is not None and time.perf_counter() > self.deadline):
|
|
592
|
+
self.exhausted = True
|
|
593
|
+
self.steps = 0
|
|
594
|
+
return False
|
|
595
|
+
return True
|
|
596
|
+
|
|
597
|
+
def poll(self) -> None:
|
|
598
|
+
"""Raise :class:`~unicode_logic_kit._deadline.DeadlineReached` once the deadline has passed.
|
|
599
|
+
|
|
600
|
+
For the loops that run between two :meth:`tick` calls (the frame closure, the box
|
|
601
|
+
rule): they read the clock themselves, so one step of the search cannot outlast the
|
|
602
|
+
deadline however many worlds it has to close. No charge is made against the budget.
|
|
603
|
+
:func:`_run` catches the exception and gives up the whole search.
|
|
604
|
+
"""
|
|
605
|
+
if self.deadline is not None and time.perf_counter() > self.deadline:
|
|
606
|
+
self.exhausted = True
|
|
607
|
+
self.steps = 0
|
|
608
|
+
raise DeadlineReached
|
|
609
|
+
|
|
610
|
+
def conds(self, relname: str) -> Tuple[str, ...]:
|
|
611
|
+
"""Frame conditions for a relation name, per the configured systems."""
|
|
612
|
+
if relname == _ALETHIC:
|
|
613
|
+
return _FRAMES[self.frame]
|
|
614
|
+
if relname == _DEONTIC:
|
|
615
|
+
return _FRAMES[self.systems.get("deontic", "KD")]
|
|
616
|
+
if relname == _TEMPORAL:
|
|
617
|
+
return _FRAMES[self.systems.get("temporal", "K")]
|
|
618
|
+
if relname.startswith(_KNOWS):
|
|
619
|
+
return _FRAMES[self.systems.get("epistemic", "K")]
|
|
620
|
+
if relname.startswith(_BELIEVES):
|
|
621
|
+
return _FRAMES[self.systems.get("doxastic", "K")]
|
|
622
|
+
return ()
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _assert(b: _Branch, w: int, f: Node) -> bool:
|
|
626
|
+
"""Assert ``w: f``; return True iff it was new."""
|
|
627
|
+
s = b.tv.setdefault(w, _OrderedSet())
|
|
628
|
+
if f in s:
|
|
629
|
+
return False
|
|
630
|
+
s.add(f)
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _closes(b: _Branch) -> bool:
|
|
635
|
+
"""True iff some world holds a formula and its negation (or ⊥).
|
|
636
|
+
|
|
637
|
+
An atom of ``b.facts`` is true at EVERY world, so a world that holds its
|
|
638
|
+
negation is contradictory too, whatever else it holds.
|
|
639
|
+
"""
|
|
640
|
+
for s in b.tv.values():
|
|
641
|
+
for f in s:
|
|
642
|
+
if is_falsum(f):
|
|
643
|
+
return True
|
|
644
|
+
if isinstance(f, Not) and is_true_constant(f.formula):
|
|
645
|
+
return True # ¬$true is false at every world, like ⊥ and $false
|
|
646
|
+
if _neg(f) in s:
|
|
647
|
+
return True
|
|
648
|
+
if b.facts and isinstance(f, Not) and f.formula in b.facts:
|
|
649
|
+
return True
|
|
650
|
+
return False
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _relations_read_by(node: Node) -> Tuple[str, ...]:
|
|
654
|
+
"""The relation names a modal operator reads: ``node`` itself, never its operands.
|
|
655
|
+
|
|
656
|
+
The same names :func:`_decompose` files its box and diamond rules under (the group
|
|
657
|
+
operators read the relation of each agent of the group), and the ones
|
|
658
|
+
:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` reads in the model. Every other
|
|
659
|
+
node reads none.
|
|
660
|
+
"""
|
|
661
|
+
if isinstance(node, (Box, Diamond)):
|
|
662
|
+
return (_ALETHIC,)
|
|
663
|
+
if isinstance(node, (Obligatory, Permitted)):
|
|
664
|
+
return (_DEONTIC,)
|
|
665
|
+
if isinstance(node, (Next,) + _TEMPORAL_CLOSURE):
|
|
666
|
+
return (_TEMPORAL,)
|
|
667
|
+
if isinstance(node, Knows):
|
|
668
|
+
return (_KNOWS + _agent_key(node.agent),)
|
|
669
|
+
if isinstance(node, Believes):
|
|
670
|
+
return (_BELIEVES + _agent_key(node.agent),)
|
|
671
|
+
if isinstance(node, Says):
|
|
672
|
+
return (_SAYS + _agent_key(node.agent),)
|
|
673
|
+
if isinstance(node, Wants):
|
|
674
|
+
return (_WANTS + _agent_key(node.agent),)
|
|
675
|
+
if isinstance(node, (EverybodyKnows, DistributedKnowledge, CommonKnowledge)):
|
|
676
|
+
return tuple(_KNOWS + _agent_key(a) for a in node.group)
|
|
677
|
+
return ()
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _mentioned_relations(formulas) -> Tuple[str, ...]:
|
|
681
|
+
"""Every relation name any of ``formulas`` reads, in the order they first occur.
|
|
682
|
+
|
|
683
|
+
A relation is mentioned when an operator that reads it occurs ANYWHERE in a formula,
|
|
684
|
+
also in a disjunct the open branch of the search does not take: the model that is read
|
|
685
|
+
off an open branch has to give that relation the shape its system asks for, though the
|
|
686
|
+
branch has no edge of it.
|
|
687
|
+
"""
|
|
688
|
+
names = _OrderedSet()
|
|
689
|
+
for f in formulas:
|
|
690
|
+
for node in f.walk():
|
|
691
|
+
for rel in _relations_read_by(node):
|
|
692
|
+
names.add(rel)
|
|
693
|
+
return tuple(names)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _relnames(b: _Branch):
|
|
697
|
+
"""Relation names that are 'live' on this branch (have edges or box obligations),
|
|
698
|
+
in the order they first appeared (a ``set`` of names would follow the hash seed)."""
|
|
699
|
+
names = _OrderedSet()
|
|
700
|
+
for rel in b.rels:
|
|
701
|
+
names.add(rel)
|
|
702
|
+
for (_w, rel, _body) in b.boxes:
|
|
703
|
+
names.add(rel)
|
|
704
|
+
return names
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _frame_close(b: _Branch, ctx: _Ctx) -> bool:
|
|
708
|
+
"""Apply reflexive/symmetric/transitive/euclidean edge rules; return True if changed.
|
|
709
|
+
|
|
710
|
+
The closure of a large edge set takes many times the work of one search step, so it
|
|
711
|
+
reads the clock itself (:meth:`_Ctx.poll`) once per edge it takes up and gives the
|
|
712
|
+
whole search up at the deadline.
|
|
713
|
+
"""
|
|
714
|
+
changed = False
|
|
715
|
+
worlds = list(b.tv)
|
|
716
|
+
for rel in list(_relnames(b)):
|
|
717
|
+
conds = ctx.conds(rel)
|
|
718
|
+
if not conds:
|
|
719
|
+
continue
|
|
720
|
+
edges = b.rels.setdefault(rel, set())
|
|
721
|
+
if "refl" in conds:
|
|
722
|
+
for w in worlds:
|
|
723
|
+
ctx.poll()
|
|
724
|
+
if (w, w) not in edges:
|
|
725
|
+
edges.add((w, w))
|
|
726
|
+
changed = True
|
|
727
|
+
if "sym" in conds:
|
|
728
|
+
for (w, v) in list(edges):
|
|
729
|
+
ctx.poll()
|
|
730
|
+
if (v, w) not in edges:
|
|
731
|
+
edges.add((v, w))
|
|
732
|
+
changed = True
|
|
733
|
+
if "eucl" in conds:
|
|
734
|
+
out = {}
|
|
735
|
+
for (w, v) in edges:
|
|
736
|
+
out.setdefault(w, []).append(v)
|
|
737
|
+
for w, succs in out.items():
|
|
738
|
+
for v in succs:
|
|
739
|
+
ctx.poll()
|
|
740
|
+
for u in succs:
|
|
741
|
+
if (v, u) not in edges:
|
|
742
|
+
edges.add((v, u))
|
|
743
|
+
changed = True
|
|
744
|
+
if "trans" in conds:
|
|
745
|
+
added = True
|
|
746
|
+
while added:
|
|
747
|
+
added = False
|
|
748
|
+
for (w, v) in list(edges):
|
|
749
|
+
ctx.poll()
|
|
750
|
+
for (v2, u) in list(edges):
|
|
751
|
+
if v == v2 and (w, u) not in edges:
|
|
752
|
+
edges.add((w, u))
|
|
753
|
+
added = True
|
|
754
|
+
changed = True
|
|
755
|
+
return changed
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _apply_boxes(b: _Branch, ctx: Optional[_Ctx] = None) -> bool:
|
|
759
|
+
"""Push every box obligation to its successors; return True if anything was new.
|
|
760
|
+
|
|
761
|
+
With a ``ctx`` the clock is read once per obligation (see :meth:`_Ctx.poll`).
|
|
762
|
+
"""
|
|
763
|
+
changed = False
|
|
764
|
+
for (w, rel, body) in list(b.boxes):
|
|
765
|
+
if ctx is not None:
|
|
766
|
+
ctx.poll()
|
|
767
|
+
for (a, v) in b.rels.get(rel, ()):
|
|
768
|
+
if a == w and _assert(b, v, body):
|
|
769
|
+
changed = True
|
|
770
|
+
return changed
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _expand_simple(b: _Branch) -> bool:
|
|
774
|
+
"""Apply double-negation / α / box-record rules; return True if anything changed.
|
|
775
|
+
|
|
776
|
+
A temporal-closure operator is marked inert (see the ``unsupported`` branch)
|
|
777
|
+
rather than raising, keeping every verdict sound and every crash impossible.
|
|
778
|
+
"""
|
|
779
|
+
changed = False
|
|
780
|
+
for w in list(b.tv):
|
|
781
|
+
for f in list(b.tv[w]):
|
|
782
|
+
if (w, f) in b.expanded:
|
|
783
|
+
continue
|
|
784
|
+
kind = _decompose(f)
|
|
785
|
+
tag = kind[0]
|
|
786
|
+
if tag in ("lit", "true"):
|
|
787
|
+
b.expanded.add((w, f))
|
|
788
|
+
elif tag == "alpha":
|
|
789
|
+
for comp in kind[1]:
|
|
790
|
+
if _assert(b, w, comp):
|
|
791
|
+
changed = True
|
|
792
|
+
b.expanded.add((w, f))
|
|
793
|
+
changed = True
|
|
794
|
+
elif tag == "box":
|
|
795
|
+
_, rel, body = kind
|
|
796
|
+
if (w, rel, body) not in b.boxes:
|
|
797
|
+
b.boxes.add((w, rel, body))
|
|
798
|
+
changed = True
|
|
799
|
+
b.expanded.add((w, f))
|
|
800
|
+
elif tag == "unsupported":
|
|
801
|
+
# A temporal-closure operator (G/F/U/H/P/Y/S over the closure of
|
|
802
|
+
# the temporal relation) has no rule here. Leave it INERT rather
|
|
803
|
+
# than raising: closure of a branch is monotone (a contradiction
|
|
804
|
+
# among the expanded formulas makes the full set unsatisfiable
|
|
805
|
+
# regardless of the inert ones), so "closed" verdicts stay sound;
|
|
806
|
+
# and an open branch's model only ever reaches a caller after
|
|
807
|
+
# satisfies_modal — which evaluates these operators exactly —
|
|
808
|
+
# verifies it, so no spurious countermodel can leak. The price is
|
|
809
|
+
# honest incompleteness: a branch kept open only by an inert
|
|
810
|
+
# formula yields "unknown", never a wrong verdict.
|
|
811
|
+
b.expanded.add((w, f))
|
|
812
|
+
# beta / dia handled by the search loop
|
|
813
|
+
return changed
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _find_beta(b: _Branch):
|
|
817
|
+
"""Return ``(w, f, options)`` for an unexpanded branching formula, or None."""
|
|
818
|
+
for w in b.tv:
|
|
819
|
+
for f in b.tv[w]:
|
|
820
|
+
if (w, f) in b.expanded:
|
|
821
|
+
continue
|
|
822
|
+
kind = _decompose(f)
|
|
823
|
+
if kind[0] == "beta":
|
|
824
|
+
return (w, f, kind[1])
|
|
825
|
+
return None
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def _find_diamond(b: _Branch):
|
|
829
|
+
"""Return ``(w, f, relname, body)`` for an unexpanded diamond, or None."""
|
|
830
|
+
for w in b.tv:
|
|
831
|
+
for f in b.tv[w]:
|
|
832
|
+
if (w, f) in b.expanded:
|
|
833
|
+
continue
|
|
834
|
+
kind = _decompose(f)
|
|
835
|
+
if kind[0] == "dia":
|
|
836
|
+
return (w, f, kind[1], kind[2])
|
|
837
|
+
return None
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _blocked(b: _Branch, w: int, relname: str, ctx: _Ctx) -> bool:
|
|
841
|
+
"""Subset-blocking for transitive relations: an earlier world subsumes ``w``.
|
|
842
|
+
|
|
843
|
+
Only applied when the relation is transitive (where unbounded regress is
|
|
844
|
+
otherwise possible); sound for the K4 family and, with the counter-model
|
|
845
|
+
verification downstream, safe for S5/B too.
|
|
846
|
+
"""
|
|
847
|
+
if "trans" not in ctx.conds(relname):
|
|
848
|
+
return False
|
|
849
|
+
sw = b.tv.get(w, _OrderedSet())
|
|
850
|
+
for u in b.tv:
|
|
851
|
+
if u < w and sw <= b.tv[u]:
|
|
852
|
+
return True
|
|
853
|
+
return False
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _find_seriality(b: _Branch, ctx: _Ctx):
|
|
857
|
+
"""A serial relation with a box obligation at a world that has no successor."""
|
|
858
|
+
for (w, rel, _body) in b.boxes:
|
|
859
|
+
if "serial" not in ctx.conds(rel):
|
|
860
|
+
continue
|
|
861
|
+
if not any(a == w for (a, _v) in b.rels.get(rel, ())):
|
|
862
|
+
return (w, rel)
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _build_model(b: _Branch, ctx: _Ctx) -> KripkeModel:
|
|
867
|
+
"""Read an open saturated branch off as a Kripke model of the frame class of ``ctx``.
|
|
868
|
+
|
|
869
|
+
The valuation is the atoms of each world. A serial relation also needs a successor for a
|
|
870
|
+
world that has none, and the search only gave one to a world with a box obligation, so
|
|
871
|
+
every other dead end of such a relation sees itself here: the self-loop adds no obligation
|
|
872
|
+
(the world has no box of that relation to satisfy) and keeps a transitive, symmetric or
|
|
873
|
+
euclidean relation closed (a world with no successor has no path through it, and a
|
|
874
|
+
euclidean relation with an edge into the world has the loop already).
|
|
875
|
+
|
|
876
|
+
A relation the formulas read but the branch never used (its operator sits in a disjunct
|
|
877
|
+
the branch does not take) has no edge on the branch, and an absent relation is the empty
|
|
878
|
+
relation, which is neither reflexive nor serial. It is completed the same way: when its
|
|
879
|
+
system asks for reflexivity or seriality every world sees itself. Nothing the branch
|
|
880
|
+
asserts reads that relation (an assertion of one of its operators would have made it
|
|
881
|
+
live), so the loops cannot change what the branch makes true; a transitive, symmetric
|
|
882
|
+
or euclidean relation that has only loops is still all three. A relation whose system
|
|
883
|
+
asks for none of reflexivity and seriality stays empty, which is all of them vacuously.
|
|
884
|
+
|
|
885
|
+
The caller checks the result against the formula and the frame (see :func:`_fits_frame`
|
|
886
|
+
and :func:`modal_countermodel`), which is what keeps the one construct a loop can change
|
|
887
|
+
(a distributed-knowledge box over relations that are all dead ends at the world) honest.
|
|
888
|
+
"""
|
|
889
|
+
valuation = {}
|
|
890
|
+
always = {key_text(fact) for fact in b.facts}
|
|
891
|
+
for w, s in b.tv.items():
|
|
892
|
+
valuation[w] = {key_text(f) for f in s
|
|
893
|
+
if isinstance(f, Atom) and not is_truth_constant(f)} | always
|
|
894
|
+
relations = {r: set(e) for r, e in b.rels.items()}
|
|
895
|
+
worlds = set(b.tv) | {0}
|
|
896
|
+
completed = _OrderedSet()
|
|
897
|
+
for rel in _relnames(b):
|
|
898
|
+
completed.add(rel)
|
|
899
|
+
for rel in ctx.mentioned:
|
|
900
|
+
completed.add(rel)
|
|
901
|
+
for rel in completed:
|
|
902
|
+
conds = ctx.conds(rel)
|
|
903
|
+
if "refl" in conds or "serial" in conds:
|
|
904
|
+
edges = relations.setdefault(rel, set())
|
|
905
|
+
if "refl" in conds:
|
|
906
|
+
edges.update((w, w) for w in worlds)
|
|
907
|
+
seeing = {a for (a, _v) in edges}
|
|
908
|
+
edges.update((w, w) for w in worlds if w not in seeing)
|
|
909
|
+
return KripkeModel(worlds, relations, valuation)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _frame_condition_holds(condition: str, edges, worlds) -> bool:
|
|
913
|
+
"""Whether the finite frame ``(worlds, edges)`` satisfies one of the conditions this tableau has rules for.
|
|
914
|
+
|
|
915
|
+
The five conditions of :data:`_TABLEAU_CONDITIONS`, read straight off their definitions in
|
|
916
|
+
:data:`~unicode_logic_kit.fol.frames.FRAME_CONDITIONS` in time linear in the edges and their
|
|
917
|
+
out-degrees. (:func:`~unicode_logic_kit.fol.frames.holds_on_finite_frame` answers the same
|
|
918
|
+
question for every condition of the registry, but its cost grows with about the fourth power
|
|
919
|
+
of the number of worlds: 26 ms for a transitive chain of 40 worlds against under 1 ms here,
|
|
920
|
+
and a tableau may return a model of several hundred worlds. The tests hold this check to
|
|
921
|
+
the registry's on every frame of up to three worlds.)
|
|
922
|
+
"""
|
|
923
|
+
successors: dict = {}
|
|
924
|
+
for (a, v) in edges:
|
|
925
|
+
successors.setdefault(a, set()).add(v)
|
|
926
|
+
if condition == "refl":
|
|
927
|
+
return all(w in successors.get(w, ()) for w in worlds)
|
|
928
|
+
if condition == "serial":
|
|
929
|
+
return all(w in successors for w in worlds)
|
|
930
|
+
if condition == "sym":
|
|
931
|
+
return all(a in successors.get(v, ()) for (a, v) in edges)
|
|
932
|
+
if condition == "trans":
|
|
933
|
+
return all(u in successors[a] for (a, v) in edges for u in successors.get(v, ()))
|
|
934
|
+
if condition == "eucl":
|
|
935
|
+
return all(u in successors.get(v, ()) for succs in successors.values()
|
|
936
|
+
for v in succs for u in succs)
|
|
937
|
+
raise ValueError(f"modal_tableau: no check for the frame condition {condition!r}")
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _fits_frame(model: KripkeModel, ctx: _Ctx) -> bool:
|
|
941
|
+
"""True iff every relation of ``model`` and every relation the formulas read satisfies
|
|
942
|
+
the frame conditions of its system.
|
|
943
|
+
|
|
944
|
+
A relation the formulas read that the model does not list is the empty relation (the
|
|
945
|
+
model's own reading of a missing name), and it is checked as such: a check over the
|
|
946
|
+
listed relations alone passes a model with no entry for a reflexive or serial relation.
|
|
947
|
+
"""
|
|
948
|
+
names = list(model.relations)
|
|
949
|
+
names.extend(rel for rel in ctx.mentioned if rel not in model.relations)
|
|
950
|
+
for rel in names:
|
|
951
|
+
edges = model.relations.get(rel, ())
|
|
952
|
+
for condition in ctx.conds(rel):
|
|
953
|
+
if not _frame_condition_holds(condition, edges, model.worlds):
|
|
954
|
+
return False
|
|
955
|
+
return True
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def _makes_roots_true(model: KripkeModel, ctx: _Ctx) -> bool:
|
|
959
|
+
"""False iff ``model``, read as it is, makes one of the root formulas false at world 0.
|
|
960
|
+
|
|
961
|
+
A branch can be open with a formula on it that this tableau has no rule for (a negated
|
|
962
|
+
distributed-knowledge formula, a temporal closure operator): the formula stays on the
|
|
963
|
+
branch and the model read off it need not make it true. Whether it does is not something
|
|
964
|
+
the branch can tell, so the evaluator of the kit is asked. A formula the evaluator cannot
|
|
965
|
+
read in this model is left to the caller, which makes the same check and says so.
|
|
966
|
+
"""
|
|
967
|
+
for f in ctx.roots:
|
|
968
|
+
try:
|
|
969
|
+
if not satisfies_modal(f, model, 0):
|
|
970
|
+
return False
|
|
971
|
+
except (NotImplementedError, ValueError, TypeError, KeyError, RecursionError):
|
|
972
|
+
return True
|
|
973
|
+
return True
|
|
974
|
+
|
|
975
|
+
|
|
976
|
+
def _solve(b: _Branch, ctx: _Ctx):
|
|
977
|
+
"""Depth-first saturation of one branch.
|
|
978
|
+
|
|
979
|
+
Returns ``("closed", None)``, ``("open", model)``, or ``("unknown", None)``.
|
|
980
|
+
"""
|
|
981
|
+
while True:
|
|
982
|
+
if not ctx.tick():
|
|
983
|
+
return ("unknown", None)
|
|
984
|
+
if _closes(b):
|
|
985
|
+
return ("closed", None)
|
|
986
|
+
|
|
987
|
+
# 1) propositional + box + frame saturation to fixpoint
|
|
988
|
+
progressed = True
|
|
989
|
+
while progressed:
|
|
990
|
+
if not ctx.tick():
|
|
991
|
+
return ("unknown", None)
|
|
992
|
+
progressed = False
|
|
993
|
+
if _expand_simple(b):
|
|
994
|
+
progressed = True
|
|
995
|
+
if _frame_close(b, ctx):
|
|
996
|
+
progressed = True
|
|
997
|
+
if _close_distributed(b):
|
|
998
|
+
progressed = True
|
|
999
|
+
if _apply_boxes(b, ctx):
|
|
1000
|
+
progressed = True
|
|
1001
|
+
if _closes(b):
|
|
1002
|
+
return ("closed", None)
|
|
1003
|
+
|
|
1004
|
+
# 2) a branching formula?
|
|
1005
|
+
beta = _find_beta(b)
|
|
1006
|
+
if beta is not None:
|
|
1007
|
+
w, f, options = beta
|
|
1008
|
+
all_closed = True
|
|
1009
|
+
for opt in options:
|
|
1010
|
+
child = b.copy()
|
|
1011
|
+
child.expanded.add((w, f))
|
|
1012
|
+
for comp in opt:
|
|
1013
|
+
_assert(child, w, comp)
|
|
1014
|
+
res, model = _solve(child, ctx)
|
|
1015
|
+
if res == "open":
|
|
1016
|
+
return ("open", model)
|
|
1017
|
+
if res != "closed":
|
|
1018
|
+
all_closed = False
|
|
1019
|
+
return ("closed", None) if all_closed else ("unknown", None)
|
|
1020
|
+
|
|
1021
|
+
# 3) an unfulfilled diamond?
|
|
1022
|
+
dia = _find_diamond(b)
|
|
1023
|
+
if dia is not None:
|
|
1024
|
+
w, f, relname, body = dia
|
|
1025
|
+
b.expanded.add((w, f))
|
|
1026
|
+
if _blocked(b, w, relname, ctx):
|
|
1027
|
+
continue
|
|
1028
|
+
if b.wcount >= ctx.max_worlds:
|
|
1029
|
+
return ("unknown", None)
|
|
1030
|
+
v = b.wcount
|
|
1031
|
+
b.wcount += 1
|
|
1032
|
+
b.tv.setdefault(v, _OrderedSet())
|
|
1033
|
+
b.rels.setdefault(relname, set()).add((w, v))
|
|
1034
|
+
_assert(b, v, body)
|
|
1035
|
+
continue
|
|
1036
|
+
|
|
1037
|
+
# 4) seriality witness for a box-bearing world with no successor
|
|
1038
|
+
ser = _find_seriality(b, ctx)
|
|
1039
|
+
if ser is not None:
|
|
1040
|
+
w, relname = ser
|
|
1041
|
+
if b.wcount >= ctx.max_worlds:
|
|
1042
|
+
return ("unknown", None)
|
|
1043
|
+
v = b.wcount
|
|
1044
|
+
b.wcount += 1
|
|
1045
|
+
b.tv.setdefault(v, _OrderedSet())
|
|
1046
|
+
b.rels.setdefault(relname, set()).add((w, v))
|
|
1047
|
+
continue
|
|
1048
|
+
|
|
1049
|
+
# 5) saturated and open: the model that is read off must be one of the frame class and
|
|
1050
|
+
# must make the root formulas true. A branch whose model is not is "unknown" and the
|
|
1051
|
+
# search goes on with the branches that are left, as it does for any other bound.
|
|
1052
|
+
model = _build_model(b, ctx)
|
|
1053
|
+
if _fits_frame(model, ctx) and _makes_roots_true(model, ctx):
|
|
1054
|
+
return ("open", model)
|
|
1055
|
+
return ("unknown", None)
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
#: How this route reads an atom — the clause :func:`_reject_equality` hands the shared
|
|
1059
|
+
#: refusal so the message says why identity cannot be read here.
|
|
1060
|
+
_EQUALITY_ROUTE = "the propositional modal tableau"
|
|
1061
|
+
_EQUALITY_ATOM_READING = ("an atom is a propositional letter: a branch closes on a "
|
|
1062
|
+
"syntactic complement and an open branch is read off as "
|
|
1063
|
+
"a valuation of rendered atom keys")
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _reject_equality(formulas) -> None:
|
|
1067
|
+
"""Refuse an equality / disequality atom ANYWHERE in ``formulas``, by name.
|
|
1068
|
+
|
|
1069
|
+
A whole-tree scan of every formula, run by :func:`_run` before anything else
|
|
1070
|
+
touches them (see the module docstring's "Equality is NOT interpreted here").
|
|
1071
|
+
It walks the formulas exactly as the caller wrote them — announcement operators
|
|
1072
|
+
and all, ahead of :func:`~unicode_logic_kit.fol.pal.reduce_announcements` — so an
|
|
1073
|
+
atom inside an announcement or inside a quantifier this tableau would treat as an
|
|
1074
|
+
opaque literal is refused too, not only one in the propositional skeleton.
|
|
1075
|
+
"""
|
|
1076
|
+
for f in formulas:
|
|
1077
|
+
reject_equality_in(f, "modal_tableau", _EQUALITY_ROUTE,
|
|
1078
|
+
atom_reading=_EQUALITY_ATOM_READING)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
#: The cross-family bridge names the HOL / qml routes accept. Listed here only so
|
|
1082
|
+
#: the refusal below can name them; this module implements NONE of them.
|
|
1083
|
+
_KNOWN_BRIDGES = ("knowledge_implies_belief", "sincerity", "ought_implies_can")
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _check_bridges(bridges) -> None:
|
|
1087
|
+
"""Refuse any ``bridges=`` request, naming the routes that can honour it.
|
|
1088
|
+
|
|
1089
|
+
A bridge is a frame condition on TWO relations at once; this tableau's
|
|
1090
|
+
structural rules (``_frame_close`` / ``_find_seriality``) each work inside a
|
|
1091
|
+
single relation, so there is no rule to apply and no sound way to approximate
|
|
1092
|
+
one. Accepting the argument and ignoring it would decide a strictly WEAKER
|
|
1093
|
+
logic than the caller asked for and would make this module disagree with the
|
|
1094
|
+
HOL routes on the same formula — so it raises instead, in the same style as the
|
|
1095
|
+
GL guard below: name the boundary, name what does express it.
|
|
1096
|
+
"""
|
|
1097
|
+
if not bridges:
|
|
1098
|
+
return
|
|
1099
|
+
if isinstance(bridges, str):
|
|
1100
|
+
bridges = [bridges]
|
|
1101
|
+
raise NotImplementedError(
|
|
1102
|
+
f"modal_tableau: cross-family bridges {sorted(set(bridges))!r} are not "
|
|
1103
|
+
"supported by this tableau — a bridge constrains two DIFFERENT "
|
|
1104
|
+
"accessibility relations at once (e.g. rb ⊆ rk for K_aφ → "
|
|
1105
|
+
"B_aφ), and every structural rule here acts inside a single relation. "
|
|
1106
|
+
"Use a route that emits the bridge as an axiom: fol.qml.qml_is_valid / "
|
|
1107
|
+
"qml_axioms, or hol.isabelle_modal.to_isabelle_modal (with "
|
|
1108
|
+
"hol.isabelle_runner.isabelle_decide_modal) / "
|
|
1109
|
+
f"hol.thf_modal.to_thf_modal_full. Known bridges: {list(_KNOWN_BRIDGES)}.")
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _check_frame(frame: str, systems) -> None:
|
|
1113
|
+
_check_one_frame(frame)
|
|
1114
|
+
for fam, sys in (systems or {}).items():
|
|
1115
|
+
if fam not in ("epistemic", "doxastic", "deontic", "temporal"):
|
|
1116
|
+
raise ValueError(
|
|
1117
|
+
f"modal_tableau: unknown system family {fam!r} (use epistemic / "
|
|
1118
|
+
"doxastic / deontic / temporal).")
|
|
1119
|
+
_check_one_frame(sys, what=f"system {sys!r} for {fam}")
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _check_one_frame(frame: str, what: str = "") -> None:
|
|
1123
|
+
"""Resolve a frame name and refuse every condition this tableau has no
|
|
1124
|
+
rule for — by name, with the route that does carry it named too."""
|
|
1125
|
+
try:
|
|
1126
|
+
conds = resolve_frame(frame)
|
|
1127
|
+
except ValueError as exc:
|
|
1128
|
+
raise ValueError(f"modal_tableau: {exc}") from None
|
|
1129
|
+
require_supported(
|
|
1130
|
+
f"modal_tableau ({what})" if what else "modal_tableau",
|
|
1131
|
+
conds, _TABLEAU_CONDITIONS,
|
|
1132
|
+
hint="This labelled tableau has rules for reflexivity, transitivity, "
|
|
1133
|
+
"symmetry, seriality and euclideanness only. Use the "
|
|
1134
|
+
"first-order route fol.qml (Z3) for the other first-order "
|
|
1135
|
+
"conditions, or the higher-order embeddings — "
|
|
1136
|
+
"hol.isabelle_modal.to_isabelle_modal / isabelle_decide_modal "
|
|
1137
|
+
"and hol.thf_modal.to_thf_modal_full — for the ones that are "
|
|
1138
|
+
"not first-order definable at all (GL, S4.1, Grz); for a "
|
|
1139
|
+
"bounded REFUTATION (not a proof) of those three, "
|
|
1140
|
+
"atp.kripke_enum.modal_enum_search decides them directly via "
|
|
1141
|
+
"their finite frame characterisation.")
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _refuse_unsupported_constructs(formulas) -> None:
|
|
1145
|
+
"""Refuse, by name, a ↓ binder, a nominal / ``@`` or a counterfactual anywhere in ``formulas``."""
|
|
1146
|
+
for f in formulas:
|
|
1147
|
+
if _contains_down(f):
|
|
1148
|
+
# Checked BEFORE the general hybrid-constructs guard below so a
|
|
1149
|
+
# ↓-formula gets its own clear, undecidability-naming message
|
|
1150
|
+
# (N1) rather than being lumped under the general nominals/@
|
|
1151
|
+
# rejection — even though _contains_hybrid would ALSO already
|
|
1152
|
+
# catch it for free (Down.variable is a Nominal, walked
|
|
1153
|
+
# automatically — see _contains_down's own docstring).
|
|
1154
|
+
raise NotImplementedError(
|
|
1155
|
+
"modal_tableau: the ↓ binder is not supported by this labelled "
|
|
1156
|
+
"tableau — H(@,↓) validity is undecidable, and this tableau's "
|
|
1157
|
+
"rule set (like hybrid_is_valid's Z3 route) only ever "
|
|
1158
|
+
"soundly-and-completely covers DECIDABLE fragments. Use "
|
|
1159
|
+
"fol.modal_translation.down_is_valid (Z3, PROVED-only) or "
|
|
1160
|
+
"atp.kripke_enum.KripkeEnumBackend / modal_enum_search "
|
|
1161
|
+
"(bounded search, REFUTED-only), or evaluate directly with "
|
|
1162
|
+
"semantics.kripke.satisfies_modal.")
|
|
1163
|
+
if _contains_hybrid(f):
|
|
1164
|
+
raise NotImplementedError(
|
|
1165
|
+
"modal_tableau: hybrid constructs (nominals/@) are not supported "
|
|
1166
|
+
"by the modal tableau; use hybrid_is_valid or a KripkeModel.")
|
|
1167
|
+
if _contains_counterfactual(f):
|
|
1168
|
+
raise NotImplementedError(
|
|
1169
|
+
"modal_tableau: the counterfactuals □→/◇→ are evaluated over a "
|
|
1170
|
+
"similarity ordering (Lewis spheres), not an accessibility "
|
|
1171
|
+
"relation, so this tableau cannot decide them. Use cf_valid / "
|
|
1172
|
+
"cf_countermodel (bounded sphere-model search), cf_satisfies "
|
|
1173
|
+
"over a CounterfactualModel, or isabelle_decide_counterfactual.")
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _run(formulas, frame: str, systems, max_worlds: int, max_steps: int,
|
|
1177
|
+
bridges=None, timeout: Optional[int] = None, causes: Optional[List[str]] = None):
|
|
1178
|
+
"""Build the root branch from ``formulas`` at world 0 and search it.
|
|
1179
|
+
|
|
1180
|
+
``timeout`` (milliseconds, default none) is one more bound next to ``max_worlds`` and
|
|
1181
|
+
``max_steps``, checked at every step of the search and inside the frame closure and the
|
|
1182
|
+
box rule, which can outlast a step on a large edge set; so is Python's recursion limit
|
|
1183
|
+
(the search recurses once per branching formula along a branch, and every walk over a
|
|
1184
|
+
formula, before and during the search, once per level of its nesting). Past either bound
|
|
1185
|
+
the answer is ``("unknown", None)``, never an exception: also for a formula nested too
|
|
1186
|
+
deeply for those walks, whose guards below then cannot be run to their end.
|
|
1187
|
+
|
|
1188
|
+
``formulas`` is first run through :func:`~unicode_logic_kit.fol.pal.reduce_announcements`
|
|
1189
|
+
(a no-op on a formula with no Announce/AnnounceDiamond node), so every public
|
|
1190
|
+
entry point of this module DECIDES public-announcement formulas — no
|
|
1191
|
+
modal-tableau rule for Announce/AnnounceDiamond exists or is needed, since the
|
|
1192
|
+
reduction eliminates them into the ordinary modal fragment this tableau
|
|
1193
|
+
already handles, BEFORE tableau search ever begins. A temporal operator (or
|
|
1194
|
+
Would/Might/Nominal/At/a quantifier) found INSIDE an announcement's scope
|
|
1195
|
+
still raises — that is pal.reduce_announcements's own clean, precise
|
|
1196
|
+
NotImplementedError (unsound/undefined relativization, not this module's
|
|
1197
|
+
concern), propagated unchanged; see that module's docstring for why each
|
|
1198
|
+
case is rejected.
|
|
1199
|
+
|
|
1200
|
+
Hybrid constructs are rejected up front — a nominal names ONE world, a
|
|
1201
|
+
constraint this labelled tableau has no rule for, and treating it as an
|
|
1202
|
+
ordinary atom would produce wrong verdicts (e.g. it would refute ``@i i``).
|
|
1203
|
+
A ``bridges=`` request is rejected here for the same reason (see
|
|
1204
|
+
:func:`_check_bridges`), and so is an equality / disequality atom anywhere in
|
|
1205
|
+
``formulas`` (see :func:`_reject_equality` — scanned FIRST, over the formulas as
|
|
1206
|
+
given, so no later guard or search can answer before it has looked). Every
|
|
1207
|
+
public entry point funnels through here, so all three guards cover them all.
|
|
1208
|
+
"""
|
|
1209
|
+
formulas = list(formulas)
|
|
1210
|
+
# Every walk below is recursive, so a formula nested deeper than the interpreter's stack
|
|
1211
|
+
# allows ends one of them in a ``RecursionError``: the answer is then "unknown" (the
|
|
1212
|
+
# formula was not read to its end, so nothing may be said about it). The arguments that
|
|
1213
|
+
# do not depend on the formulas are still checked first, so a wrong ``frame`` is refused
|
|
1214
|
+
# whatever the depth.
|
|
1215
|
+
too_deep = False
|
|
1216
|
+
try:
|
|
1217
|
+
_reject_equality(formulas)
|
|
1218
|
+
formulas = [reduce_announcements(f) for f in formulas]
|
|
1219
|
+
except RecursionError:
|
|
1220
|
+
too_deep = True
|
|
1221
|
+
_check_frame(frame, systems)
|
|
1222
|
+
_check_bridges(bridges)
|
|
1223
|
+
if too_deep:
|
|
1224
|
+
if causes is not None:
|
|
1225
|
+
causes.append("nesting")
|
|
1226
|
+
return ("unknown", None)
|
|
1227
|
+
try:
|
|
1228
|
+
_refuse_unsupported_constructs(formulas)
|
|
1229
|
+
# The operators of an agent are filed under the relation named after the agent: two
|
|
1230
|
+
# different agent terms of one name (the numeral 1 and the constant '1') would be one
|
|
1231
|
+
# agent, and a branch could close for a formula about two.
|
|
1232
|
+
refuse_alike_agents(formulas, "modal_tableau")
|
|
1233
|
+
# A sorted constant ``c:S`` is the constant ``c`` and is an element of ``S`` at every
|
|
1234
|
+
# world (``semantics.kripke``'s module docstring): ``Mortal(c:S)`` and ``Mortal(c)``
|
|
1235
|
+
# are ONE letter, and ``S(c)`` is a letter true everywhere -- a branch with its
|
|
1236
|
+
# negation at any world is closed, and a model read off an open branch makes it
|
|
1237
|
+
# true at every world, so the countermodel is one the many-sorted reading allows.
|
|
1238
|
+
membership: List[Node] = []
|
|
1239
|
+
formulas = [_forget_constant_sorts(f, membership) for f in formulas]
|
|
1240
|
+
ctx = _Ctx(frame, systems, max_worlds, max_steps, timeout,
|
|
1241
|
+
mentioned=_mentioned_relations(formulas), roots=formulas)
|
|
1242
|
+
root = _Branch()
|
|
1243
|
+
root.facts = frozenset(membership)
|
|
1244
|
+
for f in formulas:
|
|
1245
|
+
_assert(root, 0, f)
|
|
1246
|
+
return _solve(root, ctx)
|
|
1247
|
+
except RecursionError:
|
|
1248
|
+
if causes is not None:
|
|
1249
|
+
causes.append("nesting")
|
|
1250
|
+
return ("unknown", None)
|
|
1251
|
+
except DeadlineReached:
|
|
1252
|
+
return ("unknown", None)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def modal_tableau_closed(formulas, frame: str = "K", systems=None,
|
|
1256
|
+
max_worlds: int = 400, max_steps: int = 200000,
|
|
1257
|
+
bridges=None, timeout: Optional[int] = None) -> bool:
|
|
1258
|
+
"""Return True iff ``formulas`` are jointly unsatisfiable at a world (the tableau closes).
|
|
1259
|
+
|
|
1260
|
+
Interprets the list as a set of formulas true at the same (root) world under the
|
|
1261
|
+
chosen ``frame`` (alethic system) and ``systems`` (per-family systems for
|
|
1262
|
+
epistemic / doxastic / deontic / temporal relations). Sound: a True is a real
|
|
1263
|
+
closed tableau. A False means "no closed tableau within the bound", never a
|
|
1264
|
+
positive satisfiability claim — use :func:`modal_countermodel` for that.
|
|
1265
|
+
|
|
1266
|
+
``bridges`` exists only to be REFUSED: any non-empty request raises
|
|
1267
|
+
``NotImplementedError`` pointing at the routes that implement cross-family
|
|
1268
|
+
bridges (see :func:`_check_bridges`). An ``=`` / ``≠`` atom anywhere in the formula raises ``NotImplementedError`` (see the
|
|
1269
|
+
module docstring's "Equality is NOT interpreted here").
|
|
1270
|
+
"""
|
|
1271
|
+
res, _ = _run(formulas, frame, systems, max_worlds, max_steps, bridges, timeout)
|
|
1272
|
+
return res == "closed"
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def is_modal_valid(formula: Node, frame: str = "K", systems=None,
|
|
1276
|
+
max_worlds: int = 400, max_steps: int = 200000,
|
|
1277
|
+
bridges=None, timeout: Optional[int] = None) -> bool:
|
|
1278
|
+
"""Return True iff ``formula`` is modally valid over ``frame`` — ``¬formula`` closes.
|
|
1279
|
+
|
|
1280
|
+
Sound: only the closed tableau yields True. An open or bound-exhausted search
|
|
1281
|
+
yields False (the formula is then invalid-or-unknown; :func:`modal_decide`
|
|
1282
|
+
distinguishes the two with a verified counter-model). A non-empty ``bridges``
|
|
1283
|
+
raises ``NotImplementedError`` (see :func:`_check_bridges`). An ``=`` / ``≠`` atom anywhere in the formula raises ``NotImplementedError`` (see the
|
|
1284
|
+
module docstring's "Equality is NOT interpreted here").
|
|
1285
|
+
"""
|
|
1286
|
+
res, _ = _run([Not(formula)], frame, systems, max_worlds, max_steps, bridges, timeout)
|
|
1287
|
+
return res == "closed"
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def modal_prove(premises, conclusion: Node, frame: str = "K", systems=None,
|
|
1291
|
+
max_worlds: int = 400, max_steps: int = 200000,
|
|
1292
|
+
bridges=None, timeout: Optional[int] = None) -> bool:
|
|
1293
|
+
"""Return True iff ``premises`` locally entail ``conclusion`` over ``frame``.
|
|
1294
|
+
|
|
1295
|
+
Local consequence: the tableau for ``premises ∪ {¬conclusion}`` at one world
|
|
1296
|
+
closes. Sound (a True is a closed tableau); incomplete only up to the bound.
|
|
1297
|
+
A non-empty ``bridges`` raises ``NotImplementedError``
|
|
1298
|
+
(see :func:`_check_bridges`). An ``=`` / ``≠`` atom anywhere in the formula raises ``NotImplementedError`` (see the
|
|
1299
|
+
module docstring's "Equality is NOT interpreted here").
|
|
1300
|
+
"""
|
|
1301
|
+
res, _ = _run(list(premises) + [Not(conclusion)], frame, systems,
|
|
1302
|
+
max_worlds, max_steps, bridges, timeout)
|
|
1303
|
+
return res == "closed"
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
def modal_countermodel(formula: Node, frame: str = "K", systems=None,
|
|
1307
|
+
max_worlds: int = 400, max_steps: int = 200000,
|
|
1308
|
+
bridges=None, timeout: Optional[int] = None):
|
|
1309
|
+
"""Return a Kripke model falsifying ``formula`` over ``frame``, or None.
|
|
1310
|
+
|
|
1311
|
+
None means the formula is valid (the tableau closed) **or** the search was
|
|
1312
|
+
inconclusive within the bound. The returned model is *verified* twice: it is only
|
|
1313
|
+
handed back when :func:`satisfies_modal` confirms the formula is false at its
|
|
1314
|
+
root world, and when every relation of it and every relation the formula reads satisfies
|
|
1315
|
+
the frame conditions of its system (a serial relation gives each of its dead ends a
|
|
1316
|
+
successor, itself, and a relation the formula reads that the search never used gets the
|
|
1317
|
+
loops its system asks for; see the module docstring), so a counter-model is never
|
|
1318
|
+
spurious and never a structure of another frame class. A relation the formula does not
|
|
1319
|
+
read is not part of the model. A non-empty ``bridges``
|
|
1320
|
+
raises ``NotImplementedError`` (see :func:`_check_bridges`). An ``=`` / ``≠`` atom anywhere in the formula raises ``NotImplementedError`` (see the
|
|
1321
|
+
module docstring's "Equality is NOT interpreted here").
|
|
1322
|
+
"""
|
|
1323
|
+
res, model = _run([Not(formula)], frame, systems, max_worlds, max_steps, bridges, timeout)
|
|
1324
|
+
if res != "open" or model is None:
|
|
1325
|
+
return None
|
|
1326
|
+
try:
|
|
1327
|
+
refuted = not satisfies_modal(formula, model, 0)
|
|
1328
|
+
except (NotImplementedError, ValueError, TypeError, KeyError, RecursionError):
|
|
1329
|
+
# The verifier cannot evaluate the formula in this model (e.g. an opaque
|
|
1330
|
+
# quantified construct with no domain information, or a formula nested deeper
|
|
1331
|
+
# than the evaluator can walk) — the candidate is unverifiable, so it must not
|
|
1332
|
+
# be handed back as a counter-model.
|
|
1333
|
+
refuted = False
|
|
1334
|
+
return model if refuted else None
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def modal_decide(formula: Node, frame: str = "K", systems=None,
|
|
1338
|
+
max_worlds: int = 400, max_steps: int = 200000,
|
|
1339
|
+
bridges=None, timeout: Optional[int] = None) -> str:
|
|
1340
|
+
"""Decide ``formula`` over ``frame``: ``"valid"`` / ``"invalid"`` / ``"unknown"``.
|
|
1341
|
+
|
|
1342
|
+
* ``"valid"`` — the tableau for ``¬formula`` closed (a sound proof).
|
|
1343
|
+
* ``"invalid"`` — an open branch yielded a counter-model **verified** by
|
|
1344
|
+
:func:`satisfies_modal` and a model of the frame (see :func:`modal_countermodel`).
|
|
1345
|
+
* ``"unknown"`` — the search hit the world/step bound or the deadline, the formula
|
|
1346
|
+
is nested too deeply for the recursive walks, or an open branch's
|
|
1347
|
+
model failed verification (so neither verdict is safe to assert).
|
|
1348
|
+
|
|
1349
|
+
Mirrors the valid / invalid / unknown contract of the local-Isabelle runner
|
|
1350
|
+
(:func:`~unicode_logic_kit.hol.isabelle_runner.isabelle_decide_modal`), but runs
|
|
1351
|
+
fully in-process with no external prover. A non-empty ``bridges`` raises
|
|
1352
|
+
``NotImplementedError`` rather than silently deciding the bridge-free logic
|
|
1353
|
+
(see :func:`_check_bridges`). An ``=`` / ``≠`` atom anywhere in the formula raises ``NotImplementedError`` (see the
|
|
1354
|
+
module docstring's "Equality is NOT interpreted here").
|
|
1355
|
+
"""
|
|
1356
|
+
return _decide_explained(formula, frame, systems, max_worlds, max_steps, bridges, timeout)[0]
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def _decide_explained(formula: Node, frame: str = "K", systems=None,
|
|
1360
|
+
max_worlds: int = 400, max_steps: int = 200000,
|
|
1361
|
+
bridges=None, timeout: Optional[int] = None) -> Tuple[str, bool]:
|
|
1362
|
+
""":func:`modal_decide`'s answer, and whether the recursion limit ended one of its walks.
|
|
1363
|
+
|
|
1364
|
+
The second component is true only next to ``"unknown"``: the formula was not read to its
|
|
1365
|
+
end (or its candidate countermodel could not be checked to its end) because a walk over
|
|
1366
|
+
it recursed deeper than the interpreter allows. A caller that reports the answer can then
|
|
1367
|
+
name the bound that was hit instead of guessing between it and the search budget.
|
|
1368
|
+
"""
|
|
1369
|
+
causes: List[str] = []
|
|
1370
|
+
res, model = _run([Not(formula)], frame, systems, max_worlds, max_steps, bridges, timeout,
|
|
1371
|
+
causes=causes)
|
|
1372
|
+
if res == "closed":
|
|
1373
|
+
return "valid", False
|
|
1374
|
+
if res == "open" and model is not None:
|
|
1375
|
+
try:
|
|
1376
|
+
if not satisfies_modal(formula, model, 0):
|
|
1377
|
+
return "invalid", False
|
|
1378
|
+
except RecursionError:
|
|
1379
|
+
causes.append("nesting") # unverifiable candidate → honest "unknown"
|
|
1380
|
+
except (NotImplementedError, ValueError, TypeError, KeyError):
|
|
1381
|
+
pass # unverifiable candidate → honest "unknown"
|
|
1382
|
+
return "unknown", bool(causes)
|