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