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,921 @@
|
|
|
1
|
+
"""Analytic (semantic) tableaux — a refutation method that also yields countermodels.
|
|
2
|
+
|
|
3
|
+
A tableau decomposes a set of formulas with the standard signed-free rules: the
|
|
4
|
+
*α* (non-branching) rules break a conjunctive formula into its parts, the *β*
|
|
5
|
+
(branching) rules split a disjunctive one, the *δ* rule witnesses an existential with
|
|
6
|
+
a fresh constant, and the *γ* rule instantiates a universal at the available terms. A
|
|
7
|
+
branch **closes** when it contains both ``φ`` and ``¬φ`` (or ``⊥``); the set is jointly
|
|
8
|
+
**unsatisfiable** iff *every* branch closes. An *open* saturated branch is, by
|
|
9
|
+
contrast, a model — so a failed refutation hands back a countermodel for free.
|
|
10
|
+
|
|
11
|
+
The truth constants ``$true`` and ``$false`` (``⊤`` and ``⊥`` in the unicode syntax)
|
|
12
|
+
are constants, not letters: a branch that holds ``$false`` or ``¬$true`` closes on
|
|
13
|
+
its own, and ``$true`` and ``¬$false`` hold in every interpretation, so they are
|
|
14
|
+
dropped (they neither close a branch nor appear in the model an open branch gives).
|
|
15
|
+
|
|
16
|
+
This gives a fourth proof method alongside resolution, Fitch, and the sequent
|
|
17
|
+
calculus: ``is_valid_tableau(φ)`` builds a tableau for ``¬φ`` (valid iff it closes),
|
|
18
|
+
and ``tableau_model`` returns the open branch's literals as a satisfying assignment.
|
|
19
|
+
|
|
20
|
+
Propositional tableaux are decidable and complete; the first-order rules are run under
|
|
21
|
+
a step bound (``γ``-instantiation is only semi-decidable), so — like the resolution
|
|
22
|
+
prover — a non-closing first-order tableau within the bound is reported as "open"
|
|
23
|
+
without claiming satisfiability. The bounds are the step budget (``max_steps`` rule
|
|
24
|
+
applications and closure tests in all, so no branch can be longer than that), the
|
|
25
|
+
per-branch term pool (``max_terms``) and an optional wall-clock ``timeout``. The search
|
|
26
|
+
is a loop over an explicit stack of pending branches, so the LENGTH of a branch does not
|
|
27
|
+
depend on the interpreter's recursion limit: a branch of any length up to ``max_steps`` is
|
|
28
|
+
searched. The NESTING of a formula does: the helpers that walk a formula (substitution of a
|
|
29
|
+
witness, the collection of its terms, hashing) recurse once per level, so a formula nested
|
|
30
|
+
deeper than they can walk within the interpreter's recursion limit (hundreds of levels)
|
|
31
|
+
ends the search like a bound: "no closed tableau", never a :class:`RecursionError` and never
|
|
32
|
+
a claim of satisfiability (:func:`nesting_depth` measures it).
|
|
33
|
+
|
|
34
|
+
Many-sorted input is searched through its guard image: ``∀x:S φ`` is ``∀x (S(x) → φ)``,
|
|
35
|
+
and the facts that reading needs (no sort is empty, a sorted constant ``c:S`` lies in
|
|
36
|
+
``S``) join the formulas as further members of the refuted set — see :func:`tableau_closed`.
|
|
37
|
+
|
|
38
|
+
Public API: :func:`tableau_closed`, :func:`is_valid_tableau`, :func:`prove_tableau`,
|
|
39
|
+
:func:`tableau_model`, and — for a recorded, independently-checkable proof object —
|
|
40
|
+
:func:`prove_tableau_detailed` / :class:`TableauProof` (checked by
|
|
41
|
+
:mod:`unicode_logic_kit.atp.tableau_check`).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import time
|
|
45
|
+
from dataclasses import dataclass
|
|
46
|
+
from typing import Dict, List, Optional, Tuple
|
|
47
|
+
|
|
48
|
+
from ..fol._atom_keys import AtomKeys
|
|
49
|
+
from ..fol._identifiers import symbol_names
|
|
50
|
+
from ..fol._msfl_nodes import key_text
|
|
51
|
+
from ..fol.nodes import (
|
|
52
|
+
Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Variable, Constant, Number, Function,
|
|
53
|
+
Contrast, Count, Cardinality,
|
|
54
|
+
SortedQuantifier, SortedConstant, SortedCount, SortedCardinality, to_fol, sort_axioms,
|
|
55
|
+
)
|
|
56
|
+
from ..fol._truth_constants import is_true_constant, is_false_constant
|
|
57
|
+
from .fitch import FALSUM, is_falsum, _subst_var, _q_kind, _free_vars
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _neg(f: Node) -> Node:
|
|
61
|
+
"""Return the complementary formula of ``f`` (``¬φ`` ↔ ``φ``)."""
|
|
62
|
+
return f.formula if isinstance(f, Not) else Not(f)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _any_modal(formulas) -> bool:
|
|
66
|
+
"""True iff any formula carries a modal/temporal/epistemic/deontic operator.
|
|
67
|
+
|
|
68
|
+
Such formulas have no classical tableau rule; they are routed to the labelled
|
|
69
|
+
modal tableau (:mod:`unicode_logic_kit.atp.modal_tableau`) instead of raising.
|
|
70
|
+
"""
|
|
71
|
+
from .modal_tableau import has_modal
|
|
72
|
+
return any(has_modal(f) for f in formulas)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _reject_exotic(formulas, entry: str) -> None:
|
|
76
|
+
"""Reject non-classical node families with a pointer at the right tool.
|
|
77
|
+
|
|
78
|
+
Without this pre-dispatch check these nodes surfaced as a bare
|
|
79
|
+
``ValueError: tableau: no rule for X`` from deep inside the rule dispatcher —
|
|
80
|
+
the same class of gap the modal routing above already closes. Each family has
|
|
81
|
+
its own decision procedure; none has a sound classical tableau rule.
|
|
82
|
+
"""
|
|
83
|
+
from ..fol._linear_nodes import Tensor, With, OPlus, LinearImplies, OfCourse, One
|
|
84
|
+
from ..fol._lambek_nodes import Product, Under, Over
|
|
85
|
+
from ..fol._team_nodes import Dependence, SlashedExists
|
|
86
|
+
from ..fol._so_nodes import SecondOrderQuantifier
|
|
87
|
+
from ..semantics._modal_reject import FUZZY_TYPES
|
|
88
|
+
hints = (
|
|
89
|
+
((Tensor, With, OPlus, LinearImplies, OfCourse, One),
|
|
90
|
+
"a linear-logic (ILL) connective; decide derivability with "
|
|
91
|
+
"atp.linear.ill_prove / ill_derivable"),
|
|
92
|
+
((Product, Under, Over),
|
|
93
|
+
"a Lambek-calculus connective; decide derivability with "
|
|
94
|
+
"atp.lambek.lambek_prove / lambek_derivable"),
|
|
95
|
+
((Dependence, SlashedExists),
|
|
96
|
+
"team-semantic (dependence/IF logic); evaluate with team_satisfies / "
|
|
97
|
+
"team_models"),
|
|
98
|
+
((SecondOrderQuantifier,),
|
|
99
|
+
"second-order; use the sequent calculus's SO rules, satisfies_so, or "
|
|
100
|
+
"hol.secondorder"),
|
|
101
|
+
(FUZZY_TYPES,
|
|
102
|
+
"a Łukasiewicz connective; evaluate with semantics.fuzzy.evaluate or "
|
|
103
|
+
"decide with atp.z3_fuzzy.fuzzy_is_valid"),
|
|
104
|
+
((SortedCardinality,),
|
|
105
|
+
"a many-sorted set-cardinality term, which is second-order; evaluate it "
|
|
106
|
+
"with semantics.tarski.satisfies / the finite model finder, or export to "
|
|
107
|
+
"HOL via hol.secondorder"),
|
|
108
|
+
)
|
|
109
|
+
for f in formulas:
|
|
110
|
+
for sub in f.walk():
|
|
111
|
+
for types, hint in hints:
|
|
112
|
+
if isinstance(sub, types):
|
|
113
|
+
raise NotImplementedError(
|
|
114
|
+
f"{entry}: {type(sub).__name__} is {hint}. No sound "
|
|
115
|
+
"classical proof rule exists for it here.")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _ground_terms(node: Node, acc: set) -> None:
|
|
119
|
+
"""Collect the closed (variable-free) constant/number/function terms in ``node``."""
|
|
120
|
+
if isinstance(node, (Constant, Number)):
|
|
121
|
+
acc.add(node)
|
|
122
|
+
elif isinstance(node, Function):
|
|
123
|
+
if not _free_vars(node):
|
|
124
|
+
acc.add(node)
|
|
125
|
+
for a in node.args:
|
|
126
|
+
_ground_terms(a, acc)
|
|
127
|
+
elif isinstance(node, Atom):
|
|
128
|
+
for a in node.args:
|
|
129
|
+
_ground_terms(a, acc)
|
|
130
|
+
else:
|
|
131
|
+
for child in node._child_nodes():
|
|
132
|
+
_ground_terms(child, acc)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _terms_of(formula: Node, existing: Tuple[Node, ...], cap: int) -> Tuple[Node, ...]:
|
|
136
|
+
"""Return ``existing`` extended with ``formula``'s ground terms, capped at ``cap``."""
|
|
137
|
+
if len(existing) >= cap:
|
|
138
|
+
return existing
|
|
139
|
+
acc: set = set()
|
|
140
|
+
_ground_terms(formula, acc)
|
|
141
|
+
result = list(existing)
|
|
142
|
+
# The order is by the bare names of the constants (key_text), so that the quotes of a
|
|
143
|
+
# quoted constant play no part in which term is tried first.
|
|
144
|
+
for t in sorted(acc, key=key_text):
|
|
145
|
+
if t not in result:
|
|
146
|
+
result.append(t)
|
|
147
|
+
if len(result) >= cap:
|
|
148
|
+
break
|
|
149
|
+
return tuple(result)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_literal(f: Node) -> bool:
|
|
153
|
+
"""True iff ``f`` is an atom, a negated atom, or ⊥ (no rule applies)."""
|
|
154
|
+
if is_falsum(f):
|
|
155
|
+
return True
|
|
156
|
+
if isinstance(f, Atom):
|
|
157
|
+
return True
|
|
158
|
+
if isinstance(f, Not) and isinstance(f.formula, Atom):
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _closes_alone(f: Node) -> bool:
|
|
164
|
+
"""True iff the literal ``f`` closes its branch with no partner: ⊥, the truth
|
|
165
|
+
constant ``$false`` (``is_falsum`` reads both), or ``¬$true``."""
|
|
166
|
+
return is_falsum(f) or (isinstance(f, Not) and is_true_constant(f.formula))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _holds_always(f: Node) -> bool:
|
|
170
|
+
"""True iff the literal ``f`` holds in every interpretation and so adds nothing
|
|
171
|
+
to a branch: the truth constant ``$true`` or ``¬$false``. It is dropped, never
|
|
172
|
+
recorded as a literal (so it can neither close a branch nor appear in a model)."""
|
|
173
|
+
return is_true_constant(f) or (isinstance(f, Not) and is_false_constant(f.formula))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class _Ctx:
|
|
177
|
+
"""Search context: a step budget, a wall-clock deadline, a fresh-constant source
|
|
178
|
+
and a term-pool cap.
|
|
179
|
+
|
|
180
|
+
``avoid`` holds every name the problem carries (:func:`symbol_names` of ALL the
|
|
181
|
+
formulas the search starts from): a generated constant never has one of them.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, max_steps: int, max_terms: int, timeout: Optional[int] = None,
|
|
185
|
+
avoid=frozenset()):
|
|
186
|
+
self.budget = [max_steps]
|
|
187
|
+
self.max_terms = max_terms
|
|
188
|
+
self._fresh = [0]
|
|
189
|
+
self._avoid = frozenset(avoid)
|
|
190
|
+
self.open_branch: Optional[frozenset] = None
|
|
191
|
+
# ``timeout`` is in milliseconds, counted from the moment the search starts.
|
|
192
|
+
self.deadline = None if timeout is None else time.perf_counter() + timeout / 1000.0
|
|
193
|
+
|
|
194
|
+
def spend(self) -> bool:
|
|
195
|
+
"""Charge one step; False once the step budget is gone or the deadline has passed.
|
|
196
|
+
|
|
197
|
+
The deadline is read at every step, so the search ends within one step's work
|
|
198
|
+
of it. After it has passed the budget is emptied, which makes every pending
|
|
199
|
+
branch return at once.
|
|
200
|
+
"""
|
|
201
|
+
if self.budget[0] <= 0:
|
|
202
|
+
return False
|
|
203
|
+
if self.deadline is not None and time.perf_counter() > self.deadline:
|
|
204
|
+
self.budget[0] = 0
|
|
205
|
+
return False
|
|
206
|
+
self.budget[0] -= 1
|
|
207
|
+
return True
|
|
208
|
+
|
|
209
|
+
def fresh_const(self) -> Constant:
|
|
210
|
+
"""The next generated constant: ``_t0``, ``_t1``, … skipping every name of the problem.
|
|
211
|
+
|
|
212
|
+
A user constant spelled ``_t0`` is not the witness of an existential: the witness
|
|
213
|
+
of ``∃x P(x)`` is an element nothing else is known about, so it must not be
|
|
214
|
+
a symbol that another formula already talks about.
|
|
215
|
+
"""
|
|
216
|
+
while True:
|
|
217
|
+
name = f"_t{self._fresh[0]}"
|
|
218
|
+
self._fresh[0] += 1
|
|
219
|
+
if name not in self._avoid:
|
|
220
|
+
return Constant(name)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _rule(f: Node):
|
|
224
|
+
"""Classify ``f`` and return its expansion.
|
|
225
|
+
|
|
226
|
+
Returns one of:
|
|
227
|
+
``("alpha", [comp, …])`` — add all components to the branch;
|
|
228
|
+
``("beta", [[…], […]])`` — split the branch (each list a new branch's adds);
|
|
229
|
+
``("delta", var, body, neg)`` — witness with a fresh constant;
|
|
230
|
+
``("gamma", var, body, neg)`` — universal, instantiate at terms.
|
|
231
|
+
"""
|
|
232
|
+
if isinstance(f, And):
|
|
233
|
+
return ("alpha", [f.left, f.right])
|
|
234
|
+
if isinstance(f, Contrast):
|
|
235
|
+
# Concession is truth-functionally conjunction (Contrast's own contract).
|
|
236
|
+
return ("alpha", [f.left, f.right])
|
|
237
|
+
if isinstance(f, Or):
|
|
238
|
+
return ("beta", [[f.left], [f.right]])
|
|
239
|
+
if isinstance(f, Implies):
|
|
240
|
+
return ("beta", [[Not(f.left)], [f.right]])
|
|
241
|
+
if isinstance(f, Iff):
|
|
242
|
+
return ("beta", [[f.left, f.right], [Not(f.left), Not(f.right)]])
|
|
243
|
+
if isinstance(f, Xor):
|
|
244
|
+
return ("beta", [[f.left, Not(f.right)], [Not(f.left), f.right]])
|
|
245
|
+
if isinstance(f, Count):
|
|
246
|
+
# The distinct-witnesses expansion is plain FOL, which this tableau's
|
|
247
|
+
# quantifier rules handle — the same lowering to_z3/to_prover9 use.
|
|
248
|
+
return ("alpha", [f._expand()])
|
|
249
|
+
if _q_kind(f) == "∃":
|
|
250
|
+
return ("delta", f.variable, f.formula, False)
|
|
251
|
+
if _q_kind(f) == "∀":
|
|
252
|
+
return ("gamma", f.variable, f.formula, False)
|
|
253
|
+
if isinstance(f, Not):
|
|
254
|
+
g = f.formula
|
|
255
|
+
if isinstance(g, Not):
|
|
256
|
+
return ("alpha", [g.formula])
|
|
257
|
+
if isinstance(g, And):
|
|
258
|
+
return ("beta", [[Not(g.left)], [Not(g.right)]])
|
|
259
|
+
if isinstance(g, Contrast):
|
|
260
|
+
return ("beta", [[Not(g.left)], [Not(g.right)]])
|
|
261
|
+
if isinstance(g, Or):
|
|
262
|
+
return ("alpha", [Not(g.left), Not(g.right)])
|
|
263
|
+
if isinstance(g, Implies):
|
|
264
|
+
return ("alpha", [g.left, Not(g.right)])
|
|
265
|
+
if isinstance(g, Iff):
|
|
266
|
+
return ("beta", [[g.left, Not(g.right)], [Not(g.left), g.right]])
|
|
267
|
+
if isinstance(g, Xor):
|
|
268
|
+
return ("beta", [[g.left, g.right], [Not(g.left), Not(g.right)]])
|
|
269
|
+
if isinstance(g, Count):
|
|
270
|
+
return ("alpha", [Not(g._expand())])
|
|
271
|
+
if _q_kind(g) == "∀":
|
|
272
|
+
return ("delta", g.variable, g.formula, True)
|
|
273
|
+
if _q_kind(g) == "∃":
|
|
274
|
+
return ("gamma", g.variable, g.formula, True)
|
|
275
|
+
if isinstance(f, (Cardinality,)) or (
|
|
276
|
+
isinstance(f, Not) and isinstance(f.formula, Cardinality)):
|
|
277
|
+
raise NotImplementedError(
|
|
278
|
+
"tableau: a bare Cardinality term is not a formula, and cardinality "
|
|
279
|
+
"comparisons are not first-order — evaluate them with "
|
|
280
|
+
"semantics.tarski.satisfies / the finite model finder, or export to "
|
|
281
|
+
"HOL via hol.secondorder.")
|
|
282
|
+
raise ValueError(f"tableau: no rule for {type(f).__name__} {f.to_unicode_str()}")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _instance(var: Variable, body: Node, neg: bool, term: Node) -> Node:
|
|
286
|
+
"""Return ``body[var:=term]``, negated when the source was ¬∃ / ∀ on the right."""
|
|
287
|
+
inst = _subst_var(body, var, term)
|
|
288
|
+
return Not(inst) if neg else inst
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _close(work: Tuple[Node, ...], lits: frozenset,
|
|
292
|
+
gammas: Tuple[Tuple, ...], terms: Tuple[Node, ...],
|
|
293
|
+
used: frozenset, ctx: "_Ctx") -> bool:
|
|
294
|
+
"""Return True iff this branch (and all its splits) close.
|
|
295
|
+
|
|
296
|
+
The search is depth-first and runs in a loop: one pass of it is one rule application
|
|
297
|
+
(or one closure test) and costs one step of ``ctx``. A branching rule puts its second
|
|
298
|
+
alternative on a stack of pending branches and goes on with the first; a branch that
|
|
299
|
+
closes takes the next pending one; the first branch that cannot close (saturated, the
|
|
300
|
+
term pool full, or the steps or the deadline used up) ends the whole search with False.
|
|
301
|
+
Nothing recurses, so how deep a branch may grow is bounded by the step budget alone,
|
|
302
|
+
never by the interpreter's recursion limit.
|
|
303
|
+
"""
|
|
304
|
+
pending: List[Tuple] = [] # the alternatives still to close, the newest last
|
|
305
|
+
while True:
|
|
306
|
+
if not ctx.spend():
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
closed = False
|
|
310
|
+
if work:
|
|
311
|
+
f, rest = work[0], work[1:]
|
|
312
|
+
|
|
313
|
+
if _is_literal(f):
|
|
314
|
+
if _closes_alone(f) or _neg(f) in lits:
|
|
315
|
+
closed = True
|
|
316
|
+
else:
|
|
317
|
+
if not (_holds_always(f) or f in lits):
|
|
318
|
+
# A new literal may introduce ground terms a universal can instantiate at.
|
|
319
|
+
lits = lits | {f}
|
|
320
|
+
terms = _terms_of(f, terms, ctx.max_terms)
|
|
321
|
+
work = rest
|
|
322
|
+
continue
|
|
323
|
+
else:
|
|
324
|
+
rule = _rule(f)
|
|
325
|
+
kind = rule[0]
|
|
326
|
+
if kind == "alpha":
|
|
327
|
+
work = tuple(rule[1]) + rest
|
|
328
|
+
continue
|
|
329
|
+
if kind == "beta":
|
|
330
|
+
left, right = rule[1]
|
|
331
|
+
pending.append((tuple(right) + rest, lits, gammas, terms, used))
|
|
332
|
+
work = tuple(left) + rest
|
|
333
|
+
continue
|
|
334
|
+
if kind == "delta":
|
|
335
|
+
_, var, body, neg = rule
|
|
336
|
+
if len(terms) >= ctx.max_terms:
|
|
337
|
+
# Term-pool cap reached: give up on this branch (sound but incomplete).
|
|
338
|
+
if ctx.open_branch is None:
|
|
339
|
+
ctx.open_branch = lits
|
|
340
|
+
return False
|
|
341
|
+
c = ctx.fresh_const()
|
|
342
|
+
work = (_instance(var, body, neg, c),) + rest
|
|
343
|
+
terms = terms + (c,)
|
|
344
|
+
continue
|
|
345
|
+
if kind == "gamma":
|
|
346
|
+
_, var, body, neg = rule
|
|
347
|
+
key = f
|
|
348
|
+
gammas = gammas + ((key, var, body, neg),)
|
|
349
|
+
pool = terms if terms else (ctx.fresh_const(),)
|
|
350
|
+
insts = tuple(_instance(var, body, neg, t) for t in pool)
|
|
351
|
+
used = used | {(key, t) for t in pool}
|
|
352
|
+
terms = terms if terms else pool
|
|
353
|
+
work = insts + rest
|
|
354
|
+
continue
|
|
355
|
+
raise AssertionError(kind)
|
|
356
|
+
else:
|
|
357
|
+
# No compound work left: re-instantiate a universal at a term it has not used.
|
|
358
|
+
instantiated = False
|
|
359
|
+
for key, var, body, neg in gammas:
|
|
360
|
+
for t in terms:
|
|
361
|
+
if (key, t) not in used:
|
|
362
|
+
work = (_instance(var, body, neg, t),)
|
|
363
|
+
used = used | {(key, t)}
|
|
364
|
+
instantiated = True
|
|
365
|
+
break
|
|
366
|
+
if instantiated:
|
|
367
|
+
break
|
|
368
|
+
if instantiated:
|
|
369
|
+
continue
|
|
370
|
+
# Saturated and not closed: an OPEN branch — record it as a (counter)model.
|
|
371
|
+
if ctx.open_branch is None:
|
|
372
|
+
ctx.open_branch = lits
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
# This branch closed: go on with the next alternative, if there is one.
|
|
376
|
+
if closed:
|
|
377
|
+
if not pending:
|
|
378
|
+
return True
|
|
379
|
+
work, lits, gammas, terms, used = pending.pop()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _initial_terms(formulas, cap: int) -> Tuple[Node, ...]:
|
|
383
|
+
"""The γ-instantiation seed: initial ground terms plus the input's free variables.
|
|
384
|
+
|
|
385
|
+
A FREE variable of the input is a PARAMETER of the problem: one unknown element, the
|
|
386
|
+
same in every formula (the assignment-wise consequence relation, ``Γ ⊨ φ`` iff every
|
|
387
|
+
structure AND assignment that satisfies ``Γ`` satisfies ``φ``), so it is treated as a
|
|
388
|
+
constant (``valid ∀a.φ ⟺ unsat ¬φ[a := fresh constant]``). That is how the
|
|
389
|
+
resolution prover, the finite model finder, Z3 and cvc5 read it too: ``P(x) ⊢ P(alpha)``
|
|
390
|
+
is not valid, ``P(x) ⊢ ∃y P(y)`` is. Without this a
|
|
391
|
+
γ-formula was never instantiated at a free variable and e.g.
|
|
392
|
+
``¬∃x P(x) → ¬P(a)`` (free ``a``) was silently left unproved.
|
|
393
|
+
Bound occurrences never reach this seed: it runs on the top-level input only,
|
|
394
|
+
and the free-variable set excludes them by definition.
|
|
395
|
+
"""
|
|
396
|
+
terms: Tuple[Node, ...] = ()
|
|
397
|
+
for f in formulas:
|
|
398
|
+
terms = _terms_of(f, terms, cap)
|
|
399
|
+
free: set = set()
|
|
400
|
+
for f in formulas:
|
|
401
|
+
free |= _free_vars(f) # a set of Variable NODES
|
|
402
|
+
for v in sorted(free, key=lambda n: n.name):
|
|
403
|
+
if len(terms) >= cap:
|
|
404
|
+
break
|
|
405
|
+
if v not in terms:
|
|
406
|
+
terms = terms + (v,)
|
|
407
|
+
return terms
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
_SORTED_NODES = (SortedQuantifier, SortedConstant, SortedCount, SortedCardinality)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _lower_sorted(formulas) -> List[Node]:
|
|
414
|
+
"""The formulas a tableau works on: ``formulas`` themselves, or their guard images.
|
|
415
|
+
|
|
416
|
+
A many-sorted formula has no tableau rule of its own. It is read as the one-universe
|
|
417
|
+
reading of the kit says (:func:`~unicode_logic_kit.fol.nodes.to_fol`): ``∀x:S φ`` is
|
|
418
|
+
``∀x (S(x) → φ)``, ``∃x:S φ`` is ``∃x (S(x) ∧ φ)`` and ``c:S`` is the constant ``c``.
|
|
419
|
+
That image forgets two facts the reading needs: no sort is empty and a sorted
|
|
420
|
+
constant lies in its sort. They are :func:`~unicode_logic_kit.fol.nodes.sort_axioms` of
|
|
421
|
+
ALL the formulas, appended to the image as further members of the set the tableau
|
|
422
|
+
refutes. The set is unsatisfiable under the definition exactly when the tableau of
|
|
423
|
+
these roots closes; the axioms are never part of a negated conclusion, because the
|
|
424
|
+
conclusion's negation is one member of the set and they are others.
|
|
425
|
+
|
|
426
|
+
Formulas without a sorted node are returned as they are, so an unsorted problem is
|
|
427
|
+
searched exactly as it always was.
|
|
428
|
+
"""
|
|
429
|
+
formulas = list(formulas)
|
|
430
|
+
if not any(isinstance(node, _SORTED_NODES) for f in formulas for node in f.walk()):
|
|
431
|
+
return formulas
|
|
432
|
+
return [to_fol(f) for f in formulas] + list(sort_axioms(*formulas))
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def nesting_depth(*formulas: Node) -> int:
|
|
436
|
+
"""The deepest nesting of the nodes of ``formulas``: the most nodes on one path from a root down.
|
|
437
|
+
|
|
438
|
+
A proposition ``A`` is 1, ``¬A`` is 2, and ``P(x)`` is 2 (the atom and its argument).
|
|
439
|
+
Computed with a stack of its own, so it answers for a formula of any depth. A
|
|
440
|
+
formula nested deeper than the interpreter's recursion limit lets the tableau's
|
|
441
|
+
recursive helpers (the substitution of a term for a bound variable, the
|
|
442
|
+
collection of ground terms, the hashing of a formula) raise
|
|
443
|
+
:class:`RecursionError`; the search then ends as "no closed tableau" and this is
|
|
444
|
+
the number that names the reason.
|
|
445
|
+
"""
|
|
446
|
+
deepest = 0
|
|
447
|
+
pending = [(formula, 1) for formula in formulas]
|
|
448
|
+
while pending:
|
|
449
|
+
node, depth = pending.pop()
|
|
450
|
+
if depth > deepest:
|
|
451
|
+
deepest = depth
|
|
452
|
+
pending.extend((child, depth + 1) for child in node._child_nodes())
|
|
453
|
+
return deepest
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def tableau_closed(formulas, max_steps: int = 20000, max_terms: int = 8,
|
|
457
|
+
timeout: Optional[int] = None) -> bool:
|
|
458
|
+
"""Return True iff ``formulas`` are jointly unsatisfiable (every branch closes).
|
|
459
|
+
|
|
460
|
+
Sound; complete and decidable for the propositional fragment. First-order
|
|
461
|
+
``γ``-instantiation is bounded by ``max_terms`` (the size of the per-branch term
|
|
462
|
+
pool) and ``max_steps``, so a False on a first-order input is "no closed tableau
|
|
463
|
+
within the bounds", never a claim of satisfiability. ``max_steps`` also bounds how
|
|
464
|
+
long a branch can grow (every rule application on it is a step); the interpreter's
|
|
465
|
+
recursion limit does not, because the search does not recurse over a branch. A
|
|
466
|
+
formula nested deeper than the helpers that walk it can follow within that limit
|
|
467
|
+
(see :func:`nesting_depth`) is a bound too: the call returns False, it never raises
|
|
468
|
+
:class:`RecursionError`. ``timeout``
|
|
469
|
+
(milliseconds, default none) is one more bound, checked at every step: the search
|
|
470
|
+
returns False within one step's work of it.
|
|
471
|
+
|
|
472
|
+
Many-sorted formulas are read with the guard reading of
|
|
473
|
+
:func:`~unicode_logic_kit.fol.nodes.to_fol` and gain the background facts that reading
|
|
474
|
+
needs, as further members of the set: every sort is non-empty and a sorted constant
|
|
475
|
+
``c:S`` lies in ``S`` (:func:`~unicode_logic_kit.fol.nodes.sort_axioms`). So
|
|
476
|
+
``∀x:Human Mortal(x), ¬Mortal(socrates:Human)`` is unsatisfiable and
|
|
477
|
+
``∀x:Human Mortal(x), ¬Mortal(socrates)`` is not.
|
|
478
|
+
|
|
479
|
+
Modal/temporal/epistemic/deontic formulas have no classical rule; they are routed
|
|
480
|
+
to the labelled modal tableau (over the system **K** by default — for other frames
|
|
481
|
+
call :mod:`unicode_logic_kit.atp.modal_tableau` directly).
|
|
482
|
+
"""
|
|
483
|
+
formulas = list(formulas)
|
|
484
|
+
try:
|
|
485
|
+
_reject_exotic(formulas, "tableau_closed")
|
|
486
|
+
if _any_modal(formulas):
|
|
487
|
+
from .modal_tableau import modal_tableau_closed
|
|
488
|
+
return modal_tableau_closed(formulas, timeout=timeout)
|
|
489
|
+
formulas = _lower_sorted(formulas)
|
|
490
|
+
ctx = _Ctx(max_steps, max_terms, timeout, symbol_names(*formulas))
|
|
491
|
+
return _close(tuple(formulas), frozenset(), (),
|
|
492
|
+
_initial_terms(formulas, max_terms), frozenset(), ctx)
|
|
493
|
+
except RecursionError:
|
|
494
|
+
return False
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def is_valid_tableau(formula: Node, max_steps: int = 20000, max_terms: int = 8,
|
|
498
|
+
timeout: Optional[int] = None) -> bool:
|
|
499
|
+
"""Return True iff ``formula`` is valid — its negation's tableau closes.
|
|
500
|
+
|
|
501
|
+
A modal formula is decided over the system **K** by the labelled modal tableau;
|
|
502
|
+
use :func:`unicode_logic_kit.atp.modal_tableau.is_modal_valid` for other frames.
|
|
503
|
+
``timeout`` is as for :func:`tableau_closed`.
|
|
504
|
+
"""
|
|
505
|
+
try:
|
|
506
|
+
_reject_exotic([formula], "is_valid_tableau")
|
|
507
|
+
if _any_modal([formula]):
|
|
508
|
+
from .modal_tableau import is_modal_valid
|
|
509
|
+
return is_modal_valid(formula, timeout=timeout)
|
|
510
|
+
except RecursionError:
|
|
511
|
+
return False
|
|
512
|
+
return tableau_closed([Not(formula)], max_steps, max_terms, timeout)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def prove_tableau(premises, conclusion: Node, max_steps: int = 20000, max_terms: int = 8,
|
|
516
|
+
timeout: Optional[int] = None) -> bool:
|
|
517
|
+
"""Return True iff ``premises`` entail ``conclusion`` (premises + ¬conclusion close).
|
|
518
|
+
|
|
519
|
+
For modal inputs this is **local** consequence over the system **K** (see
|
|
520
|
+
:func:`unicode_logic_kit.atp.modal_tableau.modal_prove` for other frames).
|
|
521
|
+
Many-sorted input is read as in :func:`tableau_closed`; ``timeout`` too.
|
|
522
|
+
"""
|
|
523
|
+
return tableau_closed(list(premises) + [Not(conclusion)], max_steps, max_terms, timeout)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def tableau_model(formulas, max_steps: int = 20000, max_terms: int = 8,
|
|
527
|
+
timeout: Optional[int] = None) -> Optional[dict]:
|
|
528
|
+
"""Return a satisfying literal assignment if ``formulas`` are satisfiable, else None.
|
|
529
|
+
|
|
530
|
+
On an open (saturated) branch the literals are returned as a dict mapping each
|
|
531
|
+
atom's surface form to its truth value; ``None`` means the tableau closed
|
|
532
|
+
(unsatisfiable) within the bound. Two different atoms that print alike (the numeral
|
|
533
|
+
``1`` and a constant named ``1``, a free variable ``x`` and a constant named ``x``)
|
|
534
|
+
would be ONE key of that dict, so an open branch that holds both is refused by name
|
|
535
|
+
(``NotImplementedError``) instead of being reported with one of them lost.
|
|
536
|
+
|
|
537
|
+
A modal model is a Kripke structure, not a flat literal assignment, so a modal
|
|
538
|
+
input is rejected here with a pointer to
|
|
539
|
+
:func:`unicode_logic_kit.atp.modal_tableau.modal_countermodel`, which returns a
|
|
540
|
+
verified :class:`~unicode_logic_kit.semantics.kripke.KripkeModel`.
|
|
541
|
+
|
|
542
|
+
For many-sorted input the assignment is over the guard image (see
|
|
543
|
+
:func:`tableau_closed`): the sort predicates ``S(x)`` are atoms of it like any other.
|
|
544
|
+
``timeout`` is as for :func:`tableau_closed`; a formula nested too deep to walk (see
|
|
545
|
+
there) gives ``None`` as well.
|
|
546
|
+
"""
|
|
547
|
+
formulas = list(formulas)
|
|
548
|
+
try:
|
|
549
|
+
_reject_exotic(formulas, "tableau_model")
|
|
550
|
+
if _any_modal(formulas):
|
|
551
|
+
raise NotImplementedError(
|
|
552
|
+
"tableau_model: a modal formula's model is a Kripke structure, not a flat "
|
|
553
|
+
"assignment — use unicode_logic_kit.atp.modal_tableau.modal_countermodel "
|
|
554
|
+
"(or modal_decide) instead.")
|
|
555
|
+
formulas = _lower_sorted(formulas)
|
|
556
|
+
ctx = _Ctx(max_steps, max_terms, timeout, symbol_names(*formulas))
|
|
557
|
+
closed = _close(tuple(formulas), frozenset(), (),
|
|
558
|
+
_initial_terms(formulas, max_terms), frozenset(), ctx)
|
|
559
|
+
if closed or ctx.open_branch is None:
|
|
560
|
+
return None
|
|
561
|
+
assignment = {}
|
|
562
|
+
keys = AtomKeys("tableau_model")
|
|
563
|
+
for lit in ctx.open_branch:
|
|
564
|
+
if isinstance(lit, Not) and isinstance(lit.formula, Atom):
|
|
565
|
+
assignment[keys.key(lit.formula)] = False
|
|
566
|
+
elif isinstance(lit, Atom):
|
|
567
|
+
assignment[keys.key(lit)] = True
|
|
568
|
+
return assignment
|
|
569
|
+
except RecursionError:
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# ---------------------------------------------------------------------------
|
|
574
|
+
# Tier 3: a tableau PROOF OBJECT, recorded alongside the search above with
|
|
575
|
+
# zero effect on it (see _close_recording's docstring — it is a separate,
|
|
576
|
+
# additive function; every entry point above still calls the untouched
|
|
577
|
+
# original _close, so their behaviour is unchanged by everything below).
|
|
578
|
+
# Independently checked by :mod:`unicode_logic_kit.atp.tableau_check`, which
|
|
579
|
+
# never imports this module's rule-application machinery (``_rule``,
|
|
580
|
+
# ``_close``, ``_close_recording``, ``_instance``) — only the plain data
|
|
581
|
+
# classes below.
|
|
582
|
+
# ---------------------------------------------------------------------------
|
|
583
|
+
|
|
584
|
+
@dataclass(frozen=True)
|
|
585
|
+
class TableauStep:
|
|
586
|
+
"""One rule application in a recorded tableau proof — one node of its tree.
|
|
587
|
+
|
|
588
|
+
Every step has exactly one ``parent_id`` (``0`` denotes the tableau's root —
|
|
589
|
+
:attr:`TableauProof.root_formulas`, i.e. premises + ¬conclusion) and adds the
|
|
590
|
+
formula(s) in ``produced`` to the branch it extends:
|
|
591
|
+
|
|
592
|
+
- ``"alpha"`` (non-branching): ``produced`` is all of the principal formula's
|
|
593
|
+
own components (e.g. both conjuncts of an ``And``) — one child step.
|
|
594
|
+
- ``"beta"`` (branching): recorded as TWO SIBLING steps sharing the same
|
|
595
|
+
``parent_id`` *and* the same ``principal_formula`` — one per branch, each
|
|
596
|
+
with ``branch_split=True`` and its own alternative in ``produced``.
|
|
597
|
+
- ``"gamma"`` (∀-instantiation): ``produced`` and ``terms`` are parallel
|
|
598
|
+
tuples of the same length — ``produced[i]`` is the principal formula's
|
|
599
|
+
matrix with its bound variable substituted by ``terms[i]``. A single step
|
|
600
|
+
may instantiate at several terms at once (the initial encounter of a
|
|
601
|
+
universal instantiates at every ground term already on the branch); a
|
|
602
|
+
later re-instantiation on saturation is always a single-term step.
|
|
603
|
+
- ``"delta"`` (∃-witness): ``produced`` is a single formula — the principal
|
|
604
|
+
formula's matrix substituted by the fresh witnessing constant recorded in
|
|
605
|
+
``fresh_constant``.
|
|
606
|
+
|
|
607
|
+
``step_id`` is 1-based and equal to the step's position in
|
|
608
|
+
:attr:`TableauProof.steps` (mirrors :class:`atp.resolution_check
|
|
609
|
+
.ResolutionStep`'s ``index`` convention).
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
step_id: int
|
|
613
|
+
parent_id: int
|
|
614
|
+
rule: str
|
|
615
|
+
principal_formula: Node
|
|
616
|
+
produced: Tuple[Node, ...]
|
|
617
|
+
branch_split: bool = False
|
|
618
|
+
terms: Tuple[Node, ...] = ()
|
|
619
|
+
fresh_constant: Optional[Node] = None
|
|
620
|
+
|
|
621
|
+
def __post_init__(self):
|
|
622
|
+
"""Coerce ``produced``/``terms`` to tuples for hashability."""
|
|
623
|
+
object.__setattr__(self, "produced", tuple(self.produced))
|
|
624
|
+
object.__setattr__(self, "terms", tuple(self.terms))
|
|
625
|
+
|
|
626
|
+
def to_dict(self) -> dict:
|
|
627
|
+
return {
|
|
628
|
+
"step_id": self.step_id,
|
|
629
|
+
"parent_id": self.parent_id,
|
|
630
|
+
"rule": self.rule,
|
|
631
|
+
"principal_formula": self.principal_formula.to_dict(),
|
|
632
|
+
"produced": [f.to_dict() for f in self.produced],
|
|
633
|
+
"branch_split": self.branch_split,
|
|
634
|
+
"terms": [t.to_dict() for t in self.terms],
|
|
635
|
+
"fresh_constant": self.fresh_constant.to_dict() if self.fresh_constant is not None else None,
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
@dataclass(frozen=True)
|
|
640
|
+
class TableauClosure:
|
|
641
|
+
"""One closed branch's closure pair, with the node IDs where both lie.
|
|
642
|
+
|
|
643
|
+
``leaf_id`` is the tree node (``0``, or a :class:`TableauStep`'s ``step_id``)
|
|
644
|
+
at which the branch closes — it must be a genuine leaf (no step cites it as a
|
|
645
|
+
parent). ``literal`` is the formula whose processing triggered the closure;
|
|
646
|
+
``literal_step_id`` names the branch node where it actually occurs (``0`` for
|
|
647
|
+
a root formula, else a step whose ``produced`` contains it). When ``literal``
|
|
648
|
+
is ⊥ itself the branch is self-closing and ``complement``/``complement_step_id``
|
|
649
|
+
are both ``None``; otherwise ``complement`` is ``literal``'s complementary
|
|
650
|
+
formula and ``complement_step_id`` names where IT occurs on the same branch.
|
|
651
|
+
"""
|
|
652
|
+
|
|
653
|
+
leaf_id: int
|
|
654
|
+
literal: Node
|
|
655
|
+
literal_step_id: int
|
|
656
|
+
complement: Optional[Node] = None
|
|
657
|
+
complement_step_id: Optional[int] = None
|
|
658
|
+
|
|
659
|
+
def to_dict(self) -> dict:
|
|
660
|
+
return {
|
|
661
|
+
"leaf_id": self.leaf_id,
|
|
662
|
+
"literal": self.literal.to_dict(),
|
|
663
|
+
"literal_step_id": self.literal_step_id,
|
|
664
|
+
"complement": self.complement.to_dict() if self.complement is not None else None,
|
|
665
|
+
"complement_step_id": self.complement_step_id,
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
@dataclass(frozen=True)
|
|
670
|
+
class TableauProof:
|
|
671
|
+
"""A full recorded tableau proof: the root formulas, the rule-application
|
|
672
|
+
tree, and every closed branch's closure pair.
|
|
673
|
+
|
|
674
|
+
This records what the search DID — it is not itself a certificate that the
|
|
675
|
+
search was sound. Call :func:`unicode_logic_kit.atp.tableau_check
|
|
676
|
+
.check_tableau_proof` to verify one independently before trusting it.
|
|
677
|
+
"""
|
|
678
|
+
|
|
679
|
+
root_formulas: Tuple[Node, ...]
|
|
680
|
+
steps: Tuple[TableauStep, ...] = ()
|
|
681
|
+
closures: Tuple[TableauClosure, ...] = ()
|
|
682
|
+
|
|
683
|
+
def __post_init__(self):
|
|
684
|
+
"""Coerce ``root_formulas``/``steps``/``closures`` to tuples for hashability."""
|
|
685
|
+
object.__setattr__(self, "root_formulas", tuple(self.root_formulas))
|
|
686
|
+
object.__setattr__(self, "steps", tuple(self.steps))
|
|
687
|
+
object.__setattr__(self, "closures", tuple(self.closures))
|
|
688
|
+
|
|
689
|
+
def to_dict(self) -> dict:
|
|
690
|
+
return {
|
|
691
|
+
"root_formulas": [f.to_dict() for f in self.root_formulas],
|
|
692
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
693
|
+
"closures": [c.to_dict() for c in self.closures],
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
class _Recorder:
|
|
698
|
+
"""Builds a :class:`TableauProof` tree alongside :func:`_close_recording`.
|
|
699
|
+
|
|
700
|
+
Pure bookkeeping: every method here only records what the search already
|
|
701
|
+
decided (see :func:`_close_recording`'s docstring) — nothing here feeds back
|
|
702
|
+
into a decision, so recording cannot change the search's order or result.
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
def __init__(self):
|
|
706
|
+
self._next_id = 1
|
|
707
|
+
self.steps: List[TableauStep] = []
|
|
708
|
+
self.steps_by_id: Dict[int, TableauStep] = {}
|
|
709
|
+
self.closures: List[TableauClosure] = []
|
|
710
|
+
|
|
711
|
+
def add(self, parent_id: int, rule: str, principal: Node, produced,
|
|
712
|
+
branch_split: bool = False, terms=(), fresh_constant: Optional[Node] = None) -> int:
|
|
713
|
+
"""Append a new :class:`TableauStep`, returning its fresh ``step_id``."""
|
|
714
|
+
step_id = self._next_id
|
|
715
|
+
self._next_id += 1
|
|
716
|
+
step = TableauStep(step_id, parent_id, rule, principal, tuple(produced),
|
|
717
|
+
branch_split, tuple(terms), fresh_constant)
|
|
718
|
+
self.steps.append(step)
|
|
719
|
+
self.steps_by_id[step_id] = step
|
|
720
|
+
return step_id
|
|
721
|
+
|
|
722
|
+
def _origin(self, formula: Node, node_id: int, root_formulas) -> int:
|
|
723
|
+
"""The nearest branch node (searching leaf-to-root) whose ``produced``
|
|
724
|
+
(or, at ``0``, ``root_formulas``) contains ``formula``."""
|
|
725
|
+
cur = node_id
|
|
726
|
+
while cur != 0:
|
|
727
|
+
step = self.steps_by_id[cur]
|
|
728
|
+
if any(formula == p for p in step.produced):
|
|
729
|
+
return cur
|
|
730
|
+
cur = step.parent_id
|
|
731
|
+
return 0
|
|
732
|
+
|
|
733
|
+
def close(self, node_id: int, root_formulas, literal: Node,
|
|
734
|
+
complement: Optional[Node] = None) -> None:
|
|
735
|
+
"""Record a branch closure at ``node_id`` (see :class:`TableauClosure`)."""
|
|
736
|
+
literal_id = self._origin(literal, node_id, root_formulas)
|
|
737
|
+
complement_id = self._origin(complement, node_id, root_formulas) if complement is not None else None
|
|
738
|
+
self.closures.append(TableauClosure(node_id, literal, literal_id, complement, complement_id))
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def _close_recording(work: Tuple[Node, ...], lits: frozenset,
|
|
742
|
+
gammas: Tuple[Tuple, ...], terms: Tuple[Node, ...],
|
|
743
|
+
used: frozenset, ctx: "_Ctx", rec: "_Recorder",
|
|
744
|
+
node_id: int, root_formulas: Tuple[Node, ...]) -> bool:
|
|
745
|
+
"""A recording twin of :func:`_close` — the engine behind :func:`prove_tableau_detailed`.
|
|
746
|
+
|
|
747
|
+
Line-for-line the same search as :func:`_close` — same term pools, the same
|
|
748
|
+
``ctx.fresh_const()`` call sequence, the same budget accounting, the same
|
|
749
|
+
(deterministic, backtracking-free) control flow — with a :class:`_Recorder`
|
|
750
|
+
call added at every point :func:`_close` makes a decision, so the two never
|
|
751
|
+
diverge in VERDICT for the same inputs. Kept as a wholly separate function
|
|
752
|
+
(rather than adding recorder hooks to ``_close`` itself) specifically so
|
|
753
|
+
:func:`prove_tableau` and every other existing entry point above keep
|
|
754
|
+
calling the untouched original: this function has zero effect on their
|
|
755
|
+
behaviour, by construction, not merely by argument.
|
|
756
|
+
|
|
757
|
+
Because the search is deterministic and never backtracks (every choice —
|
|
758
|
+
which fresh constant, which pool terms, which unused ``(key, t)`` pair to
|
|
759
|
+
reinstantiate — is made once, immediately, never retried), a top-level
|
|
760
|
+
``True`` return means every :class:`TableauStep`/:class:`TableauClosure`
|
|
761
|
+
recorded during the whole call genuinely lies on the closed proof: nothing
|
|
762
|
+
here is spliced out afterwards, and nothing is recorded that a failed
|
|
763
|
+
sub-call later discards (a ``False`` anywhere ends the whole search, which is why
|
|
764
|
+
:func:`prove_tableau_detailed` simply discards the whole recorder when the top call
|
|
765
|
+
returns ``False``).
|
|
766
|
+
|
|
767
|
+
Like :func:`_close` it runs in a loop with a stack of pending branches instead of
|
|
768
|
+
recursing; a branching rule records both of its steps before the first alternative is
|
|
769
|
+
searched, as the recursive search did, so the step numbers of the proof are the same.
|
|
770
|
+
"""
|
|
771
|
+
pending: List[Tuple] = [] # the alternatives still to close, the newest last
|
|
772
|
+
while True:
|
|
773
|
+
if not ctx.spend():
|
|
774
|
+
return False
|
|
775
|
+
|
|
776
|
+
closed = False
|
|
777
|
+
if work:
|
|
778
|
+
f, rest = work[0], work[1:]
|
|
779
|
+
|
|
780
|
+
if _is_literal(f):
|
|
781
|
+
if _closes_alone(f):
|
|
782
|
+
rec.close(node_id, root_formulas, f)
|
|
783
|
+
closed = True
|
|
784
|
+
elif _neg(f) in lits:
|
|
785
|
+
rec.close(node_id, root_formulas, f, _neg(f))
|
|
786
|
+
closed = True
|
|
787
|
+
else:
|
|
788
|
+
if not (_holds_always(f) or f in lits):
|
|
789
|
+
lits = lits | {f}
|
|
790
|
+
terms = _terms_of(f, terms, ctx.max_terms)
|
|
791
|
+
work = rest
|
|
792
|
+
continue
|
|
793
|
+
else:
|
|
794
|
+
rule = _rule(f)
|
|
795
|
+
kind = rule[0]
|
|
796
|
+
if kind == "alpha":
|
|
797
|
+
node_id = rec.add(node_id, "alpha", f, rule[1])
|
|
798
|
+
work = tuple(rule[1]) + rest
|
|
799
|
+
continue
|
|
800
|
+
if kind == "beta":
|
|
801
|
+
left, right = rule[1]
|
|
802
|
+
left_id = rec.add(node_id, "beta", f, left, branch_split=True)
|
|
803
|
+
right_id = rec.add(node_id, "beta", f, right, branch_split=True)
|
|
804
|
+
pending.append((tuple(right) + rest, lits, gammas, terms, used, right_id))
|
|
805
|
+
work = tuple(left) + rest
|
|
806
|
+
node_id = left_id
|
|
807
|
+
continue
|
|
808
|
+
if kind == "delta":
|
|
809
|
+
_, var, body, neg = rule
|
|
810
|
+
if len(terms) >= ctx.max_terms:
|
|
811
|
+
# Term-pool cap reached: give up on this branch (sound but incomplete) —
|
|
812
|
+
# mirrors _close exactly; this path is never on a path that ends up True.
|
|
813
|
+
if ctx.open_branch is None:
|
|
814
|
+
ctx.open_branch = lits
|
|
815
|
+
return False
|
|
816
|
+
c = ctx.fresh_const()
|
|
817
|
+
inst = _instance(var, body, neg, c)
|
|
818
|
+
node_id = rec.add(node_id, "delta", f, (inst,), fresh_constant=c)
|
|
819
|
+
work = (inst,) + rest
|
|
820
|
+
terms = terms + (c,)
|
|
821
|
+
continue
|
|
822
|
+
if kind == "gamma":
|
|
823
|
+
_, var, body, neg = rule
|
|
824
|
+
key = f
|
|
825
|
+
gammas = gammas + ((key, var, body, neg),)
|
|
826
|
+
pool = terms if terms else (ctx.fresh_const(),)
|
|
827
|
+
insts = tuple(_instance(var, body, neg, t) for t in pool)
|
|
828
|
+
used = used | {(key, t) for t in pool}
|
|
829
|
+
terms = terms if terms else pool
|
|
830
|
+
node_id = rec.add(node_id, "gamma", f, insts, terms=pool)
|
|
831
|
+
work = insts + rest
|
|
832
|
+
continue
|
|
833
|
+
raise AssertionError(kind)
|
|
834
|
+
else:
|
|
835
|
+
# No compound work left: re-instantiate a universal at a term it has not used.
|
|
836
|
+
instantiated = False
|
|
837
|
+
for key, var, body, neg in gammas:
|
|
838
|
+
for t in terms:
|
|
839
|
+
if (key, t) not in used:
|
|
840
|
+
inst = _instance(var, body, neg, t)
|
|
841
|
+
node_id = rec.add(node_id, "gamma", key, (inst,), terms=(t,))
|
|
842
|
+
work = (inst,)
|
|
843
|
+
used = used | {(key, t)}
|
|
844
|
+
instantiated = True
|
|
845
|
+
break
|
|
846
|
+
if instantiated:
|
|
847
|
+
break
|
|
848
|
+
if instantiated:
|
|
849
|
+
continue
|
|
850
|
+
# Saturated and not closed: an OPEN branch — never reached on a path that ends up True.
|
|
851
|
+
if ctx.open_branch is None:
|
|
852
|
+
ctx.open_branch = lits
|
|
853
|
+
return False
|
|
854
|
+
|
|
855
|
+
# This branch closed: go on with the next alternative, if there is one.
|
|
856
|
+
if closed:
|
|
857
|
+
if not pending:
|
|
858
|
+
return True
|
|
859
|
+
work, lits, gammas, terms, used, node_id = pending.pop()
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _search_detailed(premises, conclusion: Node, max_steps: int = 20000,
|
|
863
|
+
max_terms: int = 8, timeout: Optional[int] = None):
|
|
864
|
+
""":func:`prove_tableau_detailed`, returning ``(proof, nesting)``.
|
|
865
|
+
|
|
866
|
+
``nesting`` is ``None`` unless the search ended because a formula was nested deeper
|
|
867
|
+
than its helpers can walk within the interpreter's recursion limit; it is then the
|
|
868
|
+
:func:`nesting_depth` of the problem, and ``proof`` is ``None``.
|
|
869
|
+
"""
|
|
870
|
+
formulas = list(premises) + [Not(conclusion)]
|
|
871
|
+
try:
|
|
872
|
+
_reject_exotic(formulas, "prove_tableau_detailed")
|
|
873
|
+
if _any_modal(formulas):
|
|
874
|
+
raise NotImplementedError(
|
|
875
|
+
"prove_tableau_detailed: modal tableaux do not build a detailed proof "
|
|
876
|
+
"object here — use modal_tableau.modal_prove for the plain verdict.")
|
|
877
|
+
formulas = _lower_sorted(formulas)
|
|
878
|
+
root_formulas = tuple(formulas)
|
|
879
|
+
ctx = _Ctx(max_steps, max_terms, timeout, symbol_names(*formulas))
|
|
880
|
+
rec = _Recorder()
|
|
881
|
+
closed = _close_recording(
|
|
882
|
+
root_formulas, frozenset(), (), _initial_terms(formulas, max_terms),
|
|
883
|
+
frozenset(), ctx, rec, 0, root_formulas)
|
|
884
|
+
if not closed:
|
|
885
|
+
return None, None
|
|
886
|
+
return TableauProof(root_formulas, tuple(rec.steps), tuple(rec.closures)), None
|
|
887
|
+
except RecursionError:
|
|
888
|
+
return None, nesting_depth(*formulas)
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def prove_tableau_detailed(premises, conclusion: Node, max_steps: int = 20000,
|
|
892
|
+
max_terms: int = 8,
|
|
893
|
+
timeout: Optional[int] = None) -> Optional["TableauProof"]:
|
|
894
|
+
"""Return a :class:`TableauProof` if a closed tableau is found within budget, else ``None``.
|
|
895
|
+
|
|
896
|
+
Builds the SAME tableau :func:`prove_tableau` would — :func:`_close_recording`
|
|
897
|
+
mirrors :func:`_close` exactly (see its docstring) — plus a proof object
|
|
898
|
+
recording every rule application and every branch's closure pair.
|
|
899
|
+
|
|
900
|
+
``None`` is NEVER a verdict of invalidity, exactly as for :func:`prove_tableau`:
|
|
901
|
+
it only means no closed tableau was found within ``max_steps``/``max_terms``/
|
|
902
|
+
``timeout`` (first-order γ-instantiation is merely semi-decidable; ``timeout`` is in
|
|
903
|
+
milliseconds, default none, and is checked at every step). The length of a branch is
|
|
904
|
+
bounded by ``max_steps``, not by the interpreter's recursion limit; a formula nested
|
|
905
|
+
deeper than the helpers that walk it can follow (see :func:`nesting_depth`) is a bound
|
|
906
|
+
too and gives ``None``, never :class:`RecursionError`. Call
|
|
907
|
+
:func:`unicode_logic_kit.atp.tableau_check.check_tableau_proof` to independently
|
|
908
|
+
verify a returned proof before trusting it — this function's own bookkeeping is
|
|
909
|
+
not a soundness guarantee.
|
|
910
|
+
|
|
911
|
+
For many-sorted input the proof's root formulas are the guard images of the premises
|
|
912
|
+
and of the negated conclusion followed by the background facts of
|
|
913
|
+
:func:`~unicode_logic_kit.fol.nodes.sort_axioms` (see :func:`tableau_closed`); the
|
|
914
|
+
checker derives the same roots from the premises and the conclusion.
|
|
915
|
+
|
|
916
|
+
Raises the same ``NotImplementedError`` as :func:`prove_tableau` for a
|
|
917
|
+
non-classical node family. A modal input is also rejected here (with a
|
|
918
|
+
pointer to :mod:`unicode_logic_kit.atp.modal_tableau`) since the labelled modal
|
|
919
|
+
tableau does not build a detailed proof object of this shape.
|
|
920
|
+
"""
|
|
921
|
+
return _search_detailed(premises, conclusion, max_steps, max_terms, timeout)[0]
|