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,566 @@
|
|
|
1
|
+
"""Five per-logic :class:`~unicode_logic_kit.atp.protocol.ProverBackend` adapters.
|
|
2
|
+
|
|
3
|
+
Each substructural / non-classical logic below already has its own
|
|
4
|
+
hand-built decision procedure or bounded search elsewhere in the kit
|
|
5
|
+
(:mod:`atp.lj`, :mod:`atp.lambek`, :mod:`atp.linear`,
|
|
6
|
+
:mod:`semantics.relevant`, :mod:`fol.modal_translation`). This module wraps
|
|
7
|
+
each one in the uniform :class:`~unicode_logic_kit.atp.protocol.Verdict`
|
|
8
|
+
contract WITHOUT pretending the five share one shape — they do not, so each
|
|
9
|
+
adapter is reasoned about on its own terms (what a ``True``/``False``/
|
|
10
|
+
``None`` from the underlying routine actually PROVES, versus where it is
|
|
11
|
+
merely "nothing found within the bound") rather than registered
|
|
12
|
+
mechanically off one template:
|
|
13
|
+
|
|
14
|
+
* :class:`IntBackend` (``logic="intuitionistic"``) — Dyckhoff's G4ip via
|
|
15
|
+
:func:`~unicode_logic_kit.atp.lj.int_prove`, a genuine terminating decision
|
|
16
|
+
procedure on the propositional fragment. REFUTED carries a best-effort
|
|
17
|
+
Kripke witness from
|
|
18
|
+
:func:`~unicode_logic_kit.semantics.intuitionistic.int_countermodel`.
|
|
19
|
+
* :class:`LambekBackend` (``logic="lambek"``) — the Lambek calculus L via
|
|
20
|
+
:func:`~unicode_logic_kit.atp.lambek.lambek_derivable`. ``premises`` is the
|
|
21
|
+
LITERAL ORDERED antecedent (word order is the entire point of L), so the
|
|
22
|
+
shared :func:`~unicode_logic_kit.atp.protocol._implication` And-fold is
|
|
23
|
+
never used here — L has no exchange/weakening to make a conjunction
|
|
24
|
+
interchangeable with its conjuncts in the first place.
|
|
25
|
+
* :class:`IllBackend` (``logic="ill"``) — intuitionistic linear logic via
|
|
26
|
+
:func:`~unicode_logic_kit.atp.linear.ill_derivable`: a complete decision
|
|
27
|
+
procedure on the ``!``-free fragment, a depth/step-bounded search once
|
|
28
|
+
``!`` occurs (see :func:`~unicode_logic_kit.atp.linear._has_bang`).
|
|
29
|
+
* :class:`RelevantBackend` (``logic="relevant"``) — the relevant logic B via
|
|
30
|
+
:func:`~unicode_logic_kit.semantics.relevant.rel_countermodel`. NEVER
|
|
31
|
+
PROVED: ``rel_valid``/``rel_countermodel`` are sound but INCOMPLETE (a
|
|
32
|
+
bounded Routley-Meyer search), so "no countermodel found" is honestly
|
|
33
|
+
UNKNOWN(bound_hit). A genuine PROVED route needs a real Isabelle proof
|
|
34
|
+
(:mod:`hol.isabelle_relevant`'s ``to_isabelle_relevant``/``battery_proof``
|
|
35
|
+
driven as a subprocess) and is an explicit out-of-scope follow-up.
|
|
36
|
+
* :class:`HybridBackend` (``logic="hybrid"``) — hybrid modal logic H(@) via
|
|
37
|
+
the standard translation (:mod:`fol.modal_translation`) into classical
|
|
38
|
+
FOL, closed under the frame axioms and decided by a per-call tracked Z3
|
|
39
|
+
``Solver()`` (:func:`~unicode_logic_kit.atp.protocol._z3_track_and_check`,
|
|
40
|
+
the same routine :class:`~unicode_logic_kit.atp.protocol.Z3Backend` uses) —
|
|
41
|
+
deliberately NOT
|
|
42
|
+
:func:`~unicode_logic_kit.fol.modal_translation.hybrid_is_valid` (nor the
|
|
43
|
+
:func:`~unicode_logic_kit.atp.z3_models.is_valid` underneath it), whose bare
|
|
44
|
+
bool collapses a genuine countermodel (SAT) and a solver timeout/unknown
|
|
45
|
+
into the same ``False``.
|
|
46
|
+
|
|
47
|
+
Routing: :func:`unicode_logic_kit.api.prove`'s ``logic="auto"`` detects
|
|
48
|
+
``"lambek"`` / ``"ill"`` / ``"hybrid"`` from an unambiguous syntactic
|
|
49
|
+
marker — each is the sole occupant of its own parser mode's node types
|
|
50
|
+
(Product/Under/Over; the ``linear`` mode's Tensor/With/OPlus/LinearImplies/
|
|
51
|
+
OfCourse/One/Top/Zero; Nominal/At). Intuitionistic and relevant logic reuse
|
|
52
|
+
the PLAIN classical propositional AST with no syntactic marker of their
|
|
53
|
+
own, so ``logic=`` must always be given explicitly for those two —
|
|
54
|
+
auto-detection never guesses between a "classical", "intuitionistic" and
|
|
55
|
+
"relevant" reading of the same formula.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
import time
|
|
59
|
+
from typing import Optional, Sequence
|
|
60
|
+
|
|
61
|
+
from .._deadline import instant as _instant, run_until as _run_until
|
|
62
|
+
from ..fol.nodes import Node
|
|
63
|
+
from ._substructural_input import ILL, LAMBEK, Calculus, unreadable_reason
|
|
64
|
+
from .protocol import (
|
|
65
|
+
ProverBackend, Verdict, PROVED, REFUTED, UNKNOWN,
|
|
66
|
+
_timed, _implication, _z3_track_and_check, _z3_model_assignment,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"IntBackend", "LambekBackend", "IllBackend", "RelevantBackend", "HybridBackend",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _unreadable_refusal(logic: str, calculus: Calculus, formula: Node,
|
|
75
|
+
premises: Sequence[Node]) -> Optional[str]:
|
|
76
|
+
"""Why a propositional substructural calculus cannot read this input, or ``None``.
|
|
77
|
+
|
|
78
|
+
Intuitionistic linear logic and the Lambek calculus read the connectives they have rules
|
|
79
|
+
for (``calculus.glyphs``) and atoms: no individuals, no sorts, no quantifiers, no counting,
|
|
80
|
+
no identity, and no connective of another logic. An atom over terms is one category (a
|
|
81
|
+
sound reading, argued in ``atp/_substructural_input.py``); any other node read as one more
|
|
82
|
+
opaque category would make a derivability verdict about a DIFFERENT formula
|
|
83
|
+
(``∀x P(x)`` does not derive ``P(alpha)`` there, and it does in every first-order
|
|
84
|
+
reading; ``And(A, B)`` does not derive ``A``, and it does classically), so the node is
|
|
85
|
+
refused by name instead and the text says what to do.
|
|
86
|
+
"""
|
|
87
|
+
return unreadable_reason([*premises, formula], logic, calculus)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Witness serializers — IntKripkeModel / RelevantModel are not JSON-native.
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def _int_kripke_witness_to_dict(model, world: int) -> dict:
|
|
95
|
+
"""JSON-able witness for an
|
|
96
|
+
:class:`~unicode_logic_kit.semantics.intuitionistic.IntKripkeModel`
|
|
97
|
+
countermodel: the up-set order, the monotone valuation, and (for a
|
|
98
|
+
first-order witness, which :class:`IntBackend` never actually reaches —
|
|
99
|
+
see its docstring) the per-world domains.
|
|
100
|
+
"""
|
|
101
|
+
out = {
|
|
102
|
+
"kind": "intuitionistic_kripke",
|
|
103
|
+
"world": world,
|
|
104
|
+
"upset": {str(w): sorted(ws) for w, ws in model.upset.items()},
|
|
105
|
+
"valuation": {k: sorted(ws) for k, ws in model.valuation.items()},
|
|
106
|
+
}
|
|
107
|
+
if model.domains is not None:
|
|
108
|
+
out["domains"] = {str(w): sorted(ds) for w, ds in model.domains.items()}
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _relevant_model_to_dict(model, world: str) -> dict:
|
|
113
|
+
"""JSON-able witness for a
|
|
114
|
+
:class:`~unicode_logic_kit.semantics.relevant.RelevantModel` countermodel.
|
|
115
|
+
|
|
116
|
+
``RelevantModel`` is frozen/hashable but not JSON-native (its
|
|
117
|
+
``star``/``valuation`` are the module's own ``_FrozenMap``); this
|
|
118
|
+
unpacks every field the constructor takes into plain lists/dicts, so
|
|
119
|
+
both ``json.dumps`` and rebuilding a ``RelevantModel`` from the dict
|
|
120
|
+
round-trip cleanly (the differential test exercises exactly that).
|
|
121
|
+
"""
|
|
122
|
+
return {
|
|
123
|
+
"kind": "relevant_routley_meyer",
|
|
124
|
+
"world": world,
|
|
125
|
+
"worlds": list(model.worlds),
|
|
126
|
+
"normal": sorted(model.normal),
|
|
127
|
+
"star": {w: model.star[w] for w in model.worlds},
|
|
128
|
+
"R": sorted([list(triple) for triple in model.R]),
|
|
129
|
+
"valuation": {k: sorted(ws) for k, ws in model.valuation.items()},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Intuitionistic propositional logic
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
class IntBackend(ProverBackend):
|
|
138
|
+
"""Propositional intuitionistic logic via Dyckhoff's G4ip (:mod:`atp.lj`).
|
|
139
|
+
|
|
140
|
+
``int_prove`` is a genuine, terminating decision procedure on the
|
|
141
|
+
propositional fragment (see its own docstring): PROVED and REFUTED are
|
|
142
|
+
both definitive. Quantified input is out of scope for G4ip (propositional
|
|
143
|
+
only — :func:`~unicode_logic_kit.atp.lj._reject_quantified`); it comes back
|
|
144
|
+
UNKNOWN(unsupported) rather than raising, so a batch run over mixed-arity
|
|
145
|
+
formulas records the gap instead of crashing. A REFUTED verdict's witness
|
|
146
|
+
is a best-effort call to
|
|
147
|
+
:func:`~unicode_logic_kit.semantics.intuitionistic.int_countermodel` on the
|
|
148
|
+
folded goal ``(∧ premises) → formula`` — the REFUTED status itself is
|
|
149
|
+
always definitive (it comes from ``int_prove``, not from this search),
|
|
150
|
+
but the witness search is NOT: ``int_countermodel``'s bounded Kripke
|
|
151
|
+
search at its default ``max_worlds=3`` can still miss a countermodel for
|
|
152
|
+
some genuine non-theorems, since the finite-model-property bound grows
|
|
153
|
+
with the formula (see ``int_valid``'s own docstring for a worked
|
|
154
|
+
example needing 4 worlds). The ``Verdict`` contract permits
|
|
155
|
+
``countermodel=None`` for exactly this reason, so a bound miss never
|
|
156
|
+
turns into a broken Verdict — only a REFUTED with no witness attached.
|
|
157
|
+
|
|
158
|
+
The call's ``timeout`` bounds both searches
|
|
159
|
+
(:func:`~unicode_logic_kit._deadline.run_until`): an ``int_prove`` that has not
|
|
160
|
+
finished is UNKNOWN(timeout), and a witness search that has not finished
|
|
161
|
+
leaves a REFUTED verdict without its witness. An ``int_prove`` that spends its
|
|
162
|
+
step budget (the calculus terminates, but the number of steps is exponential in
|
|
163
|
+
the nesting of implications) is UNKNOWN(bound_hit): a spent budget is a bound,
|
|
164
|
+
not a failure of the backend.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
name = "intuitionistic"
|
|
168
|
+
logics = frozenset({"intuitionistic"})
|
|
169
|
+
external = False
|
|
170
|
+
|
|
171
|
+
def available(self) -> bool:
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
175
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
176
|
+
from .lj import int_prove, _MAX_STEPS
|
|
177
|
+
from ..semantics.intuitionistic import int_countermodel
|
|
178
|
+
|
|
179
|
+
premises = list(premises)
|
|
180
|
+
cm_kwargs = {k: options.pop(k) for k in
|
|
181
|
+
("max_worlds", "domain_elements", "max_steps") if k in options}
|
|
182
|
+
deadline = _instant(timeout)
|
|
183
|
+
started = time.perf_counter()
|
|
184
|
+
try:
|
|
185
|
+
(finished, proved), elapsed = _timed(
|
|
186
|
+
lambda: _run_until(deadline, lambda: int_prove(premises, formula)))
|
|
187
|
+
except NotImplementedError as exc:
|
|
188
|
+
return Verdict(UNKNOWN, self.name, logic="intuitionistic",
|
|
189
|
+
reason="unsupported", detail=str(exc))
|
|
190
|
+
except RecursionError:
|
|
191
|
+
# The search recurses once per rule it applies along a branch of the derivation,
|
|
192
|
+
# so the interpreter's recursion limit is a bound of it, next to the step budget:
|
|
193
|
+
# a formula whose nesting is small but whose search is long (nested Peirce
|
|
194
|
+
# formulas) reaches it. Nothing was decided.
|
|
195
|
+
import sys
|
|
196
|
+
return Verdict(UNKNOWN, self.name, logic="intuitionistic", reason="bound_hit",
|
|
197
|
+
wall_time=time.perf_counter() - started,
|
|
198
|
+
detail="the proof search recursed deeper than the interpreter's "
|
|
199
|
+
f"recursion limit ({sys.getrecursionlimit()}) allows before "
|
|
200
|
+
"it decided the sequent: no verdict was reached")
|
|
201
|
+
except RuntimeError as exc:
|
|
202
|
+
# The prover's step counter ran out; any other RuntimeError is a failure.
|
|
203
|
+
if "step budget" not in str(exc):
|
|
204
|
+
raise
|
|
205
|
+
return Verdict(UNKNOWN, self.name, logic="intuitionistic", reason="bound_hit",
|
|
206
|
+
wall_time=time.perf_counter() - started,
|
|
207
|
+
detail=f"the prover used up its step budget ({_MAX_STEPS} steps) "
|
|
208
|
+
"before it decided the sequent: G4ip terminates, but the "
|
|
209
|
+
"number of steps is exponential in the nesting of "
|
|
210
|
+
"implications, so no verdict was reached")
|
|
211
|
+
if not finished:
|
|
212
|
+
# ``int_prove`` is a decision procedure, but its cost is exponential in the formula
|
|
213
|
+
return Verdict(UNKNOWN, self.name, logic="intuitionistic", reason="timeout",
|
|
214
|
+
wall_time=elapsed,
|
|
215
|
+
detail=f"no verdict within the {timeout} ms limit")
|
|
216
|
+
if proved:
|
|
217
|
+
return Verdict(PROVED, self.name, logic="intuitionistic", wall_time=elapsed)
|
|
218
|
+
|
|
219
|
+
# The verdict is settled; the witness is a second, best-effort search that gets what
|
|
220
|
+
# is left of the limit, and a REFUTED answer needs no witness (``countermodel=None``).
|
|
221
|
+
goal = _implication(formula, premises)
|
|
222
|
+
found, cm = _run_until(deadline, lambda: int_countermodel(goal, **cm_kwargs))
|
|
223
|
+
witness = _int_kripke_witness_to_dict(*cm) if found and cm is not None else None
|
|
224
|
+
return Verdict(REFUTED, self.name, logic="intuitionistic", wall_time=elapsed,
|
|
225
|
+
countermodel=witness)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# The Lambek calculus L
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
class LambekBackend(ProverBackend):
|
|
233
|
+
"""The Lambek calculus L via :mod:`atp.lambek` — a complete, terminating
|
|
234
|
+
decision procedure (see that module's docstring: every rule's premises
|
|
235
|
+
are strictly smaller than its conclusion, so ``lambek_prove`` returning
|
|
236
|
+
``None`` PROVES underivability).
|
|
237
|
+
|
|
238
|
+
``premises`` is read as the sequence's LITERAL ORDER, never folded
|
|
239
|
+
through the shared ``∧``-based :func:`~unicode_logic_kit.atp.protocol._implication`
|
|
240
|
+
helper: L drops exchange along with weakening/contraction, so
|
|
241
|
+
``A, A\\B ⊢ B`` and ``A\\B, A ⊢ B`` are different sequents with different
|
|
242
|
+
answers — collapsing ``premises`` into an unordered classical conjunction
|
|
243
|
+
would silently erase exactly the distinction this calculus exists to
|
|
244
|
+
draw (the regression battery in ``tests/test_logic_backends.py`` pins
|
|
245
|
+
this pair directly). Every PROVED verdict is re-validated internally by
|
|
246
|
+
:func:`~unicode_logic_kit.atp.lambek.check_lambek_proof` (inside
|
|
247
|
+
``lambek_prove``), so a search bug can only lose proofs, never invent
|
|
248
|
+
one. A REFUTED verdict's ``countermodel`` is always ``None`` — L has no
|
|
249
|
+
semantic model theory in this kit, only the syntactic decision procedure
|
|
250
|
+
(which is itself exhaustive, so ``None`` from the search IS the
|
|
251
|
+
refutation, not an admission of incompleteness).
|
|
252
|
+
|
|
253
|
+
The calculus reads the connectives ``• \\ /`` and atoms; an atom over terms is one
|
|
254
|
+
category. Every other node is refused by name, UNKNOWN(unsupported): a quantifier (sorted
|
|
255
|
+
or not), a counting or cardinality node, a sorted constant, an equality atom and a node of
|
|
256
|
+
another logic (``And``, ``Not``, ``Box``, ...) have no rule in L, and an opaque category
|
|
257
|
+
in their place would answer about another formula. So is an EMPTY premise list: L has no
|
|
258
|
+
sequent with an empty antecedent (Lambek's restriction), so there is nothing to decide,
|
|
259
|
+
and a backend states that as an answer instead of raising.
|
|
260
|
+
|
|
261
|
+
The call's ``timeout`` bounds the search (:func:`~unicode_logic_kit._deadline.run_until`):
|
|
262
|
+
the search is exhaustive and terminating, and its cost is exponential in the sequent, so a
|
|
263
|
+
search that has not finished at the limit is UNKNOWN(timeout), never a refutation.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
name = "lambek"
|
|
267
|
+
logics = frozenset({"lambek"})
|
|
268
|
+
external = False
|
|
269
|
+
|
|
270
|
+
def available(self) -> bool:
|
|
271
|
+
return True
|
|
272
|
+
|
|
273
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
274
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
275
|
+
from .lambek import lambek_prove
|
|
276
|
+
|
|
277
|
+
premises = list(premises)
|
|
278
|
+
deadline = _instant(timeout)
|
|
279
|
+
if not premises:
|
|
280
|
+
return Verdict(UNKNOWN, self.name, logic="lambek", reason="unsupported",
|
|
281
|
+
detail="lambek: decide() needs a nonempty, ORDERED premises "
|
|
282
|
+
"sequence — the Lambek calculus has no sequent with an "
|
|
283
|
+
"empty antecedent (Lambek's own restriction; see "
|
|
284
|
+
"atp.lambek's module docstring), so there is no notion "
|
|
285
|
+
"of unconditional validity to fall back to. Give the "
|
|
286
|
+
"premises the goal is read from, in order.")
|
|
287
|
+
refusal = _unreadable_refusal("lambek", LAMBEK, formula, premises)
|
|
288
|
+
if refusal is not None:
|
|
289
|
+
return Verdict(UNKNOWN, self.name, logic="lambek", reason="unsupported",
|
|
290
|
+
detail=refusal)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
(finished, derivation), elapsed = _timed(
|
|
294
|
+
lambda: _run_until(deadline, lambda: lambek_prove(premises, formula)))
|
|
295
|
+
except NotImplementedError as exc:
|
|
296
|
+
return Verdict(UNKNOWN, self.name, logic="lambek",
|
|
297
|
+
reason="unsupported", detail=str(exc))
|
|
298
|
+
if not finished:
|
|
299
|
+
# The search is exhaustive and terminating, but its cost is exponential in the sequent.
|
|
300
|
+
return Verdict(UNKNOWN, self.name, logic="lambek", reason="timeout",
|
|
301
|
+
wall_time=elapsed,
|
|
302
|
+
detail=f"no verdict within the {timeout} ms limit")
|
|
303
|
+
if derivation is not None:
|
|
304
|
+
return Verdict(PROVED, self.name, logic="lambek", wall_time=elapsed)
|
|
305
|
+
return Verdict(REFUTED, self.name, logic="lambek", wall_time=elapsed,
|
|
306
|
+
countermodel=None,
|
|
307
|
+
detail="no cut-free L derivation exists — lambek_prove's "
|
|
308
|
+
"backward search is exhaustive and terminating "
|
|
309
|
+
"(every rule strictly shrinks the sequent), so this "
|
|
310
|
+
"is a genuine syntactic refutation, not a bound")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# Intuitionistic linear logic (ILL)
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
class IllBackend(ProverBackend):
|
|
318
|
+
"""Intuitionistic linear logic (ILL) via :mod:`atp.linear`.
|
|
319
|
+
|
|
320
|
+
The cut-free backward search is a complete, terminating decision
|
|
321
|
+
procedure on the ``!``-FREE fragment (every rule strictly shrinks the
|
|
322
|
+
sequent there — see that module's docstring), so PROVED/REFUTED are both
|
|
323
|
+
definitive when no ``!`` occurs (checked via
|
|
324
|
+
:func:`~unicode_logic_kit.atp.linear._has_bang` on ``formula`` and every
|
|
325
|
+
premise) AND the depth bound in force is at least the sequent's own
|
|
326
|
+
"safe" bound (``total`` size when ``!``-free, ``2*total + 4`` once ``!``
|
|
327
|
+
occurs — exactly :func:`~unicode_logic_kit.atp.linear.ill_prove`'s own
|
|
328
|
+
default). A caller-supplied ``max_depth`` can shrink the search below
|
|
329
|
+
that safe bound (e.g. to force the bounded path deterministically for
|
|
330
|
+
testing); a ``False`` result from a search truncated below the safe
|
|
331
|
+
bound proves nothing, so it is reported UNKNOWN(bound_hit) too, never
|
|
332
|
+
REFUTED. Once ``!`` occurs, the contraction rule ``!C`` can GROW a
|
|
333
|
+
sequent, so the search is depth/step-bounded there regardless, and a
|
|
334
|
+
``False`` means only "no derivation found within the bound" — reported
|
|
335
|
+
as UNKNOWN(bound_hit), NEVER REFUTED, exactly matching
|
|
336
|
+
:func:`~unicode_logic_kit.atp.linear.ill_prove`'s own honesty contract.
|
|
337
|
+
|
|
338
|
+
The calculus reads the connectives ``⊗ & ⊕ ⊸ ! 𝟙 ⊤ 𝟘`` and atoms; an atom over terms is
|
|
339
|
+
one category. Every other node is refused by name, UNKNOWN(unsupported): a quantifier
|
|
340
|
+
(sorted or not), a counting or cardinality node, a sorted constant, an equality atom and a
|
|
341
|
+
node of another logic (``And``, ``Not``, ``Box``, ...) have no rule in ILL, and an opaque
|
|
342
|
+
category in their place would answer about another formula.
|
|
343
|
+
|
|
344
|
+
The call's ``timeout`` bounds the search (:func:`~unicode_logic_kit._deadline.run_until`):
|
|
345
|
+
even the ``!``-free search, a complete decision procedure, is exponential in the sequent,
|
|
346
|
+
so a search that has not finished at the limit is UNKNOWN(timeout), never a refutation.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
name = "ill"
|
|
350
|
+
logics = frozenset({"ill"})
|
|
351
|
+
external = False
|
|
352
|
+
|
|
353
|
+
def available(self) -> bool:
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
357
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
358
|
+
from .linear import ill_derivable, _has_bang, _size
|
|
359
|
+
|
|
360
|
+
premises = list(premises)
|
|
361
|
+
deadline = _instant(timeout)
|
|
362
|
+
refusal = _unreadable_refusal("ill", ILL, formula, premises)
|
|
363
|
+
if refusal is not None:
|
|
364
|
+
return Verdict(UNKNOWN, self.name, logic="ill", reason="unsupported",
|
|
365
|
+
detail=refusal)
|
|
366
|
+
max_depth = options.pop("max_depth", None)
|
|
367
|
+
max_steps = options.pop("max_steps", 200000)
|
|
368
|
+
bang = _has_bang(formula) or any(_has_bang(p) for p in premises)
|
|
369
|
+
# The same "safe" depth ill_prove itself falls back to when max_depth
|
|
370
|
+
# is None (see its docstring) — a smaller CALLER-supplied max_depth
|
|
371
|
+
# is an intentionally truncated search, not a complete one.
|
|
372
|
+
total = sum(_size(p) for p in premises) + _size(formula)
|
|
373
|
+
safe_depth = 2 * total + 4 if bang else total
|
|
374
|
+
bounded = max_depth is not None and max_depth < safe_depth
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
(finished, proved), elapsed = _timed(
|
|
378
|
+
lambda: _run_until(deadline, lambda: ill_derivable(
|
|
379
|
+
premises, formula, max_depth=max_depth, max_steps=max_steps)))
|
|
380
|
+
except NotImplementedError as exc:
|
|
381
|
+
return Verdict(UNKNOWN, self.name, logic="ill",
|
|
382
|
+
reason="unsupported", detail=str(exc))
|
|
383
|
+
if not finished:
|
|
384
|
+
# Even the !-free search, a complete decision procedure, is exponential in the sequent.
|
|
385
|
+
return Verdict(UNKNOWN, self.name, logic="ill", reason="timeout",
|
|
386
|
+
wall_time=elapsed,
|
|
387
|
+
detail=f"no verdict within the {timeout} ms limit")
|
|
388
|
+
if proved:
|
|
389
|
+
return Verdict(PROVED, self.name, logic="ill", wall_time=elapsed)
|
|
390
|
+
if bang or bounded:
|
|
391
|
+
return Verdict(UNKNOWN, self.name, logic="ill", reason="bound_hit",
|
|
392
|
+
wall_time=elapsed,
|
|
393
|
+
detail="no ILL derivation found within max_depth/"
|
|
394
|
+
"max_steps — the !-bearing fragment's search "
|
|
395
|
+
"is bounded, not complete, and/or max_depth "
|
|
396
|
+
"was set below the sequent's own safe bound, "
|
|
397
|
+
"so this is honestly unknown, never a refutation")
|
|
398
|
+
return Verdict(REFUTED, self.name, logic="ill", wall_time=elapsed,
|
|
399
|
+
countermodel=None,
|
|
400
|
+
detail="the !-free fragment's search is a complete "
|
|
401
|
+
"decision procedure; no cut-free ILL derivation "
|
|
402
|
+
"exists (a syntactic refutation — ILL has no "
|
|
403
|
+
"semantic model theory in this kit)")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
# Relevant logic B
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
class RelevantBackend(ProverBackend):
|
|
411
|
+
"""The relevant logic B via the bounded Routley-Meyer search in
|
|
412
|
+
:mod:`semantics.relevant`.
|
|
413
|
+
|
|
414
|
+
NEVER emits PROVED. :func:`~unicode_logic_kit.semantics.relevant.rel_valid`
|
|
415
|
+
(equivalently, "no countermodel from
|
|
416
|
+
:func:`~unicode_logic_kit.semantics.relevant.rel_countermodel`") is sound
|
|
417
|
+
but INCOMPLETE — an exhaustive search only up to ``max_worlds`` worlds —
|
|
418
|
+
so a clean search is reported UNKNOWN(bound_hit), never treated as a
|
|
419
|
+
proof of validity, exactly as ``rel_valid``'s own docstring insists. A
|
|
420
|
+
REFUTED verdict is always backed by a genuine countermodel: the search
|
|
421
|
+
itself re-verifies it with
|
|
422
|
+
:func:`~unicode_logic_kit.semantics.relevant.rel_satisfies` before
|
|
423
|
+
returning (see ``rel_countermodel``'s docstring), and this backend
|
|
424
|
+
serializes that exact model into ``countermodel`` (see
|
|
425
|
+
:func:`_relevant_model_to_dict`).
|
|
426
|
+
|
|
427
|
+
A genuine PROVED route for B needs a real Isabelle proof over EVERY
|
|
428
|
+
Routley-Meyer interpretation (not just small ones) — see
|
|
429
|
+
:mod:`unicode_logic_kit.hol.isabelle_relevant`'s ``to_isabelle_relevant`` /
|
|
430
|
+
``battery_proof`` — and is an explicit out-of-scope follow-up, not part
|
|
431
|
+
of this backend.
|
|
432
|
+
|
|
433
|
+
The call's ``timeout`` bounds the search (:func:`~unicode_logic_kit._deadline.run_until`);
|
|
434
|
+
a search it cut off is UNKNOWN(timeout), not bound_hit.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
name = "relevant"
|
|
438
|
+
logics = frozenset({"relevant"})
|
|
439
|
+
external = False
|
|
440
|
+
|
|
441
|
+
def available(self) -> bool:
|
|
442
|
+
return True
|
|
443
|
+
|
|
444
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
445
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
446
|
+
from ..semantics.relevant import rel_countermodel
|
|
447
|
+
|
|
448
|
+
max_worlds = options.pop("max_worlds", 2)
|
|
449
|
+
goal = _implication(formula, premises)
|
|
450
|
+
try:
|
|
451
|
+
(finished, result), elapsed = _timed(
|
|
452
|
+
lambda: _run_until(_instant(timeout),
|
|
453
|
+
lambda: rel_countermodel(goal, max_worlds=max_worlds)))
|
|
454
|
+
except TypeError as exc:
|
|
455
|
+
return Verdict(UNKNOWN, self.name, logic="relevant",
|
|
456
|
+
reason="unsupported", detail=str(exc))
|
|
457
|
+
if not finished:
|
|
458
|
+
return Verdict(UNKNOWN, self.name, logic="relevant", reason="timeout",
|
|
459
|
+
wall_time=elapsed,
|
|
460
|
+
detail=f"no B-countermodel found within the {timeout} ms limit")
|
|
461
|
+
if result is None:
|
|
462
|
+
return Verdict(UNKNOWN, self.name, logic="relevant", reason="bound_hit",
|
|
463
|
+
wall_time=elapsed,
|
|
464
|
+
detail=f"no B-countermodel with <= {max_worlds} worlds "
|
|
465
|
+
"found — rel_countermodel's search is sound but "
|
|
466
|
+
"incomplete, so this is NEVER treated as a proof "
|
|
467
|
+
"(see rel_valid's own honesty contract)")
|
|
468
|
+
model, world = result
|
|
469
|
+
return Verdict(REFUTED, self.name, logic="relevant", wall_time=elapsed,
|
|
470
|
+
countermodel=_relevant_model_to_dict(model, world))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# ---------------------------------------------------------------------------
|
|
474
|
+
# Hybrid modal logic H(@)
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
class HybridBackend(ProverBackend):
|
|
478
|
+
"""Hybrid modal logic H(@) and H(@,↓) via the standard translation + a real Z3 ``Solver``.
|
|
479
|
+
|
|
480
|
+
The ↓ binder needs nothing extra here: the standard translation maps
|
|
481
|
+
``↓x.φ`` to ``φ``'s translation with ``x`` bound to the current world
|
|
482
|
+
variable (the bounded fragment of FOL), which is faithful in both
|
|
483
|
+
directions, so ``unsat`` is a proof and ``sat`` a genuine countermodel of
|
|
484
|
+
the translation, hence of the formula. H(@,↓) is undecidable, so Z3 may
|
|
485
|
+
time out; that stays UNKNOWN. (The narrower
|
|
486
|
+
:func:`~unicode_logic_kit.atp.hybrid_down.down_is_valid` keeps its
|
|
487
|
+
PROVED-only contract.)
|
|
488
|
+
|
|
489
|
+
Deliberately does NOT call
|
|
490
|
+
:func:`~unicode_logic_kit.fol.modal_translation.hybrid_is_valid` (nor the
|
|
491
|
+
:func:`~unicode_logic_kit.atp.z3_models.is_valid` it delegates to): both
|
|
492
|
+
return a bare ``bool``, so a genuine countermodel (Z3 SAT) and Z3 giving
|
|
493
|
+
up (UNKNOWN/timeout) are both reported as the same ``False`` — exactly
|
|
494
|
+
the ambiguity this uniform layer exists to remove. This backend instead
|
|
495
|
+
builds the SAME ST-closed goal ``hybrid_is_valid`` builds —
|
|
496
|
+
:func:`~unicode_logic_kit.fol.modal_translation.standard_translation`
|
|
497
|
+
closed under :func:`~unicode_logic_kit.fol.modal_translation.frame_axioms`
|
|
498
|
+
(the frame axioms of every relation the goal mentions, and the membership of
|
|
499
|
+
each sorted constant ``c:S`` in ``S`` at every world) — and decides it with the kit's own tracked per-call
|
|
500
|
+
:func:`~unicode_logic_kit.atp.protocol._z3_track_and_check` (the identical
|
|
501
|
+
routine :class:`~unicode_logic_kit.atp.protocol.Z3Backend` uses for plain
|
|
502
|
+
FOL), so Z3's own ``unsat``/``sat``/``unknown`` map to real
|
|
503
|
+
PROVED / REFUTED(countermodel) / UNKNOWN(timeout|incomplete).
|
|
504
|
+
|
|
505
|
+
``premises`` fold through the shared
|
|
506
|
+
:func:`~unicode_logic_kit.atp.protocol._implication` And/Implies helper at
|
|
507
|
+
the MODAL level first — exactly like
|
|
508
|
+
:class:`~unicode_logic_kit.atp.protocol.ModalTableauBackend` — before
|
|
509
|
+
translation: hybrid logic keeps full classical structure over ``∧``/
|
|
510
|
+
``→``, so this fold is sound here (unlike the substructural Lambek/ILL
|
|
511
|
+
backends, where it would not be). ``frame`` (default ``"K"``) selects the
|
|
512
|
+
alethic frame class, exactly as it does for ``hybrid_is_valid``; so do the two other
|
|
513
|
+
keywords of that function: ``systems`` (the frame system of the agent-indexed families,
|
|
514
|
+
e.g. ``{"epistemic": "S5"}``, which makes ``Knows(a, P) → P`` valid) and ``temporal_closure``
|
|
515
|
+
(default ``True``: the temporal relation is reflexive and transitive; ``False`` drops
|
|
516
|
+
both, so ``Ⓖφ → φ`` is no longer valid). All three reach
|
|
517
|
+
:func:`~unicode_logic_kit.fol.modal_translation.frame_axioms` and nothing else, and an
|
|
518
|
+
unknown frame system or modal family is UNKNOWN(unsupported), never an exception.
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
name = "hybrid"
|
|
522
|
+
logics = frozenset({"hybrid"})
|
|
523
|
+
external = False
|
|
524
|
+
|
|
525
|
+
def available(self) -> bool:
|
|
526
|
+
return True
|
|
527
|
+
|
|
528
|
+
def decide(self, formula: Node, premises: Sequence[Node] = (),
|
|
529
|
+
timeout: int = 10000, **options) -> Verdict:
|
|
530
|
+
from ..fol.modal_translation import standard_translation, frame_axioms
|
|
531
|
+
from ..fol.nodes import Quantifier, Variable, And, Implies
|
|
532
|
+
|
|
533
|
+
frame = options.pop("frame", "K")
|
|
534
|
+
systems = options.pop("systems", None)
|
|
535
|
+
temporal_closure = options.pop("temporal_closure", True)
|
|
536
|
+
goal_modal = _implication(formula, list(premises))
|
|
537
|
+
try:
|
|
538
|
+
translated = standard_translation(goal_modal, world="w")
|
|
539
|
+
closed = Quantifier("∀", Variable("w"), translated)
|
|
540
|
+
hyp = None
|
|
541
|
+
# The axioms of every relation the goal's translation emits (so ``Ⓖφ → φ``
|
|
542
|
+
# and ``Ⓞφ → Ⓟφ`` are decided as ``hybrid_is_valid`` decides them) and the
|
|
543
|
+
# membership of every sorted constant in its sort at every world.
|
|
544
|
+
for axiom in frame_axioms(goal_modal, frame, systems=systems,
|
|
545
|
+
temporal_closure=temporal_closure):
|
|
546
|
+
hyp = axiom if hyp is None else And(hyp, axiom)
|
|
547
|
+
st_goal = closed if hyp is None else Implies(hyp, closed)
|
|
548
|
+
z3_formula = st_goal.to_z3()
|
|
549
|
+
except (NotImplementedError, ValueError) as exc:
|
|
550
|
+
return Verdict(UNKNOWN, self.name, logic="hybrid",
|
|
551
|
+
reason="unsupported", detail=str(exc))
|
|
552
|
+
|
|
553
|
+
from z3 import sat, unsat
|
|
554
|
+
|
|
555
|
+
(res, solver), elapsed = _timed(
|
|
556
|
+
lambda: _z3_track_and_check(z3_formula, [], timeout))
|
|
557
|
+
if res == unsat:
|
|
558
|
+
return Verdict(PROVED, self.name, logic="hybrid", wall_time=elapsed)
|
|
559
|
+
if res == sat:
|
|
560
|
+
assignment = _z3_model_assignment(solver.model(), 0)
|
|
561
|
+
return Verdict(REFUTED, self.name, logic="hybrid", wall_time=elapsed,
|
|
562
|
+
countermodel={"kind": "z3_model", "assignment": assignment})
|
|
563
|
+
why = solver.reason_unknown()
|
|
564
|
+
reason = "timeout" if ("timeout" in why or "cancel" in why) else "incomplete"
|
|
565
|
+
return Verdict(UNKNOWN, self.name, logic="hybrid", reason=reason,
|
|
566
|
+
wall_time=elapsed, detail=why)
|