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,637 @@
|
|
|
1
|
+
"""Modal frame conditions and the named axioms they correspond to.
|
|
2
|
+
|
|
3
|
+
The kit reasons about modality on six routes — the standard translation
|
|
4
|
+
(:mod:`unicode_logic_kit.fol.qml`), a labelled tableau
|
|
5
|
+
(:mod:`unicode_logic_kit.atp.modal_tableau`), finite-frame enumeration
|
|
6
|
+
(:mod:`unicode_logic_kit.atp.kripke_enum`), natural deduction
|
|
7
|
+
(:mod:`unicode_logic_kit.atp.fitch`), the hybrid translation
|
|
8
|
+
(:mod:`unicode_logic_kit.fol.modal_translation`) and the higher-order
|
|
9
|
+
embeddings (:mod:`unicode_logic_kit.hol.isabelle_modal`,
|
|
10
|
+
:mod:`unicode_logic_kit.hol.thf_modal`). Each used to keep its OWN copy of the
|
|
11
|
+
frame table, which is how a route silently ended up understanding fewer
|
|
12
|
+
systems than its neighbour. This module is the single source of truth they
|
|
13
|
+
all read: what a named system consists of, what each condition means, and —
|
|
14
|
+
the part that makes it checkable — the modal axiom each condition
|
|
15
|
+
corresponds to.
|
|
16
|
+
|
|
17
|
+
The correspondence is not decoration. ``tests/test_modal_frames.py``
|
|
18
|
+
BRUTE-FORCES it: for every first-order condition here, over every frame on
|
|
19
|
+
up to three worlds and every valuation, "the axiom is valid on this frame"
|
|
20
|
+
and "the frame satisfies this condition" must agree. A wrong entry in this
|
|
21
|
+
module therefore fails a test rather than quietly teaching one route a
|
|
22
|
+
different modal logic than another.
|
|
23
|
+
|
|
24
|
+
The Geach family
|
|
25
|
+
-----------------
|
|
26
|
+
Most named conditions are instances of ONE schema. With the Scott–Lemmon
|
|
27
|
+
(Geach) coordinates ``G(m, n, r, s)``::
|
|
28
|
+
|
|
29
|
+
axiom ◇^m □^n p → □^r ◇^s p
|
|
30
|
+
condition ∀w,u,v (w R^m u ∧ w R^r v → ∃t (u R^n t ∧ v R^s t))
|
|
31
|
+
|
|
32
|
+
reflexivity is ``G(0,1,0,0)``, transitivity ``G(0,1,2,0)``, symmetry
|
|
33
|
+
``G(0,0,1,1)``, seriality ``G(0,1,0,1)``, euclideanness ``G(1,0,1,1)``,
|
|
34
|
+
directedness ``G(1,1,1,1)``, partial functionality ``G(1,0,1,0)`` and
|
|
35
|
+
density ``G(0,2,1,0)``. The named conditions keep their own hand-written
|
|
36
|
+
axioms (readable, and unchanged since before this module existed), and a
|
|
37
|
+
test proves each equivalent to the Geach-generated one — so the unification
|
|
38
|
+
is machine-checked rather than asserted in a comment.
|
|
39
|
+
|
|
40
|
+
``G(m,n,r,s)`` is also accepted directly as a frame name, which is what
|
|
41
|
+
makes the infinite family reachable without inventing a name per instance::
|
|
42
|
+
|
|
43
|
+
qml_is_valid(formula, frame="G(1,1,1,1)") # ≡ frame=".2"-style
|
|
44
|
+
|
|
45
|
+
What is NOT first-order definable
|
|
46
|
+
----------------------------------
|
|
47
|
+
Three named axioms have no first-order frame condition at all: Löb
|
|
48
|
+
(``□(□p→p)→□p``, the GL system), McKinsey (``□◇p→◇□p``, S4.1) and
|
|
49
|
+
Grzegorczyk (``□(□(p→□p)→p)→p``, Grz). They are marked
|
|
50
|
+
``first_order=False`` and carried ONLY by the higher-order routes, which
|
|
51
|
+
assert the schema itself, quantified over propositions. Every route that
|
|
52
|
+
reasons over an unbounded domain of worlds via a single first-order sentence
|
|
53
|
+
(:mod:`unicode_logic_kit.fol.qml`, :mod:`unicode_logic_kit.fol.modal_translation`,
|
|
54
|
+
:mod:`unicode_logic_kit.atp.fitch`, the labelled tableau) refuses them by name
|
|
55
|
+
— the boundary is stated, never approximated.
|
|
56
|
+
|
|
57
|
+
On a *finite, bounded* frame each of the three still has a purely
|
|
58
|
+
STRUCTURAL characterisation instead of a first-order sentence — irreflexive
|
|
59
|
+
(Löb, alongside the separately-listed "trans"), antisymmetric (Grz,
|
|
60
|
+
alongside "refl"/"trans") and "every world reaches some terminal point, a
|
|
61
|
+
world whose only successor is itself" (McKinsey, as the registry only ever
|
|
62
|
+
uses it: alongside "refl"/"trans", i.e. restricted to preorders). That is
|
|
63
|
+
what lets :func:`holds_on_finite_frame` decide all three and
|
|
64
|
+
:mod:`unicode_logic_kit.atp.kripke_enum` refute them by bounded finite-model
|
|
65
|
+
search — checked, not merely claimed, in ``tests/test_modal_frame_registry.py``
|
|
66
|
+
and ``tests/test_finite_frame_conditions.py``. It does not make them
|
|
67
|
+
first-order over frames of every cardinality, so :func:`unguarded_frame_axiom`
|
|
68
|
+
and the routes named above still refuse them exactly as before.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
import re
|
|
72
|
+
from typing import Callable, Dict, FrozenSet, Iterable, Optional, Tuple
|
|
73
|
+
|
|
74
|
+
from ._identifiers import variable_pattern
|
|
75
|
+
from .nodes import (
|
|
76
|
+
And, Atom, Box, Diamond, Implies, Node, Not, Or, Quantifier, Variable,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"FrameCondition", "FRAME_CONDITIONS", "FRAMES", "GeachSpec",
|
|
81
|
+
"resolve_frame", "parse_geach", "geach_axiom", "modal_axiom",
|
|
82
|
+
"MODAL_AXIOMS", "holds_on_finite_frame", "is_first_order",
|
|
83
|
+
"UnsupportedFrameCondition", "unguarded_frame_axiom",
|
|
84
|
+
"require_supported", "AXIOM_ALIASES",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class UnsupportedFrameCondition(NotImplementedError):
|
|
89
|
+
"""A route was handed a frame condition it cannot express soundly.
|
|
90
|
+
|
|
91
|
+
Raised instead of ignoring the condition — a route that silently drops
|
|
92
|
+
"directed" from ``S4.2`` would report countermodels that the frame
|
|
93
|
+
class excludes, which is exactly the failure this exception exists to
|
|
94
|
+
make impossible.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# The Geach / Scott–Lemmon family
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
class GeachSpec(tuple):
|
|
103
|
+
"""The four Scott–Lemmon coordinates ``(m, n, r, s)`` of ``◇^m □^n p →
|
|
104
|
+
□^r ◇^s p``."""
|
|
105
|
+
|
|
106
|
+
__slots__ = ()
|
|
107
|
+
|
|
108
|
+
def __new__(cls, m: int, n: int, r: int, s: int):
|
|
109
|
+
for value in (m, n, r, s):
|
|
110
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"GeachSpec: coordinates must be non-negative ints, got "
|
|
113
|
+
f"{(m, n, r, s)!r}")
|
|
114
|
+
return super().__new__(cls, (m, n, r, s))
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def m(self) -> int:
|
|
118
|
+
return self[0]
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def n(self) -> int:
|
|
122
|
+
return self[1]
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def r(self) -> int:
|
|
126
|
+
return self[2]
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def s(self) -> int:
|
|
130
|
+
return self[3]
|
|
131
|
+
|
|
132
|
+
def __repr__(self) -> str: # pragma: no cover - repr
|
|
133
|
+
return f"G({self.m},{self.n},{self.r},{self.s})"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_geach(name: str) -> Optional[GeachSpec]:
|
|
137
|
+
"""``"G(1,1,1,1)"`` → :class:`GeachSpec`; ``None`` if ``name`` is not one.
|
|
138
|
+
|
|
139
|
+
Accepts the condition spelling ``"geach:1,1,1,1"`` too — that is how a
|
|
140
|
+
Geach condition travels inside a frame's condition tuple, so every route
|
|
141
|
+
can keep treating conditions as plain strings.
|
|
142
|
+
"""
|
|
143
|
+
text = name.strip()
|
|
144
|
+
if text.startswith("geach:"):
|
|
145
|
+
body = text[len("geach:"):]
|
|
146
|
+
elif text.startswith("G(") and text.endswith(")"):
|
|
147
|
+
body = text[2:-1]
|
|
148
|
+
else:
|
|
149
|
+
return None
|
|
150
|
+
parts = [p.strip() for p in body.split(",")]
|
|
151
|
+
if len(parts) != 4 or not all(p.isdigit() for p in parts):
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"frames: {name!r} is not a Geach spec — expected G(m,n,r,s) with "
|
|
154
|
+
"four non-negative integers, e.g. G(1,1,1,1)")
|
|
155
|
+
return GeachSpec(*(int(p) for p in parts))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def geach_condition_name(spec: GeachSpec) -> str:
|
|
159
|
+
"""The condition string a frame table stores for ``spec``."""
|
|
160
|
+
return f"geach:{spec.m},{spec.n},{spec.r},{spec.s}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _iterate(op, times: int, formula: Node) -> Node:
|
|
164
|
+
for _ in range(times):
|
|
165
|
+
formula = op(formula)
|
|
166
|
+
return formula
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def geach_axiom(spec: GeachSpec, atom: str = "P") -> Node:
|
|
170
|
+
"""The modal schema ``◇^m □^n p → □^r ◇^s p`` for ``spec``."""
|
|
171
|
+
p = Atom(atom, ())
|
|
172
|
+
left = _iterate(Diamond, spec.m, _iterate(Box, spec.n, p))
|
|
173
|
+
right = _iterate(Box, spec.r, _iterate(Diamond, spec.s, p))
|
|
174
|
+
return Implies(left, right)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Named axioms
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def _p(name: str = "P") -> Node:
|
|
182
|
+
return Atom(name, ())
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
#: Axiom name → builder of its modal schema over propositional letters.
|
|
186
|
+
#: Two-letter schemas take both names; the rest ignore the second.
|
|
187
|
+
MODAL_AXIOMS: Dict[str, Callable[..., Node]] = {
|
|
188
|
+
# The Geach instances (their frame conditions are in FRAME_CONDITIONS).
|
|
189
|
+
"T": lambda p="P", q="Q": Implies(Box(_p(p)), _p(p)),
|
|
190
|
+
"D": lambda p="P", q="Q": Implies(Box(_p(p)), Diamond(_p(p))),
|
|
191
|
+
"B": lambda p="P", q="Q": Implies(_p(p), Box(Diamond(_p(p)))),
|
|
192
|
+
"4": lambda p="P", q="Q": Implies(Box(_p(p)), Box(Box(_p(p)))),
|
|
193
|
+
"5": lambda p="P", q="Q": Implies(Diamond(_p(p)), Box(Diamond(_p(p)))),
|
|
194
|
+
"CD": lambda p="P", q="Q": Implies(Diamond(_p(p)), Box(_p(p))),
|
|
195
|
+
"C4": lambda p="P", q="Q": Implies(Box(Box(_p(p))), Box(_p(p))),
|
|
196
|
+
".2": lambda p="P", q="Q": Implies(Diamond(Box(_p(p))), Box(Diamond(_p(p)))),
|
|
197
|
+
# Not Geach instances, but still first-order.
|
|
198
|
+
".3": lambda p="P", q="Q": Or(Box(Implies(Box(_p(p)), _p(q))),
|
|
199
|
+
Box(Implies(Box(_p(q)), _p(p)))),
|
|
200
|
+
"Mshift": lambda p="P", q="Q": Box(Implies(Box(_p(p)), _p(p))),
|
|
201
|
+
"Ver": lambda p="P", q="Q": Box(_p(p)),
|
|
202
|
+
# No first-order frame condition exists for these three.
|
|
203
|
+
"McKinsey": lambda p="P", q="Q": Implies(Box(Diamond(_p(p))),
|
|
204
|
+
Diamond(Box(_p(p)))),
|
|
205
|
+
"Loeb": lambda p="P", q="Q": Implies(Box(Implies(Box(_p(p)), _p(p))),
|
|
206
|
+
Box(_p(p))),
|
|
207
|
+
"Grz": lambda p="P", q="Q": Implies(
|
|
208
|
+
Box(Implies(Box(Implies(_p(p), Box(_p(p)))), _p(p))), _p(p)),
|
|
209
|
+
# Theorems of every normal modal logic — no frame condition, listed so a
|
|
210
|
+
# caller can ask for the formula and check that the kit validates it.
|
|
211
|
+
"K": lambda p="P", q="Q": Implies(Box(Implies(_p(p), _p(q))),
|
|
212
|
+
Implies(Box(_p(p)), Box(_p(q)))),
|
|
213
|
+
"N": lambda p="P", q="Q": Box(Or(_p(p), Not(_p(p)))),
|
|
214
|
+
"P": lambda p="P", q="Q": Not(Box(And(_p(p), Not(_p(p))))),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#: The alternative names the literature uses, mapped onto this module's.
|
|
218
|
+
#: ``M`` is deliberately NOT accepted for ``T``: this module uses ``Mshift``
|
|
219
|
+
#: for the shift-reflexivity axiom ``□(□p→p)``, and one letter cannot mean
|
|
220
|
+
#: both without inviting exactly the mistake a frame table must not make.
|
|
221
|
+
AXIOM_ALIASES: Dict[str, str] = {
|
|
222
|
+
"G": ".2", "G1": ".2", "C": ".2", # ◇□p → □◇p, one axiom
|
|
223
|
+
"H": ".3", # □(□p→q) ∨ □(□q→p)
|
|
224
|
+
"Q": "C4", # □□p → □p, one axiom
|
|
225
|
+
"W": "Loeb", # □(□p→p) → □p, one axiom
|
|
226
|
+
"L": "Loeb",
|
|
227
|
+
"Alt1": "CD", "Alt3": "CD", # ◇p→□p, ◇p∧◇q→◇(p∧q)
|
|
228
|
+
"Alt2": "Mshift", # ◇p→◇(p∧◇p)
|
|
229
|
+
"Dense": "C4", "Density": "C4",
|
|
230
|
+
".1": "McKinsey",
|
|
231
|
+
"Grzegorczyk": "Grz",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def modal_axiom(name: str, p: str = "P", q: str = "Q") -> Node:
|
|
236
|
+
"""The modal schema of a named axiom, over the given letters.
|
|
237
|
+
|
|
238
|
+
Accepts the aliases the literature uses (:data:`AXIOM_ALIASES`), so
|
|
239
|
+
``modal_axiom("W")`` and ``modal_axiom("Loeb")`` build the same formula.
|
|
240
|
+
"""
|
|
241
|
+
key = AXIOM_ALIASES.get(name, name)
|
|
242
|
+
if key not in MODAL_AXIOMS:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"frames: unknown modal axiom {name!r} (use one of "
|
|
245
|
+
f"{sorted(MODAL_AXIOMS)} or an alias {sorted(AXIOM_ALIASES)})")
|
|
246
|
+
return MODAL_AXIOMS[key](p, q)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# Frame conditions
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
class FrameCondition(tuple):
|
|
254
|
+
"""One frame condition: what it is called, what it means, which axiom it
|
|
255
|
+
corresponds to, and whether it is first-order definable at all."""
|
|
256
|
+
|
|
257
|
+
__slots__ = ()
|
|
258
|
+
|
|
259
|
+
def __new__(cls, name: str, axiom: str, description: str,
|
|
260
|
+
geach: Optional[GeachSpec] = None, first_order: bool = True):
|
|
261
|
+
return super().__new__(cls, (name, axiom, description, geach,
|
|
262
|
+
first_order))
|
|
263
|
+
|
|
264
|
+
name = property(lambda self: self[0])
|
|
265
|
+
axiom = property(lambda self: self[1])
|
|
266
|
+
description = property(lambda self: self[2])
|
|
267
|
+
geach = property(lambda self: self[3])
|
|
268
|
+
first_order = property(lambda self: self[4])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
#: Condition name → :class:`FrameCondition`. The ``geach`` coordinates are
|
|
272
|
+
#: checked against the hand-written axiom by the test suite.
|
|
273
|
+
FRAME_CONDITIONS: Dict[str, FrameCondition] = {
|
|
274
|
+
"refl": FrameCondition(
|
|
275
|
+
"refl", "T", "∀w Rww — reflexive", GeachSpec(0, 1, 0, 0)),
|
|
276
|
+
"trans": FrameCondition(
|
|
277
|
+
"trans", "4", "∀w,v,u (Rwv ∧ Rvu → Rwu) — transitive",
|
|
278
|
+
GeachSpec(0, 1, 2, 0)),
|
|
279
|
+
"sym": FrameCondition(
|
|
280
|
+
"sym", "B", "∀w,v (Rwv → Rvw) — symmetric", GeachSpec(0, 0, 1, 1)),
|
|
281
|
+
"serial": FrameCondition(
|
|
282
|
+
"serial", "D", "∀w ∃v Rwv — serial", GeachSpec(0, 1, 0, 1)),
|
|
283
|
+
"eucl": FrameCondition(
|
|
284
|
+
"eucl", "5", "∀w,v,u (Rwv ∧ Rwu → Rvu) — euclidean",
|
|
285
|
+
GeachSpec(1, 0, 1, 1)),
|
|
286
|
+
"directed": FrameCondition(
|
|
287
|
+
"directed", ".2",
|
|
288
|
+
"∀w,v,u (Rwv ∧ Rwu → ∃t (Rvt ∧ Rut)) — directed / convergent",
|
|
289
|
+
GeachSpec(1, 1, 1, 1)),
|
|
290
|
+
"functional": FrameCondition(
|
|
291
|
+
"functional", "CD",
|
|
292
|
+
"∀w,v,u (Rwv ∧ Rwu → v = u) — partially functional (at most one "
|
|
293
|
+
"successor)", GeachSpec(1, 0, 1, 0)),
|
|
294
|
+
"dense": FrameCondition(
|
|
295
|
+
"dense", "C4", "∀w,v (Rwv → ∃u (Rwu ∧ Ruv)) — dense",
|
|
296
|
+
GeachSpec(0, 2, 1, 0)),
|
|
297
|
+
"connected": FrameCondition(
|
|
298
|
+
"connected", ".3",
|
|
299
|
+
"∀w,v,u (Rwv ∧ Rwu → Rvu ∨ Ruv) — connected / no branching to the "
|
|
300
|
+
"right. Note there is deliberately NO 'or v = u' escape: that weaker "
|
|
301
|
+
"condition belongs to the OTHER common .3 formulation "
|
|
302
|
+
"(◇p ∧ ◇q → ◇(p ∧ ◇q) ∨ ◇(q ∧ ◇p)); for the □(□p→q) ∨ □(□q→p) form "
|
|
303
|
+
"used here the exact correspondent needs Rvv when v = u, and the "
|
|
304
|
+
"brute-force correspondence test confirms it on every frame up to "
|
|
305
|
+
"three worlds. Over reflexive frames the two coincide"),
|
|
306
|
+
"shift_refl": FrameCondition(
|
|
307
|
+
"shift_refl", "Mshift",
|
|
308
|
+
"∀w,v (Rwv → Rvv) — shift-reflexive (every accessible world is "
|
|
309
|
+
"reflexive; the axiom □(□p→p), also reached by ◇p→◇(p∧◇p))"),
|
|
310
|
+
"empty": FrameCondition(
|
|
311
|
+
"empty", "Ver", "∀w,v ¬Rwv — no accessible worlds at all (the Verum "
|
|
312
|
+
"system, where □p holds vacuously)"),
|
|
313
|
+
# Not first-order definable: carried only by the higher-order routes.
|
|
314
|
+
"loeb": FrameCondition(
|
|
315
|
+
"loeb", "Loeb",
|
|
316
|
+
"Gödel–Löb provability: transitive + converse well-founded — NOT "
|
|
317
|
+
"first-order definable; the higher-order routes assert the Löb schema "
|
|
318
|
+
"itself",
|
|
319
|
+
first_order=False),
|
|
320
|
+
"mckinsey": FrameCondition(
|
|
321
|
+
"mckinsey", "McKinsey",
|
|
322
|
+
"NOT first-order definable; the higher-order routes assert the "
|
|
323
|
+
"McKinsey schema □◇p → ◇□p itself", first_order=False),
|
|
324
|
+
"grz": FrameCondition(
|
|
325
|
+
"grz", "Grz",
|
|
326
|
+
"reflexive + transitive + antisymmetric (a partial order) — NOT "
|
|
327
|
+
"first-order definable over frames of every cardinality (this finite "
|
|
328
|
+
"characterisation, checked by holds_on_finite_frame, replaces the "
|
|
329
|
+
"vacuous-on-finite-frames 'no infinite ascending chain' condition; "
|
|
330
|
+
"antisymmetry is the load-bearing part — refl+trans alone is NOT "
|
|
331
|
+
"enough, e.g. the two-world full cluster is refl+trans but not "
|
|
332
|
+
"antisymmetric and fails the Grz schema); the higher-order routes "
|
|
333
|
+
"assert the Grzegorczyk schema itself", first_order=False),
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def is_first_order(condition: str) -> bool:
|
|
338
|
+
"""Whether ``condition`` has a first-order frame condition at all."""
|
|
339
|
+
if parse_geach(condition) is not None:
|
|
340
|
+
return True
|
|
341
|
+
spec = FRAME_CONDITIONS.get(condition)
|
|
342
|
+
if spec is None:
|
|
343
|
+
raise ValueError(f"frames: unknown frame condition {condition!r}")
|
|
344
|
+
return spec.first_order
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
# Named systems
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
#: System name → its frame conditions. ``D`` and ``KD`` (and ``B`` /
|
|
352
|
+
#: ``KTB``) are the same system under both spellings the literature uses.
|
|
353
|
+
FRAMES: Dict[str, Tuple[str, ...]] = {
|
|
354
|
+
"K": (),
|
|
355
|
+
"D": ("serial",),
|
|
356
|
+
"KD": ("serial",),
|
|
357
|
+
"T": ("refl",),
|
|
358
|
+
"KB": ("sym",),
|
|
359
|
+
"B": ("refl", "sym"), # Brouwer, K + T + B
|
|
360
|
+
"KTB": ("refl", "sym"),
|
|
361
|
+
"K4": ("trans",),
|
|
362
|
+
"K5": ("eucl",),
|
|
363
|
+
"K45": ("trans", "eucl"),
|
|
364
|
+
"KD4": ("serial", "trans"),
|
|
365
|
+
"KD5": ("serial", "eucl"),
|
|
366
|
+
"KD45": ("serial", "trans", "eucl"),
|
|
367
|
+
"S4": ("refl", "trans"),
|
|
368
|
+
"S5": ("refl", "trans", "sym"),
|
|
369
|
+
"S4.1": ("refl", "trans", "mckinsey"), # + McKinsey (HOL routes prove;
|
|
370
|
+
# kripke_enum refutes, bounded)
|
|
371
|
+
"S4.2": ("refl", "trans", "directed"), # convergent (.2)
|
|
372
|
+
"S4.3": ("refl", "trans", "connected"), # no-branching (.3)
|
|
373
|
+
"KCD": ("functional",), # ◇p → □p
|
|
374
|
+
"KC4": ("dense",), # □□p → □p
|
|
375
|
+
"KShift": ("shift_refl",), # □(□p → p)
|
|
376
|
+
"Ver": ("empty",), # □p — the Verum system
|
|
377
|
+
"GL": ("trans", "loeb"), # HOL routes prove; kripke_enum
|
|
378
|
+
# refutes, bounded
|
|
379
|
+
"Grz": ("refl", "trans", "grz"), # HOL routes prove; kripke_enum
|
|
380
|
+
# refutes, bounded
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def resolve_frame(frame: str) -> Tuple[str, ...]:
|
|
385
|
+
"""The conditions of a named system, or of a ``G(m,n,r,s)`` spec.
|
|
386
|
+
|
|
387
|
+
(Named ``resolve_frame`` rather than the more obvious
|
|
388
|
+
``frame_conditions``: that would collide with the
|
|
389
|
+
:data:`FRAME_CONDITIONS` registry on a case-insensitive filesystem —
|
|
390
|
+
Sphinx writes one stub file per documented name, and the two would
|
|
391
|
+
overwrite each other on Windows.)
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
ValueError: ``frame`` is neither a known system name nor a
|
|
395
|
+
well-formed Geach spec — the message lists what is available.
|
|
396
|
+
"""
|
|
397
|
+
spec = parse_geach(frame)
|
|
398
|
+
if spec is not None:
|
|
399
|
+
return (geach_condition_name(spec),)
|
|
400
|
+
if frame not in FRAMES:
|
|
401
|
+
raise ValueError(
|
|
402
|
+
f"unknown frame {frame!r} (use one of {sorted(FRAMES)}, or a "
|
|
403
|
+
"Scott–Lemmon spec like 'G(1,1,1,1)')")
|
|
404
|
+
return FRAMES[frame]
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def require_supported(route: str, conditions: Iterable[str],
|
|
408
|
+
supported: Iterable[str], *, hint: str = "") -> None:
|
|
409
|
+
"""Refuse, by name, any condition a route cannot express soundly.
|
|
410
|
+
|
|
411
|
+
The alternative — ignoring it — would make the route answer about a
|
|
412
|
+
LARGER frame class than the caller asked for, i.e. report countermodels
|
|
413
|
+
that the named system excludes. Every route calls this instead.
|
|
414
|
+
"""
|
|
415
|
+
allowed = set(supported)
|
|
416
|
+
for condition in conditions:
|
|
417
|
+
if condition in allowed:
|
|
418
|
+
continue
|
|
419
|
+
if parse_geach(condition) is not None and "geach" in allowed:
|
|
420
|
+
continue
|
|
421
|
+
spec = FRAME_CONDITIONS.get(condition)
|
|
422
|
+
what = spec.description if spec is not None else condition
|
|
423
|
+
raise UnsupportedFrameCondition(
|
|
424
|
+
f"{route}: the frame condition {condition!r} ({what}) is not "
|
|
425
|
+
f"expressible here." + (f" {hint}" if hint else ""))
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
# Finite frames — the checker the enumerator and the tests share
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
def _steps(edges: FrozenSet[Tuple[int, int]], n: int,
|
|
433
|
+
k: int) -> FrozenSet[Tuple[int, int]]:
|
|
434
|
+
"""``R^k`` over ``range(n)``: pairs joined by a path of EXACTLY ``k``
|
|
435
|
+
edges (``R^0`` is the identity)."""
|
|
436
|
+
reachable = {(w, w) for w in range(n)}
|
|
437
|
+
for _ in range(k):
|
|
438
|
+
reachable = {(w, v) for (w, mid) in reachable
|
|
439
|
+
for (mid2, v) in edges if mid == mid2}
|
|
440
|
+
return frozenset(reachable)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def holds_on_finite_frame(condition: str, edges: FrozenSet[Tuple[int, int]],
|
|
444
|
+
n: int) -> bool:
|
|
445
|
+
"""Does the finite frame ``(range(n), edges)`` satisfy ``condition``?
|
|
446
|
+
|
|
447
|
+
Every FIRST-ORDER condition in :data:`FRAME_CONDITIONS` is decided here,
|
|
448
|
+
Geach specs included. The three conditions marked ``first_order=False``
|
|
449
|
+
(``"loeb"``, ``"grz"``, ``"mckinsey"``) are decided here TOO, via the
|
|
450
|
+
structural characterisation each has on a finite frame instead of a
|
|
451
|
+
first-order sentence valid at every cardinality: ``"loeb"`` checks
|
|
452
|
+
irreflexivity (combined with the separately-listed ``"trans"`` by the
|
|
453
|
+
``"GL"`` system, that is "transitive + irreflexive", the standard
|
|
454
|
+
finite Gödel–Löb characterisation), ``"grz"`` checks antisymmetry
|
|
455
|
+
(combined with ``"refl"``/``"trans"`` by the ``"Grz"`` system, that is a
|
|
456
|
+
finite partial order), and ``"mckinsey"`` checks "every world reaches
|
|
457
|
+
some terminal point, a world whose only successor is itself" — the
|
|
458
|
+
registry only ever uses ``"mckinsey"`` alongside ``"refl"``/``"trans"``
|
|
459
|
+
(``"S4.1"``), i.e. restricted to preorders, and the characterisation is
|
|
460
|
+
scoped to exactly that generality. Delegating a finite check to
|
|
461
|
+
``entry.geach is None`` → :func:`_named_holds` covers all three, since
|
|
462
|
+
none of them is a Geach instance. This does NOT make them first-order
|
|
463
|
+
over frames of every cardinality — :func:`unguarded_frame_axiom` and the
|
|
464
|
+
routes that reason over an unbounded domain of worlds still refuse them
|
|
465
|
+
by name (see the module docstring). An unknown condition name still
|
|
466
|
+
raises :class:`ValueError`.
|
|
467
|
+
"""
|
|
468
|
+
spec = parse_geach(condition)
|
|
469
|
+
if spec is None:
|
|
470
|
+
entry = FRAME_CONDITIONS.get(condition)
|
|
471
|
+
if entry is None:
|
|
472
|
+
raise ValueError(f"frames: unknown frame condition {condition!r}")
|
|
473
|
+
if entry.geach is None:
|
|
474
|
+
return _named_holds(condition, edges, n)
|
|
475
|
+
spec = entry.geach
|
|
476
|
+
from_u = _steps(edges, n, spec.n)
|
|
477
|
+
from_v = _steps(edges, n, spec.s)
|
|
478
|
+
left = _steps(edges, n, spec.m)
|
|
479
|
+
right = _steps(edges, n, spec.r)
|
|
480
|
+
for (w, u) in left:
|
|
481
|
+
for (w2, v) in right:
|
|
482
|
+
if w != w2:
|
|
483
|
+
continue
|
|
484
|
+
if not any((u, t) in from_u and (v, t) in from_v
|
|
485
|
+
for t in range(n)):
|
|
486
|
+
return False
|
|
487
|
+
return True
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _named_holds(condition: str, edges: FrozenSet[Tuple[int, int]],
|
|
491
|
+
n: int) -> bool:
|
|
492
|
+
"""The conditions that are not Geach instances: the hand-written
|
|
493
|
+
first-order ones, plus the finite-frame structural checks for the three
|
|
494
|
+
conditions with no first-order frame axiom at every cardinality (Löb,
|
|
495
|
+
Grz, McKinsey — see :func:`holds_on_finite_frame`'s docstring)."""
|
|
496
|
+
if condition == "connected":
|
|
497
|
+
return all(not ((w, v) in edges and (w, u) in edges)
|
|
498
|
+
or (v, u) in edges or (u, v) in edges
|
|
499
|
+
for w in range(n) for v in range(n) for u in range(n))
|
|
500
|
+
if condition == "shift_refl":
|
|
501
|
+
return all((v, v) in edges for (_w, v) in edges)
|
|
502
|
+
if condition == "empty":
|
|
503
|
+
return not edges
|
|
504
|
+
if condition == "loeb":
|
|
505
|
+
# Gödel–Löb, finite characterisation: irreflexive. Combined with the
|
|
506
|
+
# "trans" the "GL" system supplies separately, this is "transitive +
|
|
507
|
+
# irreflexive" — on a FINITE frame that is exactly "transitive +
|
|
508
|
+
# converse well-founded" (a cycle, which transitivity would collapse
|
|
509
|
+
# into a self-loop, is the only way a finite domain could fail to be
|
|
510
|
+
# well-founded, and irreflexivity forbids self-loops).
|
|
511
|
+
return all((w, w) not in edges for w in range(n))
|
|
512
|
+
if condition == "grz":
|
|
513
|
+
# Grzegorczyk, finite characterisation: antisymmetric. Combined with
|
|
514
|
+
# "refl"/"trans" the "Grz" system supplies separately, this is a
|
|
515
|
+
# finite partial order — "no infinite strictly-ascending chain" is
|
|
516
|
+
# automatic once the frame is finite, so antisymmetry is the only
|
|
517
|
+
# load-bearing extra condition (see FRAME_CONDITIONS["grz"]).
|
|
518
|
+
return all(w == v for w in range(n) for v in range(n)
|
|
519
|
+
if (w, v) in edges and (v, w) in edges)
|
|
520
|
+
if condition == "mckinsey":
|
|
521
|
+
# McKinsey, restricted to preorders — the only generality the
|
|
522
|
+
# registry ever needs it in (alongside "refl"/"trans" via "S4.1"):
|
|
523
|
+
# every world reaches some terminal point, a world whose only
|
|
524
|
+
# successor is itself. One R-step suffices for "reaches" because the
|
|
525
|
+
# frame is transitive, so R already contains every multi-step path.
|
|
526
|
+
def _is_terminal(v: int) -> bool:
|
|
527
|
+
successors = {u for (w2, u) in edges if w2 == v}
|
|
528
|
+
return successors == {v}
|
|
529
|
+
return all(any((w, v) in edges and _is_terminal(v) for v in range(n))
|
|
530
|
+
for w in range(n))
|
|
531
|
+
raise ValueError(f"frames: unknown frame condition {condition!r}")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# ---------------------------------------------------------------------------
|
|
535
|
+
# One first-order emitter for the routes that need no world guard
|
|
536
|
+
# ---------------------------------------------------------------------------
|
|
537
|
+
|
|
538
|
+
_FORALL, _EXISTS = "∀", "∃"
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def unguarded_frame_axiom(condition: str, relation: str = "R", *,
|
|
542
|
+
prefix: str = "v") -> Node:
|
|
543
|
+
"""The first-order frame axiom of ``condition`` over ``relation``.
|
|
544
|
+
|
|
545
|
+
Written WITHOUT a sort guard, which is what the natural-deduction route
|
|
546
|
+
(:mod:`unicode_logic_kit.atp.fitch`) and the hybrid translation
|
|
547
|
+
(:mod:`unicode_logic_kit.fol.modal_translation`) need — both quantify over
|
|
548
|
+
worlds only. The quantified-modal route
|
|
549
|
+
(:mod:`unicode_logic_kit.fol.qml`) keeps its own ``World``-guarded
|
|
550
|
+
emitters, because there worlds and objects share one domain.
|
|
551
|
+
|
|
552
|
+
The axiom is closed over its own bound variables (named ``prefix`` + a
|
|
553
|
+
digit), so it can never capture anything in the formula it is conjoined
|
|
554
|
+
with. ``prefix`` must be a legal VARIABLE on its own — one term-valued
|
|
555
|
+
letter — because ``prefix`` + digits has to be a name the kit's own parser
|
|
556
|
+
reads back: the default used to be ``"_fw"``, and an axiom printed as
|
|
557
|
+
``∀_fw0 R(_fw0, _fw0)`` is text ``api.parse_any`` rejects (see
|
|
558
|
+
:func:`unicode_logic_kit.fol._identifiers.fresh_variables`).
|
|
559
|
+
|
|
560
|
+
Raises:
|
|
561
|
+
UnsupportedFrameCondition: ``condition`` has no first-order frame
|
|
562
|
+
condition (Löb, McKinsey, Grz).
|
|
563
|
+
ValueError: ``condition`` is not a known condition at all.
|
|
564
|
+
"""
|
|
565
|
+
if not re.fullmatch(variable_pattern(), prefix):
|
|
566
|
+
raise ValueError(
|
|
567
|
+
f"unguarded_frame_axiom: prefix {prefix!r} is not a legal variable "
|
|
568
|
+
f"name, so {prefix!r} + digits is not one either — the axiom would "
|
|
569
|
+
f"print as text this kit's own parser rejects")
|
|
570
|
+
w, u, v, s = (Variable(f"{prefix}0"), Variable(f"{prefix}1"),
|
|
571
|
+
Variable(f"{prefix}2"), Variable(f"{prefix}3"))
|
|
572
|
+
R = lambda a, b: Atom(relation, (a, b))
|
|
573
|
+
fa = lambda var, body: Quantifier(_FORALL, var, body)
|
|
574
|
+
ex = lambda var, body: Quantifier(_EXISTS, var, body)
|
|
575
|
+
|
|
576
|
+
spec = parse_geach(condition)
|
|
577
|
+
if spec is None:
|
|
578
|
+
entry = FRAME_CONDITIONS.get(condition)
|
|
579
|
+
if entry is None:
|
|
580
|
+
raise ValueError(f"frames: unknown frame condition {condition!r}")
|
|
581
|
+
if not entry.first_order:
|
|
582
|
+
raise UnsupportedFrameCondition(
|
|
583
|
+
f"frames: {condition!r} ({entry.description}) has no "
|
|
584
|
+
"first-order frame axiom")
|
|
585
|
+
if condition == "refl":
|
|
586
|
+
return fa(w, R(w, w))
|
|
587
|
+
if condition == "trans":
|
|
588
|
+
return fa(w, fa(u, fa(v, Implies(And(R(w, u), R(u, v)), R(w, v)))))
|
|
589
|
+
if condition == "sym":
|
|
590
|
+
return fa(w, fa(u, Implies(R(w, u), R(u, w))))
|
|
591
|
+
if condition == "serial":
|
|
592
|
+
return fa(w, ex(u, R(w, u)))
|
|
593
|
+
if condition == "eucl":
|
|
594
|
+
return fa(w, fa(u, fa(v, Implies(And(R(w, u), R(w, v)), R(u, v)))))
|
|
595
|
+
if condition == "directed":
|
|
596
|
+
return fa(w, fa(u, fa(v, Implies(
|
|
597
|
+
And(R(w, u), R(w, v)), ex(s, And(R(u, s), R(v, s)))))))
|
|
598
|
+
if condition == "connected":
|
|
599
|
+
return fa(w, fa(u, fa(v, Implies(
|
|
600
|
+
And(R(w, u), R(w, v)), Or(R(u, v), R(v, u))))))
|
|
601
|
+
if condition == "functional":
|
|
602
|
+
return fa(w, fa(u, fa(v, Implies(
|
|
603
|
+
And(R(w, u), R(w, v)), Atom("=", (u, v))))))
|
|
604
|
+
if condition == "dense":
|
|
605
|
+
return fa(w, fa(u, Implies(R(w, u), ex(v, And(R(w, v), R(v, u))))))
|
|
606
|
+
if condition == "shift_refl":
|
|
607
|
+
return fa(w, fa(u, Implies(R(w, u), R(u, u))))
|
|
608
|
+
if condition == "empty":
|
|
609
|
+
return fa(w, fa(u, Not(R(w, u))))
|
|
610
|
+
raise ValueError( # pragma: no cover - registry drift
|
|
611
|
+
f"frames: {condition!r} is registered but has no axiom builder")
|
|
612
|
+
|
|
613
|
+
counter = [0]
|
|
614
|
+
|
|
615
|
+
def path(a, b, k):
|
|
616
|
+
if k == 0:
|
|
617
|
+
return Atom("=", (a, b))
|
|
618
|
+
previous, mids, conj = a, [], None
|
|
619
|
+
for _ in range(k - 1):
|
|
620
|
+
z = Variable(f"{prefix}z{counter[0]}")
|
|
621
|
+
counter[0] += 1
|
|
622
|
+
mids.append(z)
|
|
623
|
+
step = R(previous, z)
|
|
624
|
+
conj = step if conj is None else And(conj, step)
|
|
625
|
+
previous = z
|
|
626
|
+
body = R(previous, b)
|
|
627
|
+
if conj is not None:
|
|
628
|
+
body = And(conj, body)
|
|
629
|
+
for z in reversed(mids):
|
|
630
|
+
body = ex(z, body)
|
|
631
|
+
return body
|
|
632
|
+
|
|
633
|
+
body = Implies(And(path(w, u, spec.m), path(w, v, spec.r)),
|
|
634
|
+
ex(s, And(path(u, s, spec.n), path(v, s, spec.s))))
|
|
635
|
+
for var in (v, u, w):
|
|
636
|
+
body = fa(var, body)
|
|
637
|
+
return body
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# PREDICATE, CONSTANT, NAME, VARIABLE, and SORT used to be declared here as
|
|
2
|
+
# fixed ASCII regexes. They are now GENERATED AT RUNTIME by
|
|
3
|
+
# unicode_logic_kit/fol/_identifiers.py, from the running interpreter's own
|
|
4
|
+
# Unicode letter tables, and spliced directly into each mode's grammar text
|
|
5
|
+
# by _fol_nodes.build_grammar() — see _identifiers.py's module docstring for
|
|
6
|
+
# why a codepoint range belongs in generated code, not frozen into this file.
|
|
7
|
+
#
|
|
8
|
+
# NUMBER, FORALL, EXISTS, and LAMBDA are unaffected by that widening (none of
|
|
9
|
+
# them classifies a letter) and stay here, %import'd as before.
|
|
10
|
+
#
|
|
11
|
+
# NUMBER carries an optional leading "-" so that a NEGATIVE literal prints and
|
|
12
|
+
# reads back: Number(-3) renders as `-3`, and before this the text `P(-3)` was a
|
|
13
|
+
# syntax error. The sibling grammars (prover9_input, tptp_input) always had it.
|
|
14
|
+
# It sits next to the binary minus without taking it over: a NUMBER is only
|
|
15
|
+
# accepted where a term STARTS, so `x-3`, `x -3` and `x - 3` all stay
|
|
16
|
+
# subtraction, and `x - -3` is subtraction of -3. A count bound stays a
|
|
17
|
+
# non-negative integer: `∃≥-2 x P(x)` (and `∃≥2.5 x P(x)`) now reach the count
|
|
18
|
+
# rule (_count_transform / _sorted_count_transform), which refuses them with a
|
|
19
|
+
# CountBoundError, a ParsingError: a one-line SYNTAX_ERROR naming the bound, as
|
|
20
|
+
# for any other parse error, not a raw lark VisitError. Audited against the
|
|
21
|
+
# old grammar over 2455 `-`-adjacent texts in all 26 dialect/parser-kind
|
|
22
|
+
# combinations: no text that parsed before parses to a different tree or fails
|
|
23
|
+
# now.
|
|
24
|
+
|
|
25
|
+
NUMBER.1: /-?[0-9]+(\.[0-9]+)?/
|
|
26
|
+
|
|
27
|
+
FORALL: "∀"
|
|
28
|
+
|
|
29
|
+
EXISTS: "∃"
|
|
30
|
+
|
|
31
|
+
LAMBDA: "λ"
|