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,944 @@
|
|
|
1
|
+
"""Standard (relational) translation of propositional modal logic into FOL.
|
|
2
|
+
|
|
3
|
+
The *standard translation* ST embeds the propositional modal fragment into
|
|
4
|
+
classical first-order logic over an explicit "current world" term, so that the
|
|
5
|
+
existing FOL back-ends (Z3, Prover9, TPTP, the resolution engine, the Tarski
|
|
6
|
+
evaluator) can reason about modal formulas. A modal formula is true at a world
|
|
7
|
+
*w* in a Kripke model exactly when its translation ``ST(φ, w)`` is true in the
|
|
8
|
+
corresponding first-order structure (worlds as the domain, accessibility
|
|
9
|
+
relations and atom-predicates as the relations) — this correspondence is what
|
|
10
|
+
``tests/test_modal_translation.py`` cross-checks against
|
|
11
|
+
:func:`unicode_logic_kit.semantics.kripke.satisfies_modal`.
|
|
12
|
+
|
|
13
|
+
Translation scheme, with ``w`` the current-world variable and ``w'`` a FRESH
|
|
14
|
+
world variable (names ``w0, w1, …`` are generated so nested modalities never
|
|
15
|
+
capture each other; the free current-world name and every variable name of
|
|
16
|
+
the formula are skipped, so even a caller who passes ``world="w0"`` keeps a
|
|
17
|
+
distinct, uncaptured free variable, and an atom ``P(w0)`` keeps its own ``w0``):
|
|
18
|
+
|
|
19
|
+
- ``Atom A`` (propositional / ground) → ``A(w)``: the atom's predicate is
|
|
20
|
+
applied to the current world. A nullary atom ``P`` becomes ``P(w)``; an atom
|
|
21
|
+
``Likes(a, b)`` becomes ``Likes(a, b, w)`` (the world is appended as the last
|
|
22
|
+
argument, so ground arguments are preserved).
|
|
23
|
+
- ``Not / And / Or / Xor / Implies / Iff`` — map through structurally at the
|
|
24
|
+
same world.
|
|
25
|
+
- ``Box φ`` → ``∀w' (R(w, w') → ST(φ, w'))``.
|
|
26
|
+
- ``Diamond φ`` → ``∃w' (R(w, w') ∧ ST(φ, w'))``.
|
|
27
|
+
- ``Knows(a, φ)`` → ``∀w' (Rk_a(w, w') → ST(φ, w'))``.
|
|
28
|
+
- ``Believes(a, φ)`` → ``∀w' (Rb_a(w, w') → ST(φ, w'))``.
|
|
29
|
+
- ``EverybodyKnows(G, φ)`` (E_G, "everyone in G knows φ") →
|
|
30
|
+
``⋀_{a∈G} ∀w' (Rk_a(w, w') → ST(φ, w'))`` — a per-agent :class:`Knows`
|
|
31
|
+
translation for every member of ``G``, ANDed together (one-step; union of
|
|
32
|
+
relations ≡ conjunction of boxes). Empty ``G`` is rejected (see Scope below).
|
|
33
|
+
- ``DistributedKnowledge(G, φ)`` (D_G, "φ is distributed knowledge in G") →
|
|
34
|
+
``∀w' ((⋀_{a∈G} Rk_a(w, w')) → ST(φ, w'))`` — ONE box over the
|
|
35
|
+
INTERSECTION of the group's relations (every ``Rk_a(w,w')`` conjunct must
|
|
36
|
+
hold of the SAME ``w'``), not a per-agent conjunction of separate boxes.
|
|
37
|
+
- ``Says(a, φ)`` → ``∀w' (Rs_a(w, w') → ST(φ, w'))`` (assertive: box over
|
|
38
|
+
a per-agent "compatible with what a says" relation).
|
|
39
|
+
- ``Wants(a, φ)`` → ``∀w' (Rw_a(w, w') → ST(φ, w'))`` (bouletic: box over
|
|
40
|
+
a per-agent desire relation).
|
|
41
|
+
- ``Always φ`` → ``∀w' (T(w, w') → ST(φ, w'))`` (box over a temporal
|
|
42
|
+
accessibility predicate ``T``).
|
|
43
|
+
- ``Eventually φ`` → ``∃w' (T(w, w') ∧ ST(φ, w'))`` (diamond over ``T``).
|
|
44
|
+
- ``Next φ`` → ``∀w' (N(w, w') → ST(φ, w'))`` (box over a one-step
|
|
45
|
+
predicate ``N``).
|
|
46
|
+
- ``Obligatory φ`` → ``∀w' (D(w, w') → ST(φ, w'))`` (box over a deontic
|
|
47
|
+
accessibility predicate ``D``).
|
|
48
|
+
- ``Permitted φ`` → ``∃w' (D(w, w') ∧ ST(φ, w'))`` (diamond over ``D``).
|
|
49
|
+
- ``Nominal i`` → ``w = nom_i``: a nominal is a world-equality against a
|
|
50
|
+
dedicated world CONSTANT. The ``"nom_"`` prefix keeps these constants out of
|
|
51
|
+
the user's constant namespace, so an atom mentioning a ground constant ``i``
|
|
52
|
+
can never collide with the nominal ``i``. UNLESS ``i`` is currently BOUND by
|
|
53
|
+
an enclosing ``↓i`` (see ``Down`` below), in which case the translation uses
|
|
54
|
+
the bound world TERM instead — the ``bindings`` mechanism, not the ``nom_``
|
|
55
|
+
constant, decides which.
|
|
56
|
+
- ``At(i, φ)`` → ``ST(φ, nom_i)``: the satisfaction operator ``@i φ``
|
|
57
|
+
re-anchors the translation at the constant world term ``nom_i`` (the current
|
|
58
|
+
world term is simply replaced — no quantifier is introduced) — again unless
|
|
59
|
+
``i`` is ``↓``-bound, in which case the bound term replaces ``nom_i``.
|
|
60
|
+
- ``Down(x, φ)`` (``↓x.φ``, N1) → ``ST(φ, world)`` with ``x`` now BOUND, for
|
|
61
|
+
the rest of ``φ``'s translation, to the CURRENT WORLD TERM ``world`` — no
|
|
62
|
+
fresh quantifier is introduced (this is the bounded-fragment translation:
|
|
63
|
+
``↓`` rebinds a name to the current world, it does not existentially
|
|
64
|
+
quantify over worlds). Concretely: every ``Nominal(x)`` / ``At(x, …)`` in
|
|
65
|
+
``φ`` — down to, but not inside, a nested ``Down(x, …)`` that shadows it —
|
|
66
|
+
translates to ``world`` (or re-anchors at it) instead of to a fresh
|
|
67
|
+
``nom_x`` constant. This is threaded through ``_translate`` as one extra
|
|
68
|
+
parameter, ``bindings: Dict[str, Node]`` (name → the world TERM it is
|
|
69
|
+
currently bound to), consulted by the ``Nominal``/``At`` cases FIRST,
|
|
70
|
+
falling back to the ``nom_`` scheme only when the name is unbound — so
|
|
71
|
+
``bindings={}`` (every call site outside ``Down``'s own recursion)
|
|
72
|
+
reproduces the pre-``Down`` translation byte-for-byte. Because ``↓`` makes
|
|
73
|
+
H(@,↓) UNDECIDABLE (unlike plain H(@)), no bare-bool validity check treats
|
|
74
|
+
a ``Down``-containing formula: ``hybrid_is_valid`` refuses it by name;
|
|
75
|
+
:func:`down_is_valid` (below) is the PROVED-only replacement.
|
|
76
|
+
|
|
77
|
+
Scope / caveats (v1):
|
|
78
|
+
|
|
79
|
+
- ``Always`` and ``Eventually`` are translated as a **box / diamond over an
|
|
80
|
+
assumed temporal accessibility predicate** ``T``. To make ``Always`` a genuine
|
|
81
|
+
"henceforth" (reflexive-transitive reachability) one would need extra frame
|
|
82
|
+
axioms forcing ``T`` to be reflexive and transitive — that is not pure
|
|
83
|
+
first-order logic (transitive closure is not first-order definable), so it is
|
|
84
|
+
out of scope here. The cross-check in the tests therefore drives the Tarski
|
|
85
|
+
structure with ``T`` interpreted as the SAME one-step ``"temporal"`` relation
|
|
86
|
+
used by the Kripke model, NOT its closure.
|
|
87
|
+
- ``Until`` is rejected: strong Until needs the transitive closure of the
|
|
88
|
+
temporal relation, which is not first-order definable.
|
|
89
|
+
- ``CommonKnowledge`` (C_G) is rejected for the exact same reason: it needs the
|
|
90
|
+
reflexive-transitive closure of the group's union relation.
|
|
91
|
+
- ``EverybodyKnows`` over an EMPTY group is rejected too: ``E_∅ φ`` is
|
|
92
|
+
vacuously true by definition (the union over zero relations is empty, so
|
|
93
|
+
"holds at every successor" holds vacuously), but this grammar has no
|
|
94
|
+
first-order truth constant to render that without borrowing an unrelated
|
|
95
|
+
atom from the caller's own vocabulary. ``DistributedKnowledge`` never
|
|
96
|
+
raises this way — its own constructor already refuses an empty group (see
|
|
97
|
+
``fol._modal_nodes.DistributedKnowledge``'s docstring).
|
|
98
|
+
- ``Quantifier`` / ``SortedQuantifier`` are rejected: first-order (quantified)
|
|
99
|
+
modal logic with object domains is out of scope for v1. So are Łukasiewicz and
|
|
100
|
+
lambda nodes.
|
|
101
|
+
|
|
102
|
+
The accessibility-predicate names are fixed so the matching Tarski structure can
|
|
103
|
+
be built mechanically: ``"R"`` (alethic), ``"Rk_" + agent`` (epistemic),
|
|
104
|
+
``"Rb_" + agent`` (doxastic), ``"Rs_" + agent`` (assertive), ``"Rw_" + agent``
|
|
105
|
+
(bouletic), ``"T"`` (temporal), ``"N"`` (next), ``"D"`` (deontic).
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, List, NoReturn, Optional
|
|
109
|
+
|
|
110
|
+
if TYPE_CHECKING:
|
|
111
|
+
from ..atp.protocol import Verdict
|
|
112
|
+
|
|
113
|
+
from ._atom_keys import refuse_alike_agents
|
|
114
|
+
from ._msfl_nodes import key_text
|
|
115
|
+
from ._identifiers import fresh_variables, symbol_names
|
|
116
|
+
from .nodes import (
|
|
117
|
+
Node,
|
|
118
|
+
Variable, Constant, LambdaVar,
|
|
119
|
+
Atom, Not, And, Or, Xor, Implies, Iff,
|
|
120
|
+
Quantifier, SortedQuantifier,
|
|
121
|
+
Box, Diamond, Knows, Believes, Says, Wants,
|
|
122
|
+
EverybodyKnows, DistributedKnowledge, CommonKnowledge,
|
|
123
|
+
Always, Eventually, Next, Until,
|
|
124
|
+
Historically, Once, Previous, Since,
|
|
125
|
+
Obligatory, Permitted,
|
|
126
|
+
Nominal, At,
|
|
127
|
+
sort_membership_axioms, substitute,
|
|
128
|
+
)
|
|
129
|
+
from ._truth_constants import truth_value
|
|
130
|
+
# Down (the ↓ binder, N1) is not yet re-exported through fol.nodes / fol's
|
|
131
|
+
# public __init__ / the top-level unicode_logic_kit package — that three-file
|
|
132
|
+
# edit is outside this change's file ownership (see the change's own
|
|
133
|
+
# report); imported directly from its defining module instead, the same
|
|
134
|
+
# class object either import path would give.
|
|
135
|
+
from ._hybrid_nodes import Down
|
|
136
|
+
from .frames import (
|
|
137
|
+
FRAMES as _SHARED_FRAMES, UnsupportedFrameCondition,
|
|
138
|
+
is_first_order, resolve_frame, unguarded_frame_axiom,
|
|
139
|
+
)
|
|
140
|
+
from ..semantics._modal_reject import (
|
|
141
|
+
FUZZY_TYPES, LAMBDA_TYPES,
|
|
142
|
+
reject_equality, reject_fuzzy, reject_lambda,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Accessibility predicate names (the contract with the matching Tarski
|
|
146
|
+
# structure; keep these stable).
|
|
147
|
+
_R_ALETHIC = "R"
|
|
148
|
+
_R_KNOWS_PREFIX = "Rk_"
|
|
149
|
+
_R_BELIEVES_PREFIX = "Rb_"
|
|
150
|
+
_R_SAYS_PREFIX = "Rs_"
|
|
151
|
+
_R_WANTS_PREFIX = "Rw_"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _agent_key(agent: Node) -> str:
|
|
155
|
+
"""Agent term's name for the per-agent relation (this propositional translation
|
|
156
|
+
rejects object quantifiers, so the agent is always a ground Constant here)."""
|
|
157
|
+
return getattr(agent, "name", None) or key_text(agent)
|
|
158
|
+
_R_TEMPORAL = "T"
|
|
159
|
+
_R_NEXT = "N"
|
|
160
|
+
_R_DEONTIC = "D"
|
|
161
|
+
|
|
162
|
+
# Prefix for the world constant a hybrid nominal translates to ("nom_" + name).
|
|
163
|
+
# The prefix keeps the generated constants disjoint from user constants, so a
|
|
164
|
+
# formula whose atoms mention a ground constant `i` cannot collide with the
|
|
165
|
+
# nominal `i`. This is the contract with any structure built for the image.
|
|
166
|
+
_NOM_PREFIX = "nom_"
|
|
167
|
+
|
|
168
|
+
# Equality is NOT an ordinary atom here. This translation is PROPOSITIONAL: an
|
|
169
|
+
# atom is a world-relative proposition, so appending the world to ``=`` would
|
|
170
|
+
# make identity a ternary, uninterpreted, world-varying relation — under which
|
|
171
|
+
# ``a = a`` comes back "not valid" (measured on 0.28.1) and ``□(a = b) → a = b``
|
|
172
|
+
# is "valid" only through reflexivity of R, not through identity. That is a
|
|
173
|
+
# silent approximation of a construct this layer has no semantics for (the
|
|
174
|
+
# Kripke evaluator reads atoms off a per-world valuation of ground-atom keys
|
|
175
|
+
# and interprets no terms either), so it is refused by name. First-order modal
|
|
176
|
+
# logic with rigid identity is :mod:`unicode_logic_kit.fol.qml`.
|
|
177
|
+
# ``≠`` is the same construct and is refused the same way: the kit prints
|
|
178
|
+
# ``a ≠ b`` as ``Atom("≠", (a, b))``, and appending the world there produced
|
|
179
|
+
# ``≠(a, b, w)`` — a ternary uninterpreted relation unrelated to the ``=`` one,
|
|
180
|
+
# so ``a = b ∨ a ≠ b`` came back "not valid" (measured on 0.28.1). Both
|
|
181
|
+
# spellings live in :data:`EQUALITY_PREDICATES`, and :func:`reject_equality`
|
|
182
|
+
# (shared with the Kripke evaluator, the modal tableau and the GMT embedding)
|
|
183
|
+
# is the single place that says why.
|
|
184
|
+
#: The predicate the FOL IMAGE uses for world identity (``@i j`` becomes
|
|
185
|
+
#: ``i = j``, and the temporal first-step axiom says ``T(w,v) → w = v ∨ …``).
|
|
186
|
+
#: An equality atom in the SOURCE is refused; one in the image is ordinary FOL.
|
|
187
|
+
_EQUALITY = "="
|
|
188
|
+
|
|
189
|
+
_EQUALITY_ROUTE = "the propositional standard translation"
|
|
190
|
+
_EQUALITY_ATOM_READING = ("an atom becomes a predicate with the world appended, "
|
|
191
|
+
"so '=' would become a world-varying uninterpreted "
|
|
192
|
+
"relation")
|
|
193
|
+
_EQUALITY_INSTEAD = ("Use unicode_logic_kit.fol.qml (quantified modal logic, "
|
|
194
|
+
"where '=' is rigid identity over the object domain) for a "
|
|
195
|
+
"formula with identity.")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _reject_equality(formula: Node) -> None:
|
|
199
|
+
""":func:`reject_equality` with this route's wording (check-and-raise)."""
|
|
200
|
+
reject_equality(formula, "standard_translation", _EQUALITY_ROUTE,
|
|
201
|
+
atom_reading=_EQUALITY_ATOM_READING,
|
|
202
|
+
instead=_EQUALITY_INSTEAD)
|
|
203
|
+
|
|
204
|
+
# Universal/existential quantifier-type spellings used by the AST.
|
|
205
|
+
_FORALL = "∀"
|
|
206
|
+
_EXISTS = "∃"
|
|
207
|
+
|
|
208
|
+
# A USER predicate named like one of the accessibility relations above used to
|
|
209
|
+
# BE that relation once the translation appended its world argument (a
|
|
210
|
+
# propositional atom ``R`` became ``R(w)`` next to the binary relation
|
|
211
|
+
# ``R(w, v)`` and crashed Z3 on the arity clash). _user_predicate keeps the
|
|
212
|
+
# two namespaces apart: U+00B7 MIDDLE DOT is punctuation no parser puts into
|
|
213
|
+
# an identifier, so the renamed user name can collide with nothing.
|
|
214
|
+
_RESERVED_RELATIONS = frozenset({_R_ALETHIC, _R_TEMPORAL, _R_NEXT, _R_DEONTIC})
|
|
215
|
+
_RESERVED_RELATION_PREFIXES = (_R_KNOWS_PREFIX, _R_BELIEVES_PREFIX,
|
|
216
|
+
_R_SAYS_PREFIX, _R_WANTS_PREFIX)
|
|
217
|
+
_USER_MARK = "·"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _user_predicate(name: str) -> str:
|
|
221
|
+
"""The name a USER atom's predicate gets in the image: unchanged unless it
|
|
222
|
+
(with any trailing ``·`` stripped) is a relation name above, then with one
|
|
223
|
+
``·`` appended — injective, and a formula that avoids those names
|
|
224
|
+
translates byte-for-byte as before (the same scheme as ``fol.qml``)."""
|
|
225
|
+
base = name.rstrip(_USER_MARK)
|
|
226
|
+
if base in _RESERVED_RELATIONS or base.startswith(_RESERVED_RELATION_PREFIXES):
|
|
227
|
+
return name + _USER_MARK
|
|
228
|
+
return name
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class _FreshWorlds:
|
|
232
|
+
"""A monotonic generator of fresh world-variable names ``w0, w1, …``.
|
|
233
|
+
|
|
234
|
+
Threading a single counter through one translation guarantees every modal
|
|
235
|
+
operator introduces a distinct bound world variable, so nested boxes /
|
|
236
|
+
diamonds cannot capture one another's worlds. A world variable never has the
|
|
237
|
+
spelling of a name the translation must leave alone: the free current-world
|
|
238
|
+
name (so a caller who passes ``world="w0"`` keeps a distinct, uncaptured free
|
|
239
|
+
variable), every name of the formula, of any kind (an atom ``P(w0)`` of the
|
|
240
|
+
source keeps its own ``w0`` under a box, which a bound world variable of that
|
|
241
|
+
name would capture) and every name the caller asks to avoid. Names are compared
|
|
242
|
+
exactly and with their case folded, because a target that reads a variable
|
|
243
|
+
``w0`` and a variable ``W0`` as one word (TPTP, Prover9) would conflate them.
|
|
244
|
+
|
|
245
|
+
A variable of the formula that is spelled like the current-world name would be
|
|
246
|
+
read as the world itself, so it is renamed, once for the whole formula, to a name
|
|
247
|
+
of its own (:meth:`term` applies the renaming to a term of an atom).
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
def __init__(self, world: str = "", names: Iterable[str] = (),
|
|
251
|
+
variables: Iterable[str] = (), avoid: Iterable[str] = ()):
|
|
252
|
+
"""Start the fresh-name counter at zero and settle the names that are never minted.
|
|
253
|
+
|
|
254
|
+
``world`` is the free current-world name, ``names`` every name of the formula,
|
|
255
|
+
``variables`` the names of its variables and ``avoid`` further names to keep
|
|
256
|
+
clear of.
|
|
257
|
+
"""
|
|
258
|
+
self._n = 0
|
|
259
|
+
reserved = set(names) | set(variables) | set(avoid) | {world}
|
|
260
|
+
taken = reserved | {name.casefold() for name in reserved}
|
|
261
|
+
self._renamed: Dict[str, Variable] = {}
|
|
262
|
+
for name in sorted(set(variables)):
|
|
263
|
+
if name.casefold() == world.casefold():
|
|
264
|
+
new_name = fresh_variables(1, letter="x", avoid=taken)[0]
|
|
265
|
+
taken = taken | {new_name}
|
|
266
|
+
self._renamed[name] = Variable(new_name)
|
|
267
|
+
self._reserved = taken
|
|
268
|
+
|
|
269
|
+
def next(self) -> Variable:
|
|
270
|
+
"""Return the next fresh world Variable (``w0``, ``w1``, …), skipping ``reserved``."""
|
|
271
|
+
while True:
|
|
272
|
+
name = f"w{self._n}"
|
|
273
|
+
self._n += 1
|
|
274
|
+
if name not in self._reserved:
|
|
275
|
+
return Variable(name)
|
|
276
|
+
|
|
277
|
+
def term(self, term: Node) -> Node:
|
|
278
|
+
"""``term`` with each variable spelled like the current world renamed (else unchanged)."""
|
|
279
|
+
for old, new in self._renamed.items():
|
|
280
|
+
term = substitute(term, Variable(old), new)
|
|
281
|
+
return term
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _box_like(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
|
|
285
|
+
bindings: Dict[str, Node]) -> Node:
|
|
286
|
+
"""Build ``∀w' (rel(world, w') → ST(body, w'))`` with a fresh ``w'``."""
|
|
287
|
+
w2 = fresh.next()
|
|
288
|
+
access = Atom(rel_name, (world, w2))
|
|
289
|
+
return Quantifier(_FORALL, w2, Implies(access, _translate(body, w2, fresh, bindings)))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _diamond_like(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
|
|
293
|
+
bindings: Dict[str, Node]) -> Node:
|
|
294
|
+
"""Build ``∃w' (rel(world, w') ∧ ST(body, w'))`` with a fresh ``w'``."""
|
|
295
|
+
w2 = fresh.next()
|
|
296
|
+
access = Atom(rel_name, (world, w2))
|
|
297
|
+
return Quantifier(_EXISTS, w2, And(access, _translate(body, w2, fresh, bindings)))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _box_intersection(rel_names: List[str], world: Node, body: Node, fresh: _FreshWorlds,
|
|
301
|
+
bindings: Dict[str, Node]) -> Node:
|
|
302
|
+
"""Build ``∀w' ((R1(w,w')∧R2(w,w')∧…) → ST(body,w'))`` — a box over the
|
|
303
|
+
INTERSECTION of several accessibility relations, one atom per relation
|
|
304
|
+
ANDed together into the antecedent, sharing ONE fresh ``w'`` (a single
|
|
305
|
+
successor has to satisfy every relation at once — this is exactly what
|
|
306
|
+
distinguishes :class:`DistributedKnowledge` from
|
|
307
|
+
:func:`_box_like`-per-agent-then-ANDed, which is what
|
|
308
|
+
:class:`EverybodyKnows` uses instead, and which shares no such single
|
|
309
|
+
witness). ``rel_names`` must be non-empty (guaranteed by
|
|
310
|
+
:class:`DistributedKnowledge`'s own constructor refusing an empty group).
|
|
311
|
+
"""
|
|
312
|
+
w2 = fresh.next()
|
|
313
|
+
guard: Node = Atom(rel_names[0], (world, w2))
|
|
314
|
+
for name in rel_names[1:]:
|
|
315
|
+
guard = And(guard, Atom(name, (world, w2)))
|
|
316
|
+
return Quantifier(_FORALL, w2, Implies(guard, _translate(body, w2, fresh, bindings)))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _box_converse(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
|
|
320
|
+
bindings: Dict[str, Node]) -> Node:
|
|
321
|
+
"""Build ``∀w' (rel(w', world) → ST(body, w'))`` — a box over the CONVERSE relation."""
|
|
322
|
+
w2 = fresh.next()
|
|
323
|
+
access = Atom(rel_name, (w2, world))
|
|
324
|
+
return Quantifier(_FORALL, w2, Implies(access, _translate(body, w2, fresh, bindings)))
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _diamond_converse(rel_name: str, world: Node, body: Node, fresh: _FreshWorlds,
|
|
328
|
+
bindings: Dict[str, Node]) -> Node:
|
|
329
|
+
"""Build ``∃w' (rel(w', world) ∧ ST(body, w'))`` — a diamond over the CONVERSE relation."""
|
|
330
|
+
w2 = fresh.next()
|
|
331
|
+
access = Atom(rel_name, (w2, world))
|
|
332
|
+
return Quantifier(_EXISTS, w2, And(access, _translate(body, w2, fresh, bindings)))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _translate(formula: Node, world: Node, fresh: _FreshWorlds,
|
|
336
|
+
bindings: Optional[Dict[str, Node]] = None) -> Node:
|
|
337
|
+
"""Recursively translate ``formula`` relative to ``world`` (the worker).
|
|
338
|
+
|
|
339
|
+
``world`` is the current-world TERM: the free Variable at the top, a bound
|
|
340
|
+
fresh Variable under a modality, or a ``nom_``-Constant under an ``@``-jump.
|
|
341
|
+
|
|
342
|
+
``bindings`` (name → world TERM) holds the ``↓``-bound nominal names in
|
|
343
|
+
scope (N1); every call site outside :class:`Down`'s own recursion passes
|
|
344
|
+
``{}`` (the default), which is exactly what makes ``bindings={}``
|
|
345
|
+
reproduce the pre-``Down`` translation byte-for-byte — see the module
|
|
346
|
+
docstring's ``Down`` bullet.
|
|
347
|
+
"""
|
|
348
|
+
if bindings is None:
|
|
349
|
+
bindings = {}
|
|
350
|
+
|
|
351
|
+
# --- atomic: append the world as the last predicate argument ---
|
|
352
|
+
if isinstance(formula, Atom):
|
|
353
|
+
if truth_value(formula) is not None:
|
|
354
|
+
return formula # `$true` / `$false` are the same at every world: no world argument
|
|
355
|
+
_reject_equality(formula)
|
|
356
|
+
return Atom(_user_predicate(formula.predicate),
|
|
357
|
+
(*(fresh.term(arg) for arg in formula.args), world))
|
|
358
|
+
|
|
359
|
+
# --- classical connectives: structural at the same world ---
|
|
360
|
+
if isinstance(formula, Not):
|
|
361
|
+
return Not(_translate(formula.formula, world, fresh, bindings))
|
|
362
|
+
if isinstance(formula, And):
|
|
363
|
+
return And(_translate(formula.left, world, fresh, bindings),
|
|
364
|
+
_translate(formula.right, world, fresh, bindings))
|
|
365
|
+
if isinstance(formula, Or):
|
|
366
|
+
return Or(_translate(formula.left, world, fresh, bindings),
|
|
367
|
+
_translate(formula.right, world, fresh, bindings))
|
|
368
|
+
if isinstance(formula, Xor):
|
|
369
|
+
return Xor(_translate(formula.left, world, fresh, bindings),
|
|
370
|
+
_translate(formula.right, world, fresh, bindings))
|
|
371
|
+
if isinstance(formula, Implies):
|
|
372
|
+
return Implies(_translate(formula.left, world, fresh, bindings),
|
|
373
|
+
_translate(formula.right, world, fresh, bindings))
|
|
374
|
+
if isinstance(formula, Iff):
|
|
375
|
+
return Iff(_translate(formula.left, world, fresh, bindings),
|
|
376
|
+
_translate(formula.right, world, fresh, bindings))
|
|
377
|
+
|
|
378
|
+
# --- alethic ---
|
|
379
|
+
if isinstance(formula, Box):
|
|
380
|
+
return _box_like(_R_ALETHIC, world, formula.formula, fresh, bindings)
|
|
381
|
+
if isinstance(formula, Diamond):
|
|
382
|
+
return _diamond_like(_R_ALETHIC, world, formula.formula, fresh, bindings)
|
|
383
|
+
|
|
384
|
+
# --- epistemic / doxastic (both box-like / universal) ---
|
|
385
|
+
if isinstance(formula, Knows):
|
|
386
|
+
return _box_like(_R_KNOWS_PREFIX + _agent_key(formula.agent), world,
|
|
387
|
+
formula.formula, fresh, bindings)
|
|
388
|
+
if isinstance(formula, Believes):
|
|
389
|
+
return _box_like(_R_BELIEVES_PREFIX + _agent_key(formula.agent), world,
|
|
390
|
+
formula.formula, fresh, bindings)
|
|
391
|
+
|
|
392
|
+
# --- group epistemic: E_G and D_G are one-step (union/intersection of
|
|
393
|
+
# per-agent relations) and ARE first-order definable; C_G needs the
|
|
394
|
+
# reflexive-transitive CLOSURE of the union relation and is NOT (see the
|
|
395
|
+
# rejection below, alongside Until/Since). ---
|
|
396
|
+
if isinstance(formula, EverybodyKnows):
|
|
397
|
+
if not formula.group:
|
|
398
|
+
raise NotImplementedError(
|
|
399
|
+
"standard_translation: E_∅ φ (a group-epistemic operator over "
|
|
400
|
+
"an empty group) is vacuously TRUE at every world by "
|
|
401
|
+
"definition (everybody_knows's own empty-group convention), "
|
|
402
|
+
"but this grammar has no first-order truth constant to render "
|
|
403
|
+
"that without borrowing an unrelated atom. Evaluate it "
|
|
404
|
+
"directly with semantics.kripke.satisfies_modal / "
|
|
405
|
+
"semantics.action_models.everybody_knows instead."
|
|
406
|
+
)
|
|
407
|
+
conj = _box_like(_R_KNOWS_PREFIX + _agent_key(formula.group[0]), world,
|
|
408
|
+
formula.formula, fresh, bindings)
|
|
409
|
+
for agent in formula.group[1:]:
|
|
410
|
+
conj = And(conj, _box_like(_R_KNOWS_PREFIX + _agent_key(agent), world,
|
|
411
|
+
formula.formula, fresh, bindings))
|
|
412
|
+
return conj
|
|
413
|
+
if isinstance(formula, DistributedKnowledge):
|
|
414
|
+
rel_names = [_R_KNOWS_PREFIX + _agent_key(a) for a in formula.group]
|
|
415
|
+
return _box_intersection(rel_names, world, formula.formula, fresh, bindings)
|
|
416
|
+
if isinstance(formula, CommonKnowledge):
|
|
417
|
+
raise NotImplementedError(
|
|
418
|
+
"standard_translation: CommonKnowledge (C_G) is not first-order "
|
|
419
|
+
"definable — common knowledge is the REFLEXIVE-TRANSITIVE CLOSURE "
|
|
420
|
+
"of the group's union relation, and transitive closure has no "
|
|
421
|
+
"first-order rendering (the same obstacle this module already "
|
|
422
|
+
"reports for Until/Since — see the module docstring). Evaluate it "
|
|
423
|
+
"with the Kripke evaluator (semantics.kripke.satisfies_modal) "
|
|
424
|
+
"instead."
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# --- assertive / bouletic (both box-like, per-agent relations) ---
|
|
428
|
+
if isinstance(formula, Says):
|
|
429
|
+
return _box_like(_R_SAYS_PREFIX + _agent_key(formula.agent), world,
|
|
430
|
+
formula.formula, fresh, bindings)
|
|
431
|
+
if isinstance(formula, Wants):
|
|
432
|
+
return _box_like(_R_WANTS_PREFIX + _agent_key(formula.agent), world,
|
|
433
|
+
formula.formula, fresh, bindings)
|
|
434
|
+
|
|
435
|
+
# --- deontic (box/diamond over a deontic accessibility predicate D) ---
|
|
436
|
+
if isinstance(formula, Obligatory):
|
|
437
|
+
return _box_like(_R_DEONTIC, world, formula.formula, fresh, bindings)
|
|
438
|
+
if isinstance(formula, Permitted):
|
|
439
|
+
return _diamond_like(_R_DEONTIC, world, formula.formula, fresh, bindings)
|
|
440
|
+
|
|
441
|
+
# --- temporal (box/diamond over an assumed accessibility predicate) ---
|
|
442
|
+
if isinstance(formula, Always):
|
|
443
|
+
return _box_like(_R_TEMPORAL, world, formula.formula, fresh, bindings)
|
|
444
|
+
if isinstance(formula, Eventually):
|
|
445
|
+
return _diamond_like(_R_TEMPORAL, world, formula.formula, fresh, bindings)
|
|
446
|
+
if isinstance(formula, Next):
|
|
447
|
+
return _box_like(_R_NEXT, world, formula.formula, fresh, bindings)
|
|
448
|
+
|
|
449
|
+
# --- past tense (box/diamond over the CONVERSE temporal/next predicate) ---
|
|
450
|
+
if isinstance(formula, Historically):
|
|
451
|
+
return _box_converse(_R_TEMPORAL, world, formula.formula, fresh, bindings)
|
|
452
|
+
if isinstance(formula, Once):
|
|
453
|
+
return _diamond_converse(_R_TEMPORAL, world, formula.formula, fresh, bindings)
|
|
454
|
+
if isinstance(formula, Previous):
|
|
455
|
+
return _box_converse(_R_NEXT, world, formula.formula, fresh, bindings)
|
|
456
|
+
|
|
457
|
+
# --- hybrid: a nominal is a world-equality; @ re-anchors the world term.
|
|
458
|
+
# ``bindings`` is consulted FIRST — a ↓-bound name uses its bound TERM
|
|
459
|
+
# directly (no fresh nom_ constant, no equality atom needed for Nominal:
|
|
460
|
+
# "x" bound to term t just means "the current world is t", i.e. w = t —
|
|
461
|
+
# same shape as the nom_ case, just with t instead of a fresh constant). ---
|
|
462
|
+
if isinstance(formula, Nominal):
|
|
463
|
+
bound = bindings.get(formula.name)
|
|
464
|
+
target = bound if bound is not None else Constant(_NOM_PREFIX + formula.name)
|
|
465
|
+
return Atom("=", (world, target))
|
|
466
|
+
if isinstance(formula, At):
|
|
467
|
+
bound = bindings.get(formula.nominal.name)
|
|
468
|
+
target = bound if bound is not None else Constant(_NOM_PREFIX + formula.nominal.name)
|
|
469
|
+
return _translate(formula.formula, target, fresh, bindings)
|
|
470
|
+
if isinstance(formula, Down):
|
|
471
|
+
# ↓x.φ: bind x to the CURRENT world term for the rest of φ's
|
|
472
|
+
# translation — NO fresh quantifier (the bounded-fragment
|
|
473
|
+
# translation; see the module docstring's Down bullet and
|
|
474
|
+
# fol._hybrid_nodes.Down's own docstring for why this is sound: ST
|
|
475
|
+
# stays meaning-preserving for full H(@,↓), this is not an
|
|
476
|
+
# approximation).
|
|
477
|
+
return _translate(formula.formula, world, fresh,
|
|
478
|
+
{**bindings, formula.variable.name: world})
|
|
479
|
+
|
|
480
|
+
# --- rejected ---
|
|
481
|
+
if isinstance(formula, (Until, Since)):
|
|
482
|
+
raise NotImplementedError(
|
|
483
|
+
"standard_translation: Until / Since are not first-order definable — "
|
|
484
|
+
"strong Until/Since need the transitive closure of the temporal "
|
|
485
|
+
"relation, which no pure FOL formula captures. Evaluate them with the "
|
|
486
|
+
"Kripke evaluator (semantics.kripke.satisfies_modal) instead."
|
|
487
|
+
)
|
|
488
|
+
if isinstance(formula, (Quantifier, SortedQuantifier)):
|
|
489
|
+
_reject_quantifier(formula)
|
|
490
|
+
if isinstance(formula, FUZZY_TYPES):
|
|
491
|
+
reject_fuzzy(formula, "standard_translation")
|
|
492
|
+
if isinstance(formula, LAMBDA_TYPES):
|
|
493
|
+
reject_lambda(formula, "standard_translation")
|
|
494
|
+
|
|
495
|
+
raise NotImplementedError(
|
|
496
|
+
f"standard_translation: unsupported node type {type(formula).__name__}."
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _reject_quantifier(formula: Node) -> NoReturn:
|
|
501
|
+
"""Reject an object-level quantifier: FO-modal domains are out of scope."""
|
|
502
|
+
raise NotImplementedError(
|
|
503
|
+
f"standard_translation: {type(formula).__name__} is not supported — the "
|
|
504
|
+
"standard translation here covers the propositional modal fragment only. "
|
|
505
|
+
"For quantified (first-order) modal logic with object domains use "
|
|
506
|
+
"unicode_logic_kit.fol.qml (qml_translate / qml_is_valid, the FO shallow "
|
|
507
|
+
"embedding with explicit constant/varying/increasing/decreasing domain "
|
|
508
|
+
"regimes)."
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _check_nominal_collision(formula: Node) -> None:
|
|
513
|
+
"""Raise if a user symbol's name collides with a nominal world constant.
|
|
514
|
+
|
|
515
|
+
Each nominal ``i`` (a ``Nominal`` or the label of an ``At``) translates to the
|
|
516
|
+
reserved world constant ``nom_i``. If the formula independently contains a
|
|
517
|
+
user symbol named ``nom_i`` -- a ``Constant``, a ``SortedConstant``, a
|
|
518
|
+
``Function``, or any other node that carries a name and is neither a nominal nor
|
|
519
|
+
a variable -- the first-order image would
|
|
520
|
+
conflate the two terms and Z3's equality reasoning could report a genuinely
|
|
521
|
+
invalid formula as valid — a hole in the "``True`` is always a proof"
|
|
522
|
+
guarantee. The parser builds such a name from the text itself (``@i P(nom_i)``),
|
|
523
|
+
and a hand-built or deserialised AST can carry one in any position; the check fails
|
|
524
|
+
fast, exactly like a dangling nominal assignment.
|
|
525
|
+
"""
|
|
526
|
+
nominal_names, user_names = set(), set()
|
|
527
|
+
for node in formula.walk():
|
|
528
|
+
if isinstance(node, Nominal):
|
|
529
|
+
nominal_names.add(node.name)
|
|
530
|
+
elif isinstance(node, At):
|
|
531
|
+
nominal_names.add(node.nominal.name)
|
|
532
|
+
elif (not isinstance(node, (Variable, LambdaVar))
|
|
533
|
+
and isinstance(getattr(node, "name", None), str)):
|
|
534
|
+
user_names.add(node.name)
|
|
535
|
+
clash = {_NOM_PREFIX + n for n in nominal_names} & user_names
|
|
536
|
+
if clash:
|
|
537
|
+
colliding = sorted(n for n in nominal_names if _NOM_PREFIX + n in clash)
|
|
538
|
+
raise ValueError(
|
|
539
|
+
f"standard_translation: user symbol(s) {sorted(clash)} collide with the "
|
|
540
|
+
f"reserved world constant(s) for nominal(s) {colliding}; the "
|
|
541
|
+
f"{_NOM_PREFIX!r} prefix is reserved for the hybrid translation — rename "
|
|
542
|
+
"the user constant/function."
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def standard_translation(formula: Node, world: str = "w",
|
|
547
|
+
avoid: Iterable[str] = ()) -> Node:
|
|
548
|
+
"""Translate a propositional modal ``formula`` into a classical FOL Node.
|
|
549
|
+
|
|
550
|
+
``world`` names the free current-world variable threaded through the
|
|
551
|
+
translation (default ``"w"``); the result is a plain first-order formula in
|
|
552
|
+
which propositional atoms ``A`` become ``A(world)`` and each modality becomes
|
|
553
|
+
a quantification over a fresh world variable bounded by an accessibility
|
|
554
|
+
predicate (see the module docstring for the exact scheme and the fixed
|
|
555
|
+
predicate names). The returned Node uses only classical FOL constructs, so it
|
|
556
|
+
can be handed to ``to_z3`` / ``to_prover9`` / ``to_tptp`` / the Tarski
|
|
557
|
+
evaluator.
|
|
558
|
+
|
|
559
|
+
Hybrid constructs translate too: ``Nominal i`` becomes the world-equality
|
|
560
|
+
``world = nom_i`` and ``At(i, φ)`` becomes ``ST(φ)`` anchored at the constant
|
|
561
|
+
``nom_i`` (the ``"nom_"`` prefix keeps nominal constants disjoint from user
|
|
562
|
+
constants — see the module docstring).
|
|
563
|
+
|
|
564
|
+
The variables of an atom (``P(w0)``) are the caller's own, and so are the names
|
|
565
|
+
the translation mints. The world variables it binds (``w0``, ``w1``, …) never have
|
|
566
|
+
the spelling of a name of ``formula`` (of any kind: variable, constant, predicate,
|
|
567
|
+
nominal, …), of ``world`` or of a name in ``avoid``, compared exactly and with the
|
|
568
|
+
case folded, so ``□P(w0)`` is ``∀w1 (R(w, w1) → P(w0, w1))`` and not a statement
|
|
569
|
+
about the bound world. A variable of ``formula`` that is spelled like ``world``
|
|
570
|
+
would be read as the current world itself, so it is renamed to a name of its own
|
|
571
|
+
(``x0``, …, clear of every name of ``formula``, of ``world`` and of ``avoid``) in
|
|
572
|
+
the image: it stays one free variable, the same in every atom. To translate several
|
|
573
|
+
formulas of one problem separately and keep that name the same in all of them, pass
|
|
574
|
+
the names of the others as ``avoid``.
|
|
575
|
+
|
|
576
|
+
Raises:
|
|
577
|
+
NotImplementedError: on ``Until`` (not first-order definable), any
|
|
578
|
+
object-level quantifier (first-order modal logic is out of scope for
|
|
579
|
+
v1), a Łukasiewicz node, or a lambda node; and on two different agent
|
|
580
|
+
terms that are named alike (the numeral ``1`` and a constant named ``1``):
|
|
581
|
+
the relation of an agent's operator is named after the agent, so the two
|
|
582
|
+
would be ONE relation and the image would say another thing than ``formula``.
|
|
583
|
+
"""
|
|
584
|
+
_check_nominal_collision(formula)
|
|
585
|
+
refuse_alike_agents([formula], "standard_translation")
|
|
586
|
+
variables = {node.name for node in formula.walk() if isinstance(node, Variable)}
|
|
587
|
+
fresh = _FreshWorlds(world, symbol_names(formula), variables, avoid)
|
|
588
|
+
return _translate(formula, Variable(world), fresh)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# =========================
|
|
592
|
+
# Hybrid validity via the standard translation + the Z3 oracle
|
|
593
|
+
# =========================
|
|
594
|
+
|
|
595
|
+
# Frame classes hybrid_is_valid understands, as conditions on the ALETHIC
|
|
596
|
+
# accessibility predicate R (the other modal families keep their minimal K
|
|
597
|
+
# reading — no axioms are asserted for Rk_a / Rb_a / T / N / D here). The
|
|
598
|
+
# table is the shared registry (unicode_logic_kit.fol.frames), so this route
|
|
599
|
+
# understands the same systems as every other one; conditions with no
|
|
600
|
+
# first-order form are refused by name.
|
|
601
|
+
_HYBRID_FRAMES = _SHARED_FRAMES
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# Which accessibility relation each modal family reads, mirroring _st above.
|
|
605
|
+
_AGENT_FAMILY_PREFIX: Dict[str, str] = {
|
|
606
|
+
"epistemic": _R_KNOWS_PREFIX,
|
|
607
|
+
"doxastic": _R_BELIEVES_PREFIX,
|
|
608
|
+
"assertive": _R_SAYS_PREFIX,
|
|
609
|
+
"bouletic": _R_WANTS_PREFIX,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def relations_used(formula: Node) -> FrozenSet[str]:
|
|
614
|
+
"""Every accessibility predicate :func:`standard_translation` emits for
|
|
615
|
+
``formula`` (``"R"``, ``"T"``, ``"N"``, ``"D"``, ``"Rk_alice"``, …).
|
|
616
|
+
|
|
617
|
+
This is what gates :func:`frame_axioms`: a condition on a relation the
|
|
618
|
+
formula never mentions is noise, and — on a route that reports a bare
|
|
619
|
+
"not valid" — noise that can change the answer.
|
|
620
|
+
"""
|
|
621
|
+
used: set = set()
|
|
622
|
+
for node in formula.walk():
|
|
623
|
+
if isinstance(node, (Box, Diamond)):
|
|
624
|
+
used.add(_R_ALETHIC)
|
|
625
|
+
elif isinstance(node, Knows):
|
|
626
|
+
used.add(_R_KNOWS_PREFIX + _agent_key(node.agent))
|
|
627
|
+
elif isinstance(node, (EverybodyKnows, DistributedKnowledge)):
|
|
628
|
+
used.update(_R_KNOWS_PREFIX + _agent_key(a) for a in node.group)
|
|
629
|
+
elif isinstance(node, Believes):
|
|
630
|
+
used.add(_R_BELIEVES_PREFIX + _agent_key(node.agent))
|
|
631
|
+
elif isinstance(node, Says):
|
|
632
|
+
used.add(_R_SAYS_PREFIX + _agent_key(node.agent))
|
|
633
|
+
elif isinstance(node, Wants):
|
|
634
|
+
used.add(_R_WANTS_PREFIX + _agent_key(node.agent))
|
|
635
|
+
elif isinstance(node, (Obligatory, Permitted)):
|
|
636
|
+
used.add(_R_DEONTIC)
|
|
637
|
+
elif isinstance(node, (Always, Eventually, Historically, Once)):
|
|
638
|
+
used.add(_R_TEMPORAL)
|
|
639
|
+
elif isinstance(node, (Next, Previous)):
|
|
640
|
+
used.add(_R_NEXT)
|
|
641
|
+
return frozenset(used)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _link(antecedent_relation: str, consequent_relation: str) -> Node:
|
|
645
|
+
"""``∀v0 ∀v1 (A(v0,v1) → B(v0,v1))`` over two relation names."""
|
|
646
|
+
w, v = Variable("v0"), Variable("v1")
|
|
647
|
+
return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
|
|
648
|
+
Atom(antecedent_relation, (w, v)), Atom(consequent_relation, (w, v)))))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _temporal_first_step() -> Node:
|
|
652
|
+
"""``∀v0 ∀v1 (T(v0,v1) → v0 = v1 ∨ ∃v2 (N(v0,v2) ∧ T(v2,v1)))``.
|
|
653
|
+
|
|
654
|
+
The first-order half of ``T ⊆ N*``: the witnessing path of a henceforth-step
|
|
655
|
+
either stands still or starts with one ``N``-step. ``T ⊆ N*`` itself demands
|
|
656
|
+
a FINITE path and is not first-order definable, but every model with
|
|
657
|
+
``T = N*`` satisfies this, which is what makes asserting it sound. The
|
|
658
|
+
unguarded twin of :func:`unicode_logic_kit.fol.qml._temporal_first_step_axiom`
|
|
659
|
+
— the two routes must agree, so they assert the same thing.
|
|
660
|
+
"""
|
|
661
|
+
w, v, u = Variable("v0"), Variable("v1"), Variable("v2")
|
|
662
|
+
return Quantifier(_FORALL, w, Quantifier(_FORALL, v, Implies(
|
|
663
|
+
Atom(_R_TEMPORAL, (w, v)),
|
|
664
|
+
Or(Atom(_EQUALITY, (w, v)),
|
|
665
|
+
Quantifier(_EXISTS, u, And(Atom(_R_NEXT, (w, u)),
|
|
666
|
+
Atom(_R_TEMPORAL, (u, v))))))))
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def frame_axioms(formula: Node, frame: str = "K", systems=None,
|
|
670
|
+
temporal_closure: bool = True) -> List[Node]:
|
|
671
|
+
"""The first-order frame axioms for EVERY relation ``formula``'s translation
|
|
672
|
+
emits — the side conditions of the standard translation.
|
|
673
|
+
|
|
674
|
+
``frame`` constrains the ALETHIC relation ``R`` and is any system in the
|
|
675
|
+
shared registry (:mod:`unicode_logic_kit.fol.frames`) or a Scott–Lemmon spec
|
|
676
|
+
like ``"G(1,1,1,1)"``; a system whose condition has no first-order form
|
|
677
|
+
(GL, S4.1, Grz) is refused by name, because this route is first-order.
|
|
678
|
+
|
|
679
|
+
The other families get the conventions :mod:`unicode_logic_kit.fol.qml` and
|
|
680
|
+
the Isabelle/THF exporters already use, so the routes agree instead of
|
|
681
|
+
contradicting each other:
|
|
682
|
+
|
|
683
|
+
- temporal: ``T`` reflexive and transitive (``temporal_closure=True``,
|
|
684
|
+
the default), ``N ⊆ T``, and :func:`_temporal_first_step` when both
|
|
685
|
+
occur — this is what makes ``Ⓖφ → φ``, ``Ⓖφ → Ⓝφ`` and the past mirrors
|
|
686
|
+
come out valid, as the Kripke evaluator reads them (it evaluates
|
|
687
|
+
``Always``/``Eventually`` over the reflexive-transitive CLOSURE of the
|
|
688
|
+
one-step relation). Until 0.28.1 NOTHING asserted these on this route,
|
|
689
|
+
so ``hybrid_is_valid(Ⓖ P → P, "S5")`` answered False while
|
|
690
|
+
``qml_is_valid`` on the same formula answered True — a bare "not valid"
|
|
691
|
+
about a formula the kit's own modal semantics validates.
|
|
692
|
+
- deontic: ``D`` serial (Standard Deontic Logic), so ``Ⓞφ → Ⓟφ`` is valid.
|
|
693
|
+
- agent-indexed: nothing unless ``systems`` asks, e.g.
|
|
694
|
+
``systems={"epistemic": "S5", "doxastic": "KD45"}`` — the families are
|
|
695
|
+
``epistemic`` / ``doxastic`` / ``assertive`` / ``bouletic`` and an unknown
|
|
696
|
+
one raises. A system for a family the formula never mentions contributes
|
|
697
|
+
nothing (same as ``qml_axioms``).
|
|
698
|
+
- sorted constants: ``c:S`` is an element of ``S`` at EVERY world (a constant is
|
|
699
|
+
a rigid designator), so each distinct ``c:S`` of the formula adds
|
|
700
|
+
``∀v0 S(c, v0)`` — the sort guard in the translation's own vocabulary (a
|
|
701
|
+
world as last argument), unguarded by any existence predicate, as in
|
|
702
|
+
``qml_axioms``. Without it ``Human(carl:Human)`` has a countermodel in
|
|
703
|
+
which ``carl`` is no ``Human``, and a route that reads Z3's ``sat`` as
|
|
704
|
+
"refuted" answers wrongly. A sort that occurs only through a constant needs
|
|
705
|
+
no non-emptiness axiom: the constant is its member.
|
|
706
|
+
|
|
707
|
+
Every axiom is closed over its own bound variables (``v0``, ``v1``, …), so
|
|
708
|
+
it can never capture anything in the translated formula, and they are
|
|
709
|
+
returned for the caller to pass as SEPARATE premises — never conjoined onto
|
|
710
|
+
the translation itself. The names are deliberately ones the kit's own
|
|
711
|
+
parser reads back: an axiom that prints as ``∀_hw0 R(_hw0, _hw0)`` is text
|
|
712
|
+
:func:`unicode_logic_kit.api.parse_any` rejects.
|
|
713
|
+
|
|
714
|
+
Raises:
|
|
715
|
+
ValueError: unknown frame system, or an unknown ``systems`` family.
|
|
716
|
+
UnsupportedFrameCondition: a condition with no first-order frame form.
|
|
717
|
+
"""
|
|
718
|
+
used = relations_used(formula)
|
|
719
|
+
axioms: List[Node] = []
|
|
720
|
+
# Resolve the frame FIRST, whatever the formula mentions: a typo in a frame
|
|
721
|
+
# name must fail loudly even for a formula with no alethic operator, or the
|
|
722
|
+
# caller would believe a system was applied that nothing ever looked at.
|
|
723
|
+
try:
|
|
724
|
+
alethic = resolve_frame(frame)
|
|
725
|
+
except ValueError as exc:
|
|
726
|
+
raise ValueError(f"frame_axioms: {exc}") from None
|
|
727
|
+
# ... and refuse a condition with no first-order form here too, rather than
|
|
728
|
+
# only when the formula happens to mention R: asking this route for GL is a
|
|
729
|
+
# request it cannot honour either way.
|
|
730
|
+
for cond in alethic:
|
|
731
|
+
if not is_first_order(cond):
|
|
732
|
+
raise UnsupportedFrameCondition(
|
|
733
|
+
f"frame_axioms: the frame condition {cond!r} has no "
|
|
734
|
+
f"first-order frame condition, so the standard translation "
|
|
735
|
+
f"cannot express {frame!r} (Löb, S4.1 and Grz need the "
|
|
736
|
+
f"higher-order routes: hol.isabelle_modal / hol.thf_modal, or "
|
|
737
|
+
f"the finite-frame enumerator atp.kripke_enum)")
|
|
738
|
+
if _R_ALETHIC in used:
|
|
739
|
+
axioms += [unguarded_frame_axiom(cond, _R_ALETHIC, prefix="v")
|
|
740
|
+
for cond in alethic]
|
|
741
|
+
if _R_TEMPORAL in used and temporal_closure:
|
|
742
|
+
axioms += [unguarded_frame_axiom("refl", _R_TEMPORAL, prefix="v"),
|
|
743
|
+
unguarded_frame_axiom("trans", _R_TEMPORAL, prefix="v")]
|
|
744
|
+
if _R_TEMPORAL in used and _R_NEXT in used:
|
|
745
|
+
# Vacuous — and misleading — unless BOTH relations occur.
|
|
746
|
+
axioms.append(_link(_R_NEXT, _R_TEMPORAL))
|
|
747
|
+
if temporal_closure:
|
|
748
|
+
axioms.append(_temporal_first_step())
|
|
749
|
+
if _R_DEONTIC in used:
|
|
750
|
+
axioms.append(unguarded_frame_axiom("serial", _R_DEONTIC, prefix="v"))
|
|
751
|
+
for family, system in dict(systems or {}).items():
|
|
752
|
+
if family not in _AGENT_FAMILY_PREFIX:
|
|
753
|
+
raise ValueError(
|
|
754
|
+
f"frame_axioms: unknown modal family {family!r} in systems "
|
|
755
|
+
f"(known: {sorted(_AGENT_FAMILY_PREFIX)})")
|
|
756
|
+
prefix = _AGENT_FAMILY_PREFIX[family]
|
|
757
|
+
try:
|
|
758
|
+
conds = resolve_frame(system)
|
|
759
|
+
except ValueError as exc:
|
|
760
|
+
raise ValueError(f"frame_axioms: {exc}") from None
|
|
761
|
+
for relation in sorted(r for r in used if r.startswith(prefix)):
|
|
762
|
+
axioms += [unguarded_frame_axiom(cond, relation, prefix="v")
|
|
763
|
+
for cond in conds]
|
|
764
|
+
for member in sort_membership_axioms(formula):
|
|
765
|
+
assert isinstance(member, Atom) # sort_membership_axioms yields atoms ``S(c)`` only
|
|
766
|
+
axioms.append(Quantifier(_FORALL, Variable("v0"), Atom(
|
|
767
|
+
_user_predicate(member.predicate), (member.args[0], Variable("v0")))))
|
|
768
|
+
return axioms
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _frame_axioms(frame: str) -> List[Node]:
|
|
772
|
+
"""Deprecated alias: :func:`frame_axioms` for an alethic-only formula.
|
|
773
|
+
|
|
774
|
+
Kept because the name was imported inside the kit; it cannot see which
|
|
775
|
+
relations a formula uses, so it only ever constrained ``R``.
|
|
776
|
+
"""
|
|
777
|
+
return frame_axioms(Box(Atom("P", ())), frame=frame)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def hybrid_is_valid(formula: Node, frame: str = "K", timeout: int = 10000,
|
|
781
|
+
systems=None, temporal_closure: bool = True) -> bool:
|
|
782
|
+
"""Return True iff the hybrid-modal ``formula`` is valid over ``frame`` (via Z3).
|
|
783
|
+
|
|
784
|
+
Validity of H(@) over a frame class: true at EVERY world of EVERY Kripke
|
|
785
|
+
model whose alethic relation satisfies the frame conditions, under EVERY
|
|
786
|
+
nominal assignment. The check is the standard translation closed over the
|
|
787
|
+
current world under the frame axioms::
|
|
788
|
+
|
|
789
|
+
frame_axioms → ∀w ST(formula)(w)
|
|
790
|
+
|
|
791
|
+
handed to the Z3 validity oracle. The nominal constants ``nom_i`` are left
|
|
792
|
+
FREE in that implication — first-order validity quantifies free constants
|
|
793
|
+
universally, which is exactly "for every nominal assignment" (each constant
|
|
794
|
+
denotes exactly one domain element = one world, matching a nominal's
|
|
795
|
+
name-exactly-one-world semantics).
|
|
796
|
+
|
|
797
|
+
``frame`` constrains the ALETHIC relation and is any system of the shared
|
|
798
|
+
registry (:mod:`unicode_logic_kit.fol.frames`) or a Scott–Lemmon spec; a
|
|
799
|
+
system with no first-order condition (GL, S4.1, Grz) is refused by name.
|
|
800
|
+
The OTHER relations the translation emits get the conventions
|
|
801
|
+
:func:`frame_axioms` documents — temporal ``T`` reflexive-transitive with
|
|
802
|
+
``N ⊆ T`` (``temporal_closure=True``) and deontic ``D`` serial, both ON by
|
|
803
|
+
default, so this route agrees with ``fol.qml`` and with the Kripke
|
|
804
|
+
evaluator instead of reporting "not valid" for ``Ⓖφ → φ``; the
|
|
805
|
+
agent-indexed epistemic / doxastic / assertive / bouletic relations stay K
|
|
806
|
+
unless ``systems={"epistemic": "S5", …}`` asks for more.
|
|
807
|
+
|
|
808
|
+
Soundness/completeness: first-order validity is only semi-decidable in
|
|
809
|
+
general, so ``is_valid`` may time out (returning False) on hard instances —
|
|
810
|
+
but hybrid logic H(@) over K is DECIDABLE, and the ST images of H(@)
|
|
811
|
+
formulas (two-variable-like, tiny) are well within Z3's reach in practice;
|
|
812
|
+
the frame-axiom variants used here (T/S4/S5) behave the same on these
|
|
813
|
+
inputs. ``True`` is always a real proof; treat ``False`` as
|
|
814
|
+
"not proven valid" (for these small hybrid instances: a genuine
|
|
815
|
+
countermodel).
|
|
816
|
+
|
|
817
|
+
Raises:
|
|
818
|
+
NotImplementedError: ``formula`` contains a ``Down`` node (the ↓
|
|
819
|
+
binder, N1). Adding ↓ makes hybrid validity UNDECIDABLE, so this
|
|
820
|
+
function's bare-``bool`` contract — where ``False`` is safe to
|
|
821
|
+
read as "a genuine countermodel" precisely BECAUSE H(@) over K is
|
|
822
|
+
decidable and Z3 reliably closes these small instances — no
|
|
823
|
+
longer holds: a bare ``False`` on a ↓-formula could equally be a
|
|
824
|
+
countermodel or an honest Z3 timeout on an undecidable query, and
|
|
825
|
+
this function has no second field to tell them apart. Use
|
|
826
|
+
:func:`down_is_valid` instead (a :class:`~unicode_logic_kit.atp.protocol.Verdict`,
|
|
827
|
+
PROVED-only — never claims REFUTED) or
|
|
828
|
+
:func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search` /
|
|
829
|
+
:class:`~unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend` for a
|
|
830
|
+
bounded-search REFUTED verdict.
|
|
831
|
+
"""
|
|
832
|
+
for n in formula.walk():
|
|
833
|
+
if isinstance(n, Down):
|
|
834
|
+
raise NotImplementedError(
|
|
835
|
+
"hybrid_is_valid: the ↓ binder (Down) makes hybrid validity "
|
|
836
|
+
"undecidable, so this bare-bool, PROVED-and-REFUTED-conflating "
|
|
837
|
+
"check cannot honestly answer for it. Use down_is_valid "
|
|
838
|
+
"(PROVED-only, never REFUTED) or "
|
|
839
|
+
"unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend / "
|
|
840
|
+
"modal_enum_search (bounded search, REFUTED-only) instead."
|
|
841
|
+
)
|
|
842
|
+
from ..atp.z3_models import is_valid # local import (as in fol.qml): keeps fol importable without z3
|
|
843
|
+
w = Variable("w")
|
|
844
|
+
closed = Quantifier(_FORALL, w, standard_translation(formula, world="w"))
|
|
845
|
+
hyp = None
|
|
846
|
+
for axiom in frame_axioms(formula, frame, systems=systems,
|
|
847
|
+
temporal_closure=temporal_closure):
|
|
848
|
+
hyp = axiom if hyp is None else And(hyp, axiom)
|
|
849
|
+
goal = closed if hyp is None else Implies(hyp, closed)
|
|
850
|
+
return is_valid(goal, timeout=timeout)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
# =========================
|
|
854
|
+
# ↓ validity via the standard translation + a direct Z3 solver call (N1)
|
|
855
|
+
# =========================
|
|
856
|
+
#
|
|
857
|
+
# down_is_valid is hybrid_is_valid's PROVED-only sibling for the FULL hybrid
|
|
858
|
+
# language H(@,↓). It exists as a SEPARATE function, not a keyword flag on
|
|
859
|
+
# hybrid_is_valid, precisely because the two make different promises:
|
|
860
|
+
# hybrid_is_valid's bare bool is safe only because H(@) over K is decidable
|
|
861
|
+
# (so "not proved" IS "refuted" there); down_is_valid must call the Z3
|
|
862
|
+
# Solver() directly — never the bare-bool is_valid() wrapper, which collapses
|
|
863
|
+
# Z3's 'sat' (a genuine countermodel) and 'unknown' (timeout / incompleteness
|
|
864
|
+
# on an undecidable query) into the same False (unicode_logic_kit.atp.z3_models
|
|
865
|
+
# .is_valid, confirmed by direct reading) — so it can tell the two apart and
|
|
866
|
+
# report them honestly as UNKNOWN, never smuggling a REFUTED claim out of a
|
|
867
|
+
# SAT witness this route never verified against a presentable finite
|
|
868
|
+
# KripkeModel (that verification is KripkeEnumBackend's job — see
|
|
869
|
+
# fol._hybrid_nodes' module docstring and atp.hybrid_down.down_decide, which
|
|
870
|
+
# combines the two into one call).
|
|
871
|
+
|
|
872
|
+
def down_is_valid(formula: Node, frame: str = "K", timeout: int = 10000,
|
|
873
|
+
systems=None, temporal_closure: bool = True) -> "Verdict":
|
|
874
|
+
"""Return a :class:`~unicode_logic_kit.atp.protocol.Verdict` for the FULL
|
|
875
|
+
hybrid-modal ``formula`` (H(@,↓), including ``Down``/↓) over ``frame``.
|
|
876
|
+
|
|
877
|
+
Builds the exact same goal ``hybrid_is_valid`` does — the standard
|
|
878
|
+
translation, closed over the current world, under the frame axioms
|
|
879
|
+
(``standard_translation`` now threads ``Down``'s local rebinding through,
|
|
880
|
+
see the module docstring) — but decides it with a Z3 ``Solver()`` called
|
|
881
|
+
directly, so ``unsat`` (of the negated goal) and ``sat``/``unknown`` are
|
|
882
|
+
told apart instead of collapsed:
|
|
883
|
+
|
|
884
|
+
- Z3 ``unsat`` → ``PROVED`` — sound unconditionally: soundness of a
|
|
885
|
+
Z3-``unsat`` verdict depends only on Z3's own soundness, never on
|
|
886
|
+
whether Z3 is a COMPLETE decision procedure for this (undecidable)
|
|
887
|
+
fragment (see ``fol._hybrid_nodes``' module docstring for the
|
|
888
|
+
co-r.e. argument this rests on: ST is meaning-preserving for H(@,↓)
|
|
889
|
+
— Areces/Blackburn/Marx 1999 — so FO-``unsat``-of-the-negation IS
|
|
890
|
+
H(@,↓)-validity, exactly).
|
|
891
|
+
- Z3 ``sat`` (of the negated goal) → ``UNKNOWN`` / ``reason="incomplete"``
|
|
892
|
+
— NEVER ``REFUTED``. A SAT witness here would, ON INSPECTION, also be a
|
|
893
|
+
sound Kripke countermodel in principle (same correspondence as above,
|
|
894
|
+
run in the other direction) — this is a deliberate COMPLETENESS
|
|
895
|
+
sacrifice, not a soundness requirement, made to sidestep turning an
|
|
896
|
+
arbitrary Z3 model back into a presentable, inspectable finite
|
|
897
|
+
:class:`~unicode_logic_kit.semantics.kripke.KripkeModel`. Use
|
|
898
|
+
:func:`~unicode_logic_kit.atp.kripke_enum.modal_enum_search` /
|
|
899
|
+
:class:`~unicode_logic_kit.atp.kripke_enum.KripkeEnumBackend` for an
|
|
900
|
+
actual REFUTED verdict, with a countermodel independently
|
|
901
|
+
re-verified by :func:`~unicode_logic_kit.semantics.kripke.satisfies_modal`
|
|
902
|
+
— or :func:`~unicode_logic_kit.atp.hybrid_down.down_decide`, which runs
|
|
903
|
+
both routes and combines them.
|
|
904
|
+
- Z3 times out → ``UNKNOWN`` / ``reason="timeout"``.
|
|
905
|
+
|
|
906
|
+
This function decides ANY formula ``standard_translation`` accepts
|
|
907
|
+
(``Down`` or not) — it is not restricted to ↓-containing input; a plain
|
|
908
|
+
H(@) formula is handled identically, just more conservatively than
|
|
909
|
+
``hybrid_is_valid`` (which, for THAT decidable fragment, is entitled to —
|
|
910
|
+
and does — read Z3 ``sat``/``unknown`` as a genuine countermodel).
|
|
911
|
+
"""
|
|
912
|
+
import time
|
|
913
|
+
from ..atp.protocol import PROVED, UNKNOWN, Verdict # local: keeps fol importable without atp
|
|
914
|
+
from z3 import Not as _ZNot, Solver, sat, unsat
|
|
915
|
+
|
|
916
|
+
w = Variable("w")
|
|
917
|
+
closed = Quantifier(_FORALL, w, standard_translation(formula, world="w"))
|
|
918
|
+
hyp = None
|
|
919
|
+
for axiom in frame_axioms(formula, frame, systems=systems,
|
|
920
|
+
temporal_closure=temporal_closure):
|
|
921
|
+
hyp = axiom if hyp is None else And(hyp, axiom)
|
|
922
|
+
goal = closed if hyp is None else Implies(hyp, closed)
|
|
923
|
+
|
|
924
|
+
solver = Solver()
|
|
925
|
+
solver.set("timeout", timeout)
|
|
926
|
+
solver.set("random_seed", 42)
|
|
927
|
+
solver.add(_ZNot(goal.to_z3()))
|
|
928
|
+
start = time.perf_counter()
|
|
929
|
+
result = solver.check()
|
|
930
|
+
elapsed = time.perf_counter() - start
|
|
931
|
+
|
|
932
|
+
if result == unsat:
|
|
933
|
+
return Verdict(PROVED, "down_is_valid", logic="hybrid", wall_time=elapsed)
|
|
934
|
+
if result == sat:
|
|
935
|
+
return Verdict(
|
|
936
|
+
UNKNOWN, "down_is_valid", logic="hybrid", reason="incomplete", wall_time=elapsed,
|
|
937
|
+
detail=("Z3 found a model of the negated goal; down_is_valid never "
|
|
938
|
+
"reads this as REFUTED (see its own docstring) — use "
|
|
939
|
+
"atp.kripke_enum.KripkeEnumBackend / modal_enum_search, or "
|
|
940
|
+
"atp.hybrid_down.down_decide, for a genuine countermodel."))
|
|
941
|
+
why = solver.reason_unknown()
|
|
942
|
+
reason = "timeout" if ("timeout" in why or "cancel" in why) else "incomplete"
|
|
943
|
+
return Verdict(UNKNOWN, "down_is_valid", logic="hybrid", reason=reason,
|
|
944
|
+
wall_time=elapsed, detail=why)
|