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,1376 @@
|
|
|
1
|
+
"""Sound first-order resolution theorem prover (refutation-based).
|
|
2
|
+
|
|
3
|
+
A self-contained, Prover9-free decision procedure for first-order entailment and
|
|
4
|
+
validity, built on the project's existing normal-form and unification machinery.
|
|
5
|
+
It is *sound* — it never reports a theorem that is not one — but, since
|
|
6
|
+
first-order resolution is only semi-decidable, it is deliberately *incomplete*
|
|
7
|
+
under a step bound: when the bound is hit it conservatively reports "not proved".
|
|
8
|
+
|
|
9
|
+
Pipeline:
|
|
10
|
+
|
|
11
|
+
1. Clausal form (:func:`to_clauses`). A formula is ``skolemize``-d (prenex NNF,
|
|
12
|
+
existentials replaced by Skolem terms, universal prefix retained), the ∀
|
|
13
|
+
prefix is dropped (``_prenex_split``), the matrix is put into CNF (``_cnf``)
|
|
14
|
+
and split into clauses (``_clauses``). Each literal is a positive ``Atom`` or
|
|
15
|
+
a ``Not(Atom)``; each clause is a ``frozenset`` of literals; the clause set is
|
|
16
|
+
a ``set`` of such frozensets. Variables are implicitly universally quantified.
|
|
17
|
+
The truth constants ``$true`` and ``$false`` are constants, not letters: a
|
|
18
|
+
clause with ``$true`` (or ``¬$false``) is true and is dropped, and a literal
|
|
19
|
+
``$false`` (or ``¬$true``) is removed from its clause, so a clause of nothing
|
|
20
|
+
else is the empty clause.
|
|
21
|
+
|
|
22
|
+
2. Standardize apart (:func:`_standardize_apart`). Before two clauses are
|
|
23
|
+
resolved their variables are renamed to fresh disjoint names so the clauses
|
|
24
|
+
share no variable. This is required for soundness.
|
|
25
|
+
|
|
26
|
+
3. Binary resolution (:func:`_resolvents`). For a literal ``L`` in one clause and
|
|
27
|
+
a complementary literal ``M`` in the other (one ``Atom A``, the other
|
|
28
|
+
``Not(B)`` with ``A``/``B`` the same predicate and arity), the atoms are
|
|
29
|
+
unified; on success with mgu σ the resolvent is σ applied to
|
|
30
|
+
``(C1 ∖ L) ∪ (C2 ∖ M)``.
|
|
31
|
+
|
|
32
|
+
4. Factoring (:func:`_factors`). Within a clause, two literals of the same
|
|
33
|
+
polarity that unify are merged under their mgu, yielding a factored clause —
|
|
34
|
+
needed for completeness on some inputs.
|
|
35
|
+
|
|
36
|
+
5. Redundancy elimination (:func:`_is_tautology`, :func:`_subsumes`). A clause
|
|
37
|
+
containing a literal and its exact complement on the same atom is a
|
|
38
|
+
tautology and is never kept. A clause D is discarded the moment some kept
|
|
39
|
+
clause C subsumes it (``∃σ`` — binding only C's variables via one-sided
|
|
40
|
+
matching, :func:`_match` — with ``Cσ ⊆ D``); conversely, keeping a new
|
|
41
|
+
clause D retires every previously kept clause that D subsumes. This keeps
|
|
42
|
+
the given-clause set free of redundant clauses without weakening what is
|
|
43
|
+
ultimately derivable — see :func:`refute` for why completeness survives.
|
|
44
|
+
|
|
45
|
+
6. Equality (:func:`_paramodulants_cross`,
|
|
46
|
+
:func:`_reflexivity_resolvents`, :func:`_demodulate_to_fixpoint`). Equality
|
|
47
|
+
(``=``) is, by itself, just an ordinary uninterpreted binary predicate to
|
|
48
|
+
every mechanism above — nothing here or elsewhere in this module injects
|
|
49
|
+
reflexivity/symmetry/transitivity/congruence AXIOMS. Instead the module
|
|
50
|
+
adds equality's INFERENCE RULES directly:
|
|
51
|
+
|
|
52
|
+
- Paramodulation: from a positive equality literal ``s ≈ t`` in one clause
|
|
53
|
+
and a literal ``L`` containing a subterm at position ``p`` in another
|
|
54
|
+
(or the same) clause, if ``s`` unifies with that subterm (mgu σ), infer
|
|
55
|
+
the clauses' remainders plus ``L[p ↦ t]`` — all under σ. Unconditionally
|
|
56
|
+
SOUND in either direction (from ``s`` into occurrences that look like
|
|
57
|
+
``s``, or symmetrically from ``t`` into occurrences that look like
|
|
58
|
+
``t``); a simple term order (:func:`_term_gt` — weight = term size, ties
|
|
59
|
+
broken lexicographically by ``key_text()``, see its docstring)
|
|
60
|
+
prunes the search by allowing paramodulation only from the
|
|
61
|
+
order-greater side when the order strictly decides, trying both
|
|
62
|
+
directions only when it does not (which, under this concrete order,
|
|
63
|
+
only happens for two occurrences of the very same term). Paramodulation
|
|
64
|
+
is ONLY generated cross-clause (:func:`_paramodulants_cross`,
|
|
65
|
+
standardized apart like :func:`_resolvents`) — a clause paramodulating
|
|
66
|
+
into itself goes through the same path with a renamed copy via the
|
|
67
|
+
given/kept loop. A shared-instance "self-paramodulation" shortcut
|
|
68
|
+
(dropping both the consumed equation and the target literal from ONE
|
|
69
|
+
instantiation) is deliberately absent: it is UNSOUND — for
|
|
70
|
+
``{u ≈ v, L[u]}`` it would infer ``{L[v]}``, false in any model that
|
|
71
|
+
satisfies the clause via ``L[u]`` alone while ``u ≠ v`` (adversarial
|
|
72
|
+
review, Tier 3: the shortcut produced confirmed false-positive
|
|
73
|
+
refutations and was removed together with its checker rule).
|
|
74
|
+
- Reflexivity resolution (:func:`_reflexivity_resolvents`): a literal
|
|
75
|
+
``¬(u ≈ v)`` where ``u``, ``v`` unify is dropped from its clause under
|
|
76
|
+
the unifier — the mechanism that closes goals like ``⊢ c=c`` (whose
|
|
77
|
+
negation clausifies to ``{¬(c=c)}``).
|
|
78
|
+
- Demodulation (:func:`_demodulate_to_fixpoint`): a SIMPLIFICATION, not a
|
|
79
|
+
search-widening inference. A kept UNIT clause ``{l ≈ r}`` is oriented by
|
|
80
|
+
the same term order and used as a rewrite rule ``l → r`` (only when the
|
|
81
|
+
order strictly says so; an unoriented unit equation contributes no
|
|
82
|
+
rule). Newly generated clauses are rewritten to a fixpoint using
|
|
83
|
+
one-sided MATCHING (:func:`_match_term` — not unification: the rule's
|
|
84
|
+
variables bind, the target's are held fixed) before being tested for
|
|
85
|
+
redundancy/kept — every individual rewrite is its own accounted step
|
|
86
|
+
against ``max_steps`` (see :func:`refute`), never a free/silent
|
|
87
|
+
rewrite. Only NEWLY GENERATED clauses are demodulated (forward
|
|
88
|
+
demodulation); already-kept clauses are never retroactively
|
|
89
|
+
re-simplified by a later rule (backward demodulation is out of scope).
|
|
90
|
+
|
|
91
|
+
Honesty note, as promised by this module's general incompleteness stance:
|
|
92
|
+
paramodulation + factoring + resolution is refutation-complete for
|
|
93
|
+
first-order logic WITH equality in principle (this is the classical
|
|
94
|
+
paramodulation completeness theorem), but this implementation does not
|
|
95
|
+
pursue the additional refinements (selection functions, the "basic"
|
|
96
|
+
restriction, splitting, a genuine reduction/simplification ORDER with the
|
|
97
|
+
substitution- and subterm-compatibility closure a real Knuth–Bendix or
|
|
98
|
+
lexicographic path order provides) that a serious equality prover needs
|
|
99
|
+
to be practically complete. This stays SOUND unconditionally; completeness
|
|
100
|
+
for equality problems is emphatically NOT guaranteed beyond what small,
|
|
101
|
+
textbook-sized problems reach within ``max_steps`` — it is a didactic
|
|
102
|
+
equality prover, not a competitive one.
|
|
103
|
+
|
|
104
|
+
7. Saturation (:func:`refute`). Resolution, factoring, and the equality rules
|
|
105
|
+
above are iterated, filtered through the redundancy elimination in step 5
|
|
106
|
+
(applied after demodulation simplifies each new candidate), until the
|
|
107
|
+
empty clause ``frozenset()`` appears (⇒ unsatisfiable / refuted) or no new,
|
|
108
|
+
non-redundant clause is produced (⇒ saturated) or a step bound is reached
|
|
109
|
+
(⇒ undecided, reported conservatively as not refuted).
|
|
110
|
+
|
|
111
|
+
Public API: :func:`to_clauses`, :func:`refute`, :func:`prove`,
|
|
112
|
+
:func:`is_valid_resolution`.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
import time
|
|
116
|
+
from typing import Optional
|
|
117
|
+
|
|
118
|
+
from .._deadline import instant as _instant, remaining_ms as _remaining_ms, run_until as _run_until
|
|
119
|
+
from ..fol._free_parameters import parameterize
|
|
120
|
+
from ..fol._identifiers import symbol_names
|
|
121
|
+
from ..fol._msfl_nodes import key_text, sort_axioms
|
|
122
|
+
from ..fol._truth_constants import truth_value
|
|
123
|
+
from ..fol.nodes import (
|
|
124
|
+
Node, Atom, Not, Implies, Quantifier, Variable, Constant, Number, Function,
|
|
125
|
+
Measure, SortedConstant, Cardinality, SortedCardinality, to_fol,
|
|
126
|
+
)
|
|
127
|
+
from ..fol.normalforms import skolemize, _prenex_split, _cnf, _clauses
|
|
128
|
+
from ..fol.unification import unify, apply_subst
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Free variables and universal closure
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def _free_var_names(node: Node, bound: frozenset = frozenset()) -> set:
|
|
136
|
+
"""Return the set of names of variables occurring free in node.
|
|
137
|
+
|
|
138
|
+
A :class:`Variable` is free unless its name is bound by an enclosing
|
|
139
|
+
:class:`Quantifier`. Recurses structurally; the quantifier's bound name is
|
|
140
|
+
added to ``bound`` for its body. The input is not mutated.
|
|
141
|
+
"""
|
|
142
|
+
if isinstance(node, Variable):
|
|
143
|
+
return set() if node.name in bound else {node.name}
|
|
144
|
+
if isinstance(node, (Constant, Number)):
|
|
145
|
+
return set()
|
|
146
|
+
if isinstance(node, Quantifier):
|
|
147
|
+
return _free_var_names(node.formula, bound | {node.variable.name})
|
|
148
|
+
names = set()
|
|
149
|
+
for child in node._child_nodes():
|
|
150
|
+
names |= _free_var_names(child, bound)
|
|
151
|
+
return names
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _universal_closure(node: Node) -> Node:
|
|
155
|
+
"""Return node wrapped in ∀ for each of its free variables.
|
|
156
|
+
|
|
157
|
+
Closing a source formula over its free variables BEFORE skolemisation is a
|
|
158
|
+
soundness requirement: ``skolemize`` is only satisfiability-preserving for
|
|
159
|
+
sentences. If a free variable were left in the matrix, the per-clause
|
|
160
|
+
"implicitly universally quantified" reading used by resolution would
|
|
161
|
+
decouple it from existentials in its scope — e.g. ``¬(P(x) → ∀y P(y))``
|
|
162
|
+
would skolemise to ``P(x) ∧ ¬P(sk0)`` with a free ``x`` and a Skolem
|
|
163
|
+
*constant* ``sk0``, which spuriously resolve to the empty clause. Closing
|
|
164
|
+
first makes the existential a Skolem *function* of the universal, so the
|
|
165
|
+
occurs-check correctly blocks the unsound resolution. The names are sorted so
|
|
166
|
+
the closure is deterministic. The input is not mutated.
|
|
167
|
+
"""
|
|
168
|
+
free = sorted(_free_var_names(node))
|
|
169
|
+
result = node
|
|
170
|
+
for name in reversed(free):
|
|
171
|
+
result = Quantifier("∀", Variable(name), result)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Clausal form
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
def _symbol_names(node: Node) -> set:
|
|
180
|
+
"""Return every constant, function, and predicate symbol name in node.
|
|
181
|
+
|
|
182
|
+
Variables are excluded (they are renamed separately). Used to detect which
|
|
183
|
+
symbols skolemisation introduced, so they can be made globally unique.
|
|
184
|
+
A :class:`SortedConstant` is a constant like any other: ``c:S`` is the
|
|
185
|
+
symbol ``c``, and it must not look "introduced" once the many-sorted
|
|
186
|
+
reduction turns it into the plain ``c``.
|
|
187
|
+
"""
|
|
188
|
+
names = set()
|
|
189
|
+
for n in node.walk():
|
|
190
|
+
if isinstance(n, (Constant, SortedConstant)):
|
|
191
|
+
names.add(n.name)
|
|
192
|
+
elif isinstance(n, Function):
|
|
193
|
+
names.add(n.name)
|
|
194
|
+
elif isinstance(n, Atom):
|
|
195
|
+
names.add(n.predicate)
|
|
196
|
+
return names
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _rename_symbols(node: Node, mapping: dict) -> Node:
|
|
200
|
+
"""Return node with each Constant/Function name remapped via ``mapping``.
|
|
201
|
+
|
|
202
|
+
Only Constant and Function *symbol names* are rewritten (predicates and
|
|
203
|
+
variables are untouched). This makes the Skolem symbols introduced by one
|
|
204
|
+
source formula disjoint from those of another, so two independently
|
|
205
|
+
skolemised sources cannot share a Skolem name (e.g. both producing ``sk0``)
|
|
206
|
+
and spuriously resolve. Recursion is fully structural (``map_children``), so
|
|
207
|
+
it works on any node — including the quantifier-prefixed, still-connective
|
|
208
|
+
formula produced by skolemisation, before prenex-splitting. The input is not
|
|
209
|
+
mutated.
|
|
210
|
+
"""
|
|
211
|
+
if isinstance(node, Constant):
|
|
212
|
+
return Constant(mapping.get(node.name, node.name))
|
|
213
|
+
if isinstance(node, Function):
|
|
214
|
+
return Function(mapping.get(node.name, node.name),
|
|
215
|
+
[_rename_symbols(a, mapping) for a in node.args])
|
|
216
|
+
return node.map_children(lambda c: _rename_symbols(c, mapping))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def to_clauses(formula: Node, sk_counter: list = None, avoid=()) -> set:
|
|
220
|
+
"""Return the clausal form of a single formula as a set of frozensets.
|
|
221
|
+
|
|
222
|
+
The formula is first universally closed over its free variables (the convention
|
|
223
|
+
of a clausal form, whose variables are all universal; :func:`prove` hands it
|
|
224
|
+
formulas without a free variable, see there), then
|
|
225
|
+
skolemised (prenex NNF, existentials → Skolem terms, ∀ prefix kept). The
|
|
226
|
+
Skolem symbols that skolemisation introduced are renamed to globally unique
|
|
227
|
+
names — using the shared mutable cursor ``sk_counter`` when one is supplied —
|
|
228
|
+
so that two independently clausified source formulas can never share a Skolem
|
|
229
|
+
symbol (which would be UNSOUND: e.g. ``∃x P(x)`` and the negated conclusion of
|
|
230
|
+
``∀y P(y)`` both skolemise to ``sk0`` and would spuriously resolve to the
|
|
231
|
+
empty clause). A Skolem symbol is also fresh against every name of the formula
|
|
232
|
+
and every name in ``avoid``: a source formula of a problem passes the names of
|
|
233
|
+
ALL the formulas of the problem, or a Skolem constant could take the spelling
|
|
234
|
+
of a constant that only another formula has. The universal prefix is then
|
|
235
|
+
dropped, the matrix is converted
|
|
236
|
+
to CNF and split into clauses. Each literal is a positive :class:`Atom` or a
|
|
237
|
+
:class:`Not` wrapping an Atom; each clause is a ``frozenset`` of literals; the
|
|
238
|
+
result is a ``set`` of those frozensets. The remaining variables are
|
|
239
|
+
implicitly universally quantified. The input node is not mutated.
|
|
240
|
+
"""
|
|
241
|
+
closed = _universal_closure(formula)
|
|
242
|
+
sk = skolemize(closed)
|
|
243
|
+
# The symbols the formula already has. Skolemisation reduces the many-sorted
|
|
244
|
+
# nodes first (a sorted constant ``c:S`` becomes the plain ``c``, a sort
|
|
245
|
+
# becomes a guard predicate), so the names are read off the reduced formula
|
|
246
|
+
# as well as off the source: whatever is in ``sk`` and in neither was
|
|
247
|
+
# introduced by skolemisation itself. A sorted constant therefore keeps its
|
|
248
|
+
# name -- it is the same constant in every formula it occurs in.
|
|
249
|
+
reduced = to_fol(closed)
|
|
250
|
+
before = _symbol_names(closed) | _symbol_names(reduced)
|
|
251
|
+
if sk_counter is None:
|
|
252
|
+
sk_counter = [0]
|
|
253
|
+
# Symbols present after skolemisation but not before are Skolem symbols.
|
|
254
|
+
introduced = _symbol_names(sk) - before
|
|
255
|
+
mapping = {}
|
|
256
|
+
if introduced:
|
|
257
|
+
# A Skolem symbol must not have the spelling of ANY name the problem carries, of
|
|
258
|
+
# any kind (a constant, a function, a predicate, a sort, a variable).
|
|
259
|
+
taken = set(symbol_names(closed, reduced)) | set(avoid)
|
|
260
|
+
for name in sorted(introduced):
|
|
261
|
+
candidate = f"_sk{sk_counter[0]}"
|
|
262
|
+
sk_counter[0] += 1
|
|
263
|
+
while candidate in taken:
|
|
264
|
+
candidate = f"_sk{sk_counter[0]}"
|
|
265
|
+
sk_counter[0] += 1
|
|
266
|
+
mapping[name] = candidate
|
|
267
|
+
if mapping:
|
|
268
|
+
sk = _rename_symbols(sk, mapping)
|
|
269
|
+
_, matrix = _prenex_split(sk)
|
|
270
|
+
cnf = _cnf(matrix)
|
|
271
|
+
result = set()
|
|
272
|
+
for clause in _clauses(cnf):
|
|
273
|
+
simplified = _drop_truth_constants(clause)
|
|
274
|
+
if simplified is not None:
|
|
275
|
+
result.add(frozenset(simplified))
|
|
276
|
+
return result
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _drop_truth_constants(literals):
|
|
280
|
+
"""The clause ``literals`` with the truth constants read as constants, or ``None``.
|
|
281
|
+
|
|
282
|
+
``$true`` and ``¬$false`` hold in every interpretation, so a clause that has one
|
|
283
|
+
is true and ``None`` is returned (the clause is dropped). ``$false`` and ``¬$true``
|
|
284
|
+
hold in none, so they are removed from the clause; a clause that has nothing else
|
|
285
|
+
is the EMPTY clause, which refutes the set. Every other literal is kept as it is.
|
|
286
|
+
"""
|
|
287
|
+
kept = []
|
|
288
|
+
for literal in literals:
|
|
289
|
+
if isinstance(literal, Not):
|
|
290
|
+
constant = truth_value(literal.formula)
|
|
291
|
+
constant = None if constant is None else not constant
|
|
292
|
+
else:
|
|
293
|
+
constant = truth_value(literal)
|
|
294
|
+
if constant is True:
|
|
295
|
+
return None
|
|
296
|
+
if constant is None:
|
|
297
|
+
kept.append(literal)
|
|
298
|
+
return kept
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
# Literal helpers
|
|
303
|
+
# ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
def _atom_of(literal: Node) -> Atom:
|
|
306
|
+
"""Return the underlying Atom of a literal (Atom or Not(Atom))."""
|
|
307
|
+
if isinstance(literal, Not):
|
|
308
|
+
return literal.formula
|
|
309
|
+
return literal
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _is_positive(literal: Node) -> bool:
|
|
313
|
+
"""Return True iff the literal is a positive Atom (not wrapped in Not)."""
|
|
314
|
+
return isinstance(literal, Atom)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _subst_literal(literal: Node, subst: dict) -> Node:
|
|
318
|
+
"""Apply a substitution to a literal, preserving its polarity."""
|
|
319
|
+
if isinstance(literal, Not):
|
|
320
|
+
return Not(apply_subst(literal.formula, subst))
|
|
321
|
+
return apply_subst(literal, subst)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _rename_term(node: Node, mapping: dict) -> Node:
|
|
325
|
+
"""Return node with each Variable replaced via a one-level name mapping.
|
|
326
|
+
|
|
327
|
+
Unlike :func:`apply_subst`, this does NOT follow chains: a variable name is
|
|
328
|
+
looked up exactly once in ``mapping``. That makes it safe for standardizing
|
|
329
|
+
apart, where a fresh name may itself appear as a key (e.g. renaming a clause
|
|
330
|
+
whose variables are already ``_r0`` while ``_r0`` is being mapped to a new
|
|
331
|
+
fresh variable), which would otherwise loop. It is also how a one-sided
|
|
332
|
+
matcher is applied (:func:`_demodulate_once`): the images of a matcher are
|
|
333
|
+
terms of the target and are never looked up again.
|
|
334
|
+
"""
|
|
335
|
+
if isinstance(node, Variable):
|
|
336
|
+
return mapping.get(node.name, node)
|
|
337
|
+
if isinstance(node, (Constant, Number)):
|
|
338
|
+
return node
|
|
339
|
+
if isinstance(node, Function):
|
|
340
|
+
return Function(node.name, [_rename_term(a, mapping) for a in node.args])
|
|
341
|
+
if isinstance(node, Measure):
|
|
342
|
+
# μ(entity, dimension) is an ordinary binary term (the exports lower it
|
|
343
|
+
# to the uninterpreted function measure/2), so renaming recurses into
|
|
344
|
+
# both argument slots exactly as for Function.
|
|
345
|
+
return Measure(_rename_term(node.entity, mapping),
|
|
346
|
+
_rename_term(node.dimension, mapping))
|
|
347
|
+
if isinstance(node, Atom):
|
|
348
|
+
return Atom(node.predicate, [_rename_term(a, mapping) for a in node.args])
|
|
349
|
+
if isinstance(node, Not):
|
|
350
|
+
return Not(_rename_term(node.formula, mapping))
|
|
351
|
+
raise TypeError(f"_rename_term: unsupported node type {type(node).__name__}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
# Standardizing variables apart
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
def _lit_key(literal) -> str:
|
|
359
|
+
"""Canonical sort key for a literal (its surface form with every constant written by
|
|
360
|
+
its bare name, ``key_text``): the saturation loop
|
|
361
|
+
must visit literals and clauses in a content-determined order so a proof
|
|
362
|
+
search is reproducible run to run (frozenset iteration is hash-randomised).
|
|
363
|
+
The quotes that the text of a formula puts around a constant such as ``k2`` play no part
|
|
364
|
+
in the order, so the search visits the same literals in the same order as it did before
|
|
365
|
+
a constant could be written in quotes."""
|
|
366
|
+
return key_text(literal)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _clause_vars(clause: frozenset) -> set:
|
|
370
|
+
"""Return the set of variable names occurring anywhere in a clause."""
|
|
371
|
+
names = set()
|
|
372
|
+
for literal in clause:
|
|
373
|
+
for node in _atom_of(literal).walk():
|
|
374
|
+
if isinstance(node, Variable):
|
|
375
|
+
names.add(node.name)
|
|
376
|
+
return names
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _rename_clause(clause: frozenset, counter: list) -> frozenset:
|
|
380
|
+
"""Return a copy of clause with every variable renamed to a fresh name.
|
|
381
|
+
|
|
382
|
+
``counter`` is a single-element list used as a shared mutable cursor so that
|
|
383
|
+
successive renamings across the whole run draw disjoint names (``_r0``,
|
|
384
|
+
``_r1`` …). The mapping is consistent within the clause, so distinct
|
|
385
|
+
occurrences of the same variable stay shared.
|
|
386
|
+
"""
|
|
387
|
+
mapping = {}
|
|
388
|
+
# Sorted: variable numbering must be a function of the clause CONTENT, not
|
|
389
|
+
# of set iteration order — otherwise two runs of the same proof search build
|
|
390
|
+
# alpha-variant (differently named) clauses, the kept-set dedup stops
|
|
391
|
+
# recognising repeats, and whether a goal closes within max_steps becomes
|
|
392
|
+
# hash-seed-dependent (an irreproducible verdict).
|
|
393
|
+
for name in sorted(_clause_vars(clause)):
|
|
394
|
+
mapping[name] = Variable(f"_r{counter[0]}")
|
|
395
|
+
counter[0] += 1
|
|
396
|
+
if not mapping:
|
|
397
|
+
return clause
|
|
398
|
+
return frozenset(_rename_term(lit, mapping) for lit in clause)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ---------------------------------------------------------------------------
|
|
402
|
+
# Redundancy elimination: tautology deletion and clause subsumption
|
|
403
|
+
# ---------------------------------------------------------------------------
|
|
404
|
+
|
|
405
|
+
def _is_tautology(clause: frozenset) -> bool:
|
|
406
|
+
"""Return True iff clause contains a literal and its exact complement.
|
|
407
|
+
|
|
408
|
+
A clause is a tautology (true in every interpretation, hence useless as a
|
|
409
|
+
resolution premise) when it contains both a positive occurrence and a
|
|
410
|
+
negative occurrence of the SAME atom — same predicate, same arguments,
|
|
411
|
+
checked by structural equality, not merely unifiability. ``P(x) ∨ ¬P(y)``
|
|
412
|
+
is NOT caught (x and y are different atoms syntactically, and identifying
|
|
413
|
+
them would require instantiation this check does not perform); ``P(x) ∨
|
|
414
|
+
¬P(x)`` is. The empty clause is never a tautology.
|
|
415
|
+
"""
|
|
416
|
+
positive = {_atom_of(l) for l in clause if _is_positive(l)}
|
|
417
|
+
negative = {_atom_of(l) for l in clause if not _is_positive(l)}
|
|
418
|
+
return bool(positive & negative)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _match_term(pattern: Node, target: Node, subst: dict):
|
|
422
|
+
"""One-sided structural match of pattern against target, extending subst.
|
|
423
|
+
|
|
424
|
+
Unlike :func:`~unicode_logic_kit.fol.unification.unify`, which may bind
|
|
425
|
+
variables on EITHER side, this binds only PATTERN's variables; TARGET is
|
|
426
|
+
held fixed and its own variables (if any) are treated as opaque constants
|
|
427
|
+
— never instantiated. That asymmetry is exactly what clause subsumption
|
|
428
|
+
needs: the subsuming clause's variables range over all its instances, but
|
|
429
|
+
the subsumed clause is the fixed fact being checked against, not a term
|
|
430
|
+
free to unify. A repeated pattern variable must match the same target
|
|
431
|
+
subterm everywhere (checked via structural equality against the existing
|
|
432
|
+
binding). Returns the extended substitution, or None if no match exists.
|
|
433
|
+
No occurs-check is needed: nothing is ever built from a pattern variable's
|
|
434
|
+
own binding, so no cyclic term can arise.
|
|
435
|
+
"""
|
|
436
|
+
if isinstance(pattern, Variable):
|
|
437
|
+
if pattern.name in subst:
|
|
438
|
+
return subst if subst[pattern.name] == target else None
|
|
439
|
+
extended = dict(subst)
|
|
440
|
+
extended[pattern.name] = target
|
|
441
|
+
return extended
|
|
442
|
+
if isinstance(pattern, Function):
|
|
443
|
+
if (not isinstance(target, Function)
|
|
444
|
+
or pattern.name != target.name
|
|
445
|
+
or len(pattern.args) != len(target.args)):
|
|
446
|
+
return None
|
|
447
|
+
for p_arg, t_arg in zip(pattern.args, target.args):
|
|
448
|
+
subst = _match_term(p_arg, t_arg, subst)
|
|
449
|
+
if subst is None:
|
|
450
|
+
return None
|
|
451
|
+
return subst
|
|
452
|
+
if isinstance(pattern, Measure):
|
|
453
|
+
if not isinstance(target, Measure):
|
|
454
|
+
return None
|
|
455
|
+
subst = _match_term(pattern.entity, target.entity, subst)
|
|
456
|
+
if subst is None:
|
|
457
|
+
return None
|
|
458
|
+
return _match_term(pattern.dimension, target.dimension, subst)
|
|
459
|
+
# Constant / Number (or any other leaf term): match iff structurally
|
|
460
|
+
# identical. Dataclass equality already requires equal type, so e.g. a
|
|
461
|
+
# Constant pattern can never match a Function or Variable target here.
|
|
462
|
+
return subst if pattern == target else None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _match(pattern_atom: Atom, target_atom: Atom, subst: dict = None):
|
|
466
|
+
"""One-sided match of two atoms: bind pattern_atom's variables against
|
|
467
|
+
target_atom's structure (see :func:`_match_term`). Same predicate and
|
|
468
|
+
arity are required; arguments are matched pairwise, threading the
|
|
469
|
+
substitution left to right. Returns the substitution, or None on failure.
|
|
470
|
+
``subst`` defaults to the empty substitution.
|
|
471
|
+
"""
|
|
472
|
+
if subst is None:
|
|
473
|
+
subst = {}
|
|
474
|
+
if (pattern_atom.predicate != target_atom.predicate
|
|
475
|
+
or len(pattern_atom.args) != len(target_atom.args)):
|
|
476
|
+
return None
|
|
477
|
+
for p_arg, t_arg in zip(pattern_atom.args, target_atom.args):
|
|
478
|
+
subst = _match_term(p_arg, t_arg, subst)
|
|
479
|
+
if subst is None:
|
|
480
|
+
return None
|
|
481
|
+
return subst
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _match_literal(pattern_lit: Node, target_lit: Node, subst: dict):
|
|
485
|
+
"""One-sided match of two literals: same polarity, then :func:`_match` on
|
|
486
|
+
their atoms. Returns the extended substitution, or None on failure."""
|
|
487
|
+
if _is_positive(pattern_lit) != _is_positive(target_lit):
|
|
488
|
+
return None
|
|
489
|
+
return _match(_atom_of(pattern_lit), _atom_of(target_lit), subst)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _subsumes(pattern_clause: frozenset, target_clause: frozenset) -> bool:
|
|
493
|
+
"""Return True iff pattern_clause subsumes target_clause.
|
|
494
|
+
|
|
495
|
+
Clause subsumption: ``∃σ`` (binding only pattern_clause's variables) with
|
|
496
|
+
``σ(pattern_clause) ⊆ target_clause`` as a set of literals — i.e. every
|
|
497
|
+
literal of pattern_clause can be assigned, under one SHARED substitution,
|
|
498
|
+
to some literal of target_clause. The assignment need not be injective:
|
|
499
|
+
two distinct pattern literals may map to the same target literal (e.g.
|
|
500
|
+
``{P(x), P(y)}`` subsumes ``{P(a)}`` via x, y ↦ a both landing on the sole
|
|
501
|
+
target literal). Decided by backtracking search over literal assignments.
|
|
502
|
+
|
|
503
|
+
Only the boolean answer is observable here (not which σ was found), and
|
|
504
|
+
that answer is a pure function of the two clauses' CONTENT regardless of
|
|
505
|
+
what order literals are tried internally — an existential search visits
|
|
506
|
+
every candidate assignment either way — so, unlike the resolvent/factor
|
|
507
|
+
generation order elsewhere in this module, no content-based sort is
|
|
508
|
+
needed for :func:`refute`'s determinism guarantee. Internally, target
|
|
509
|
+
literals are bucketed by ``(predicate, polarity)`` so each pattern
|
|
510
|
+
literal only ever considers candidates it could possibly match (a
|
|
511
|
+
predicate/polarity mismatch is rejected by an O(1) dict lookup instead of
|
|
512
|
+
a doomed call into :func:`_match_literal`), and pattern literals are tried
|
|
513
|
+
rarest-candidate-first so an unmatchable pattern literal fails fast
|
|
514
|
+
instead of being discovered deep in the recursion.
|
|
515
|
+
|
|
516
|
+
A subsumed clause is logically redundant: it is entailed by the subsuming
|
|
517
|
+
clause alone (an instance of a clause already follows from that clause),
|
|
518
|
+
so discarding it loses no logical content.
|
|
519
|
+
"""
|
|
520
|
+
pattern_lits = list(pattern_clause)
|
|
521
|
+
if not pattern_lits:
|
|
522
|
+
return True # the empty clause is a subset of every clause
|
|
523
|
+
|
|
524
|
+
buckets = {}
|
|
525
|
+
for lit in target_clause:
|
|
526
|
+
key = (_atom_of(lit).predicate, _is_positive(lit))
|
|
527
|
+
buckets.setdefault(key, []).append(lit)
|
|
528
|
+
|
|
529
|
+
def candidates(lit):
|
|
530
|
+
return buckets.get((_atom_of(lit).predicate, _is_positive(lit)), ())
|
|
531
|
+
|
|
532
|
+
# Necessary condition, checked once up front: every pattern literal needs
|
|
533
|
+
# SOME same-predicate-and-polarity candidate, or no assignment can exist.
|
|
534
|
+
if any(not candidates(lit) for lit in pattern_lits):
|
|
535
|
+
return False
|
|
536
|
+
pattern_lits.sort(key=lambda lit: len(candidates(lit)))
|
|
537
|
+
|
|
538
|
+
def backtrack(i: int, subst: dict) -> bool:
|
|
539
|
+
if i == len(pattern_lits):
|
|
540
|
+
return True
|
|
541
|
+
lit = pattern_lits[i]
|
|
542
|
+
for target_lit in candidates(lit):
|
|
543
|
+
extended = _match_literal(lit, target_lit, subst)
|
|
544
|
+
if extended is not None and backtrack(i + 1, extended):
|
|
545
|
+
return True
|
|
546
|
+
return False
|
|
547
|
+
|
|
548
|
+
return backtrack(0, {})
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
# Binary resolution
|
|
553
|
+
# ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
def _resolvents(clause1: frozenset, clause2: frozenset) -> list:
|
|
556
|
+
"""Return every binary resolvent of two (already standardized-apart) clauses.
|
|
557
|
+
|
|
558
|
+
For each positive/negative complementary pair — one literal an ``Atom A`` in
|
|
559
|
+
one clause, the other a ``Not(B)`` in the other clause with ``A``/``B`` the
|
|
560
|
+
same predicate and arity — the atoms are unified. On success with mgu σ the
|
|
561
|
+
resolvent is σ applied to ``(clause1 ∖ L) ∪ (clause2 ∖ M)``, as a frozenset.
|
|
562
|
+
"""
|
|
563
|
+
results = []
|
|
564
|
+
# Deterministic literal order (see _rename_clause): resolvent GENERATION
|
|
565
|
+
# order feeds the agenda, so it must not depend on frozenset hash order.
|
|
566
|
+
for lit1 in sorted(clause1, key=_lit_key):
|
|
567
|
+
for lit2 in sorted(clause2, key=_lit_key):
|
|
568
|
+
if _is_positive(lit1) == _is_positive(lit2):
|
|
569
|
+
continue # need complementary polarity
|
|
570
|
+
atom1 = _atom_of(lit1)
|
|
571
|
+
atom2 = _atom_of(lit2)
|
|
572
|
+
subst = unify(atom1, atom2)
|
|
573
|
+
if subst is None:
|
|
574
|
+
continue
|
|
575
|
+
rest1 = (lit for lit in clause1 if lit != lit1)
|
|
576
|
+
rest2 = (lit for lit in clause2 if lit != lit2)
|
|
577
|
+
resolvent = frozenset(
|
|
578
|
+
[_subst_literal(lit, subst) for lit in rest1]
|
|
579
|
+
+ [_subst_literal(lit, subst) for lit in rest2]
|
|
580
|
+
)
|
|
581
|
+
results.append(resolvent)
|
|
582
|
+
return results
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ---------------------------------------------------------------------------
|
|
586
|
+
# Factoring
|
|
587
|
+
# ---------------------------------------------------------------------------
|
|
588
|
+
|
|
589
|
+
def _factors(clause: frozenset) -> list:
|
|
590
|
+
"""Return every factor of a clause.
|
|
591
|
+
|
|
592
|
+
For each pair of same-polarity literals whose atoms unify, the clause is
|
|
593
|
+
instantiated by their mgu σ (merging the two literals) and the σ-image of the
|
|
594
|
+
whole clause is returned as a frozenset. Needed for completeness on inputs
|
|
595
|
+
where two literals must be identified before the empty clause can appear.
|
|
596
|
+
"""
|
|
597
|
+
results = []
|
|
598
|
+
literals = sorted(clause, key=_lit_key)
|
|
599
|
+
for i in range(len(literals)):
|
|
600
|
+
for j in range(i + 1, len(literals)):
|
|
601
|
+
lit_i, lit_j = literals[i], literals[j]
|
|
602
|
+
if _is_positive(lit_i) != _is_positive(lit_j):
|
|
603
|
+
continue # factoring needs equal polarity
|
|
604
|
+
subst = unify(_atom_of(lit_i), _atom_of(lit_j))
|
|
605
|
+
if subst is None:
|
|
606
|
+
continue
|
|
607
|
+
factored = frozenset(_subst_literal(lit, subst) for lit in clause)
|
|
608
|
+
results.append(factored)
|
|
609
|
+
return results
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ---------------------------------------------------------------------------
|
|
613
|
+
# Equality: a simple term order, subterm positions, paramodulation,
|
|
614
|
+
# reflexivity resolution, and demodulation.
|
|
615
|
+
# ---------------------------------------------------------------------------
|
|
616
|
+
|
|
617
|
+
def _is_equality_atom(atom: Atom) -> bool:
|
|
618
|
+
"""Return True iff atom is a binary ``=`` atom (a positive OR negative
|
|
619
|
+
equality literal wraps one of these — this only tests the atom itself,
|
|
620
|
+
call on ``_atom_of(literal)``)."""
|
|
621
|
+
return atom.predicate == "=" and len(atom.args) == 2
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _term_weight(term: Node) -> int:
|
|
625
|
+
"""The module's term order, part 1: WEIGHT, defined as term size.
|
|
626
|
+
|
|
627
|
+
A leaf (:class:`Variable`, :class:`Constant`, :class:`Number`) has
|
|
628
|
+
weight 1; a :class:`Function` application has weight ``1 +`` the sum of
|
|
629
|
+
its arguments' weights. :class:`Measure` and other non-Function compound
|
|
630
|
+
terms are treated as opaque leaves (weight 1) — this module's equality
|
|
631
|
+
reasoning does not descend into them (see :func:`_subterm_positions`).
|
|
632
|
+
"""
|
|
633
|
+
if isinstance(term, Function):
|
|
634
|
+
return 1 + sum(_term_weight(a) for a in term.args)
|
|
635
|
+
return 1
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _term_order_key(term: Node):
|
|
639
|
+
"""The module's term order, part 2: ties in weight are broken
|
|
640
|
+
LEXICOGRAPHICALLY by the term's rendering with every constant written by its bare name
|
|
641
|
+
(``key_text``, plain Python string comparison: the quotes of a quoted constant play no
|
|
642
|
+
part). Returns the ``(weight, rendering)`` pair compared by :func:`_term_gt`.
|
|
643
|
+
"""
|
|
644
|
+
return (_term_weight(term), key_text(term))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _term_gt(s: Node, t: Node) -> bool:
|
|
648
|
+
"""Return True iff ``s`` is STRICTLY greater than ``t`` under this
|
|
649
|
+
module's term order: compare :func:`_term_order_key` pairs (weight
|
|
650
|
+
first, then the lexicographic tie-break). This order is total up to
|
|
651
|
+
genuine term equality — two DISTINCT terms are, barring a rendering
|
|
652
|
+
collision, always strictly ordered one way or the other, so
|
|
653
|
+
"incomparable" (neither ``_term_gt(s, t)`` nor ``_term_gt(t, s)``) only
|
|
654
|
+
arises when ``s`` and ``t`` are, for the order's purposes, the same term.
|
|
655
|
+
|
|
656
|
+
Soundness note (why no substitution-compatibility closure is needed):
|
|
657
|
+
every USE of this order in this module — orienting a paramodulation
|
|
658
|
+
direction, or confirming a demodulation rewrite — compares two already
|
|
659
|
+
fully- or partially-INSTANTIATED terms directly (e.g. ``lσ`` versus
|
|
660
|
+
``rσ`` for the concrete σ actually used), never an abstract rule "before"
|
|
661
|
+
substitution. A real term-rewriting system needs its order to be stable
|
|
662
|
+
under substitution and monotone under context (the standard KBO/LPO
|
|
663
|
+
closure properties) to prove a whole many-rule system terminates; this
|
|
664
|
+
module sidesteps that requirement by re-checking the concrete inequality
|
|
665
|
+
at each individual rewrite instead of trusting it to persist abstractly.
|
|
666
|
+
A single demodulation step is still guaranteed to terminate the fixpoint
|
|
667
|
+
loop that applies it repeatedly (:func:`_demodulate_to_fixpoint`),
|
|
668
|
+
because replacing a subterm by a strictly lighter one strictly decreases
|
|
669
|
+
the WEIGHT of every ancestor up to the literal's atom (weight is
|
|
670
|
+
additive over immediate children), hence the clause's total weight — a
|
|
671
|
+
bounded-below natural number, so no infinite descending chain exists.
|
|
672
|
+
"""
|
|
673
|
+
return _term_order_key(s) > _term_order_key(t)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _paramodulation_directions(u: Node, v: Node):
|
|
677
|
+
"""Return the list of ``(from, to)`` direction pairs licensed for the
|
|
678
|
+
equation ``u ≈ v`` by the term order: paramodulate only from the
|
|
679
|
+
order-GREATER side into the smaller one when :func:`_term_gt` strictly
|
|
680
|
+
decides; if neither side is greater (only ``u`` and ``v`` being the same
|
|
681
|
+
term under the order, see its docstring), try both directions. This is a
|
|
682
|
+
pure SEARCH-SPACE restriction — paramodulation is unconditionally sound
|
|
683
|
+
in either direction regardless of what the order says (see the module
|
|
684
|
+
docstring); dropping a direction here only ever costs completeness, never
|
|
685
|
+
soundness.
|
|
686
|
+
"""
|
|
687
|
+
if _term_gt(u, v):
|
|
688
|
+
return [(u, v)]
|
|
689
|
+
if _term_gt(v, u):
|
|
690
|
+
return [(v, u)]
|
|
691
|
+
return [(u, v), (v, u)]
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _subterm_positions(atom: Atom):
|
|
695
|
+
"""Yield every position (a non-empty tuple of argument indices) that
|
|
696
|
+
addresses a non-Variable subterm reachable from atom's arguments, in
|
|
697
|
+
pre-order, left to right, descending into :class:`Function` arguments.
|
|
698
|
+
|
|
699
|
+
A position addresses a Function application itself (before descending
|
|
700
|
+
into its arguments) as well as every Constant/Number leaf; a Variable
|
|
701
|
+
position is never yielded — paramodulating INTO a bare variable
|
|
702
|
+
occurrence is a standard, deliberate restriction (redundant with
|
|
703
|
+
instantiation, and a large source of unproductive branching for no
|
|
704
|
+
completeness gain worth having in a didactic prover — see the module
|
|
705
|
+
docstring's honesty note). Non-Function compound terms (e.g.
|
|
706
|
+
:class:`Measure`) are treated as opaque leaves — reachable as a position
|
|
707
|
+
themselves, but not descended into — equality reasoning here is scoped
|
|
708
|
+
to the ordinary Function/Atom term language.
|
|
709
|
+
"""
|
|
710
|
+
def walk(term, prefix):
|
|
711
|
+
if isinstance(term, Function):
|
|
712
|
+
yield prefix
|
|
713
|
+
for i, arg in enumerate(term.args):
|
|
714
|
+
yield from walk(arg, prefix + (i,))
|
|
715
|
+
elif not isinstance(term, Variable):
|
|
716
|
+
yield prefix
|
|
717
|
+
for i, arg in enumerate(atom.args):
|
|
718
|
+
yield from walk(arg, (i,))
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _term_at(node: Node, position) -> Node:
|
|
722
|
+
"""Walk position (a tuple of argument indices, as yielded by
|
|
723
|
+
:func:`_subterm_positions`) from node down to the addressed subterm."""
|
|
724
|
+
for i in position:
|
|
725
|
+
node = node.args[i]
|
|
726
|
+
return node
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _replace_at(node: Node, position, replacement: Node) -> Node:
|
|
730
|
+
"""Return a copy of node (an :class:`Atom` or :class:`Function`) with
|
|
731
|
+
the subterm at position replaced by replacement. ``position == ()``
|
|
732
|
+
replaces node itself."""
|
|
733
|
+
if not position:
|
|
734
|
+
return replacement
|
|
735
|
+
i, rest = position[0], position[1:]
|
|
736
|
+
new_args = list(node.args)
|
|
737
|
+
new_args[i] = _replace_at(node.args[i], rest, replacement)
|
|
738
|
+
if isinstance(node, Atom):
|
|
739
|
+
return Atom(node.predicate, new_args)
|
|
740
|
+
return Function(node.name, new_args)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def _paramodulate_target(eq_clause, eq_lit, frm: Node, to: Node, target_clause, tgt_lit):
|
|
744
|
+
"""Return every one-step paramodulant of using ``eq_lit`` (``frm ≈ to``,
|
|
745
|
+
one already-chosen direction) from eq_clause to rewrite tgt_lit's atom
|
|
746
|
+
within target_clause, at every unifiable subterm position of tgt_lit.
|
|
747
|
+
|
|
748
|
+
Core of :func:`_paramodulants_cross` — kept clause-set-agnostic (it just
|
|
749
|
+
returns ``(new_target_literal, sigma)`` pairs for the caller to
|
|
750
|
+
assemble), which is also what :func:`_demodulate_once`'s cousin logic
|
|
751
|
+
mirrors on the matching side.
|
|
752
|
+
"""
|
|
753
|
+
results = []
|
|
754
|
+
tgt_atom = _atom_of(tgt_lit)
|
|
755
|
+
for pos in _subterm_positions(tgt_atom):
|
|
756
|
+
subterm = _term_at(tgt_atom, pos)
|
|
757
|
+
sigma = unify(frm, subterm)
|
|
758
|
+
if sigma is None:
|
|
759
|
+
continue
|
|
760
|
+
new_atom = _replace_at(tgt_atom, pos, to)
|
|
761
|
+
new_lit = new_atom if _is_positive(tgt_lit) else Not(new_atom)
|
|
762
|
+
results.append((new_lit, sigma))
|
|
763
|
+
return results
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _paramodulants_cross(eq_clause: frozenset, target_clause: frozenset) -> list:
|
|
767
|
+
"""Return every cross-clause paramodulant using a positive equality
|
|
768
|
+
literal of eq_clause to rewrite a literal of target_clause.
|
|
769
|
+
|
|
770
|
+
Mirrors :func:`_resolvents`'s calling convention: both clauses must
|
|
771
|
+
already be standardized apart by the caller (a soundness requirement —
|
|
772
|
+
the two clauses' variables must not be conflated), and ``target_clause``
|
|
773
|
+
may be a differently-renamed copy of the very same logical clause as
|
|
774
|
+
``eq_clause`` (the caller's given/kept-list loop already relies on this
|
|
775
|
+
for binary resolution's own "resolve a clause against itself" case; the
|
|
776
|
+
same renaming makes cross-clause paramodulation of a clause against
|
|
777
|
+
itself sound too). For each positive equality literal ``s ≈ t`` in
|
|
778
|
+
eq_clause and each direction :func:`_paramodulation_directions` allows,
|
|
779
|
+
every literal of target_clause is tried at every subterm position (see
|
|
780
|
+
:func:`_subterm_positions`); on a successful unification with mgu σ, the
|
|
781
|
+
paramodulant is σ applied to ``(eq_clause ∖ {s≈t}) ∪ (target_clause ∖
|
|
782
|
+
{L}) ∪ {L[p ↦ t]}``.
|
|
783
|
+
"""
|
|
784
|
+
results = []
|
|
785
|
+
eq_lits = [l for l in sorted(eq_clause, key=_lit_key)
|
|
786
|
+
if _is_positive(l) and _is_equality_atom(_atom_of(l))]
|
|
787
|
+
if not eq_lits:
|
|
788
|
+
return results
|
|
789
|
+
target_lits = sorted(target_clause, key=_lit_key)
|
|
790
|
+
for eq_lit in eq_lits:
|
|
791
|
+
u, v = _atom_of(eq_lit).args
|
|
792
|
+
for frm, to in _paramodulation_directions(u, v):
|
|
793
|
+
for tgt_lit in target_lits:
|
|
794
|
+
for new_lit, sigma in _paramodulate_target(
|
|
795
|
+
eq_clause, eq_lit, frm, to, target_clause, tgt_lit):
|
|
796
|
+
rest_eq = [l for l in eq_clause if l != eq_lit]
|
|
797
|
+
rest_tgt = [l for l in target_clause if l != tgt_lit]
|
|
798
|
+
results.append(frozenset(
|
|
799
|
+
[_subst_literal(l, sigma) for l in rest_eq]
|
|
800
|
+
+ [_subst_literal(l, sigma) for l in rest_tgt]
|
|
801
|
+
+ [_subst_literal(new_lit, sigma)]
|
|
802
|
+
))
|
|
803
|
+
return results
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _reflexivity_resolvents(clause: frozenset) -> list:
|
|
807
|
+
"""Return every reflexivity resolvent of clause: for each NEGATIVE
|
|
808
|
+
equality literal ``¬(u ≈ v)`` whose two sides unify (mgu σ), the literal
|
|
809
|
+
is dropped and σ applied to the rest — the mechanism that closes goals
|
|
810
|
+
such as ``⊢ c=c`` (whose negation clausifies to ``{¬(c=c)}``, unified by
|
|
811
|
+
the trivial mgu ``{}``) or, more generally, any clause carrying a
|
|
812
|
+
negated equation between two terms that are really the same up to
|
|
813
|
+
instantiation.
|
|
814
|
+
"""
|
|
815
|
+
results = []
|
|
816
|
+
for lit in sorted(clause, key=_lit_key):
|
|
817
|
+
if _is_positive(lit):
|
|
818
|
+
continue
|
|
819
|
+
atom = _atom_of(lit)
|
|
820
|
+
if not _is_equality_atom(atom):
|
|
821
|
+
continue
|
|
822
|
+
u, v = atom.args
|
|
823
|
+
sigma = unify(u, v)
|
|
824
|
+
if sigma is None:
|
|
825
|
+
continue
|
|
826
|
+
rest = frozenset(_subst_literal(l, sigma) for l in clause if l != lit)
|
|
827
|
+
results.append(rest)
|
|
828
|
+
return results
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
# Demodulation fixpoint loop: defensively capped, though termination is
|
|
832
|
+
# already guaranteed without it (see _term_gt's soundness note) — same
|
|
833
|
+
# spirit as _SUBSUMPTION_PATTERN_CAP below, a belt-and-braces bound on a
|
|
834
|
+
# process that is mathematically guaranteed to terminate anyway.
|
|
835
|
+
_DEMODULATION_ITERATION_CAP = 100
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _unit_rewrite_rules(clauses) -> list:
|
|
839
|
+
"""Return the oriented ``(l, r)`` demodulation rules licensed by the
|
|
840
|
+
UNIT (single-literal) positive equality clauses among clauses.
|
|
841
|
+
|
|
842
|
+
A non-unit clause ``{u≈v, Q}`` is never used as a rewrite rule: it only
|
|
843
|
+
asserts ``u≈v ∨ Q``, not the unconditional fact ``u≈v`` — using it to
|
|
844
|
+
rewrite unconditionally would be unsound. An equation whose two sides
|
|
845
|
+
are the same term under :func:`_term_gt` (so neither is strictly
|
|
846
|
+
greater) contributes no rule — rewriting a term to itself is vacuous and
|
|
847
|
+
an unoriented rule cannot be checked for termination.
|
|
848
|
+
"""
|
|
849
|
+
rules = []
|
|
850
|
+
for clause in clauses:
|
|
851
|
+
if len(clause) != 1:
|
|
852
|
+
continue
|
|
853
|
+
(lit,) = tuple(clause)
|
|
854
|
+
if not _is_positive(lit):
|
|
855
|
+
continue
|
|
856
|
+
atom = _atom_of(lit)
|
|
857
|
+
if not _is_equality_atom(atom):
|
|
858
|
+
continue
|
|
859
|
+
u, v = atom.args
|
|
860
|
+
if _term_gt(u, v):
|
|
861
|
+
rules.append((u, v))
|
|
862
|
+
elif _term_gt(v, u):
|
|
863
|
+
rules.append((v, u))
|
|
864
|
+
return rules
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
def _demodulate_once(clause: frozenset, rules: list):
|
|
868
|
+
"""Try one demodulation rewrite of clause using rules (oriented ``(l,
|
|
869
|
+
r)`` pairs, as :func:`_unit_rewrite_rules` produces). Returns the
|
|
870
|
+
rewritten clause, or None if no rule applies.
|
|
871
|
+
|
|
872
|
+
A rule applies at a literal's subterm (found via :func:`_subterm_positions`)
|
|
873
|
+
when ``l`` one-sidedly MATCHES it (:func:`_match_term` — the rule's own
|
|
874
|
+
variables bind, the target clause's variables are held fixed, unlike
|
|
875
|
+
paramodulation's full unification) AND the concrete orientation
|
|
876
|
+
``subterm ≻ rσ`` holds under :func:`_term_gt` for the match's σ — checked
|
|
877
|
+
on the instantiated terms, not merely inherited from the rule's abstract
|
|
878
|
+
orientation (see :func:`_term_gt`'s soundness note). The rule and the clause
|
|
879
|
+
are not standardized apart, so the rule ``f(x, y) → g(x)`` matched against
|
|
880
|
+
``f(y, z)`` gives ``{x: y, y: z}`` (and ``{y: y}`` or ``{x: y, y: x}`` for
|
|
881
|
+
other clauses): the images are terms of the clause, never looked up again, so
|
|
882
|
+
the matcher is applied to ``r`` in ONE simultaneous step
|
|
883
|
+
(:func:`_rename_term`), the instance being ``g(y)``, as the proof checkers
|
|
884
|
+
(:func:`~unicode_logic_kit.atp.resolution_check._apply_matcher`) compute it.
|
|
885
|
+
Literals and
|
|
886
|
+
positions are visited in a fixed, content-determined order (mirroring
|
|
887
|
+
every other generator in this module) so which of several possible
|
|
888
|
+
rewrites fires is reproducible run to run.
|
|
889
|
+
"""
|
|
890
|
+
for lit in sorted(clause, key=_lit_key):
|
|
891
|
+
atom = _atom_of(lit)
|
|
892
|
+
for pos in _subterm_positions(atom):
|
|
893
|
+
subterm = _term_at(atom, pos)
|
|
894
|
+
for l, r in rules:
|
|
895
|
+
sigma = _match_term(l, subterm, {})
|
|
896
|
+
if sigma is None:
|
|
897
|
+
continue
|
|
898
|
+
r_sigma = _rename_term(r, sigma)
|
|
899
|
+
if not _term_gt(subterm, r_sigma):
|
|
900
|
+
continue
|
|
901
|
+
new_atom = _replace_at(atom, pos, r_sigma)
|
|
902
|
+
new_lit = new_atom if _is_positive(lit) else Not(new_atom)
|
|
903
|
+
return frozenset([new_lit] + [other for other in clause if other != lit])
|
|
904
|
+
return None
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _demodulate_to_fixpoint(clause: frozenset, rules: list,
|
|
908
|
+
cap: int = _DEMODULATION_ITERATION_CAP):
|
|
909
|
+
"""Repeatedly apply :func:`_demodulate_once` to clause until no rule
|
|
910
|
+
applies (a simplification fixpoint) or cap rewrites have fired. Returns
|
|
911
|
+
``(simplified_clause, rewrite_count)`` — the caller (:func:`refute`)
|
|
912
|
+
charges rewrite_count against the overall step budget, so demodulation
|
|
913
|
+
is never a free/silent rewrite (see the module docstring).
|
|
914
|
+
"""
|
|
915
|
+
count = 0
|
|
916
|
+
while count < cap:
|
|
917
|
+
rewritten = _demodulate_once(clause, rules)
|
|
918
|
+
if rewritten is None:
|
|
919
|
+
break
|
|
920
|
+
clause = rewritten
|
|
921
|
+
count += 1
|
|
922
|
+
return clause, count
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
# ---------------------------------------------------------------------------
|
|
926
|
+
# Saturation
|
|
927
|
+
# ---------------------------------------------------------------------------
|
|
928
|
+
|
|
929
|
+
# A clause acting as a subsumption PATTERN (the candidate subsumer, on either
|
|
930
|
+
# side of the check) is only considered when it has at most this many
|
|
931
|
+
# literals. Subsumption by a short clause — unit and binary clauses above all
|
|
932
|
+
# — is where almost all practical redundancy in a resolution search lives
|
|
933
|
+
# (an instantiated fact or rule absorbing padded/weakened copies of itself,
|
|
934
|
+
# exactly the shape :func:`refute`'s docstring measurable-case test uses).
|
|
935
|
+
# Larger clauses generally originate from problem-specific structure that is
|
|
936
|
+
# rarely literally repeated, so extending the search to them buys little
|
|
937
|
+
# extra pruning while its worst-case cost (a per-clause O(kept-set) scan) is
|
|
938
|
+
# what it costs regardless of pattern size. Honesty note: this makes forward
|
|
939
|
+
# and backward subsumption a BOUNDED, not exhaustive, redundancy check — a
|
|
940
|
+
# clause subsumed only by a longer pattern may survive. That never affects
|
|
941
|
+
# soundness or completeness (see the completeness argument below); it only
|
|
942
|
+
# means some redundant clauses are not pruned, exactly like hitting the step
|
|
943
|
+
# bound leaves some resolvents unexplored.
|
|
944
|
+
_SUBSUMPTION_PATTERN_CAP = 3
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def refute(clauses, max_steps: int = 10000, timeout: Optional[float] = None) -> bool:
|
|
948
|
+
"""Return True iff the clause set is unsatisfiable (empty clause derivable).
|
|
949
|
+
|
|
950
|
+
``timeout`` (milliseconds, default none) is a second bound next to ``max_steps``: the
|
|
951
|
+
clock is read for every candidate clause and for every kept clause the given clause is
|
|
952
|
+
paired with, and once it has run out the call returns False ("not refuted within the
|
|
953
|
+
bound") within one such pairing's work. The call as a whole is also cut off at the
|
|
954
|
+
deadline, so the preparation of a very large clause set (sorting, the subsumption of
|
|
955
|
+
the seed clauses) cannot outlast it either.
|
|
956
|
+
|
|
957
|
+
Runs given-clause saturation: resolution between the new clause and every
|
|
958
|
+
previously kept clause, plus factoring of the new clause, PLUS (see the
|
|
959
|
+
module docstring's "Equality" section) reflexivity resolution and
|
|
960
|
+
self-paramodulation of the new clause, and cross-clause paramodulation
|
|
961
|
+
between it and every kept clause in both roles (equation-supplier /
|
|
962
|
+
rewrite-target) — with clause pairs standardized apart before each
|
|
963
|
+
cross-clause step, exactly as for resolution. Every newly generated
|
|
964
|
+
candidate is demodulated to a simplification fixpoint (using the
|
|
965
|
+
currently kept UNIT equations as oriented rewrite rules) before being
|
|
966
|
+
tested for redundancy and possibly kept — each individual demodulation
|
|
967
|
+
rewrite is charged against ``max_steps`` too, just like every other
|
|
968
|
+
inference here. Returns True as soon as the empty clause (``frozenset()``)
|
|
969
|
+
is derived; False if the set saturates with no new clause; and False
|
|
970
|
+
(conservatively, "not refuted within the bound") if ``max_steps``
|
|
971
|
+
inference/demodulation steps are taken first. Soundness rests on the
|
|
972
|
+
empty clause genuinely witnessing unsatisfiability, so True is only ever
|
|
973
|
+
returned when ``frozenset()`` is actually derived — paramodulation,
|
|
974
|
+
reflexivity resolution, and demodulation are all unconditionally sound
|
|
975
|
+
(see the module docstring), so adding them costs nothing on that front;
|
|
976
|
+
completeness for equality problems is explicitly NOT claimed beyond
|
|
977
|
+
what small problems reach within the bound (same module docstring).
|
|
978
|
+
|
|
979
|
+
Two redundancy eliminations are applied throughout (tautology deletion and
|
|
980
|
+
subsumption, see :func:`_is_tautology` / :func:`_subsumes`):
|
|
981
|
+
|
|
982
|
+
- A tautologous clause (containing a literal and its exact complement) is
|
|
983
|
+
never kept — not even as a seed clause.
|
|
984
|
+
- Forward subsumption: a newly generated clause D is discarded outright if
|
|
985
|
+
some already-kept clause C subsumes it (only attempted when
|
|
986
|
+
``len(C) <= len(D)``, the necessary case this search targets, AND
|
|
987
|
+
``len(C) <= _SUBSUMPTION_PATTERN_CAP`` — see that constant's docstring
|
|
988
|
+
for why the subsuming side is deliberately bounded).
|
|
989
|
+
- Backward subsumption: keeping a new clause D retires every already-kept
|
|
990
|
+
clause that D subsumes (same restrictions — D itself must be short
|
|
991
|
+
enough to act as a pattern), including agenda entries not yet
|
|
992
|
+
processed — the agenda-pop loop below checks membership in ``kept``
|
|
993
|
+
and silently skips an entry removed this way.
|
|
994
|
+
|
|
995
|
+
Completeness is preserved FOR THE EQUALITY-FREE FRAGMENT: resolution +
|
|
996
|
+
factoring is refutation-complete there (Robinson's theorem — any
|
|
997
|
+
unsatisfiable equality-free clause set has a derivation of the empty
|
|
998
|
+
clause by resolution and factoring alone), and both eliminations only
|
|
999
|
+
ever drop a clause that is redundant given what remains kept — this
|
|
1000
|
+
argument is UNCHANGED by adding the equality rules (they only ever add
|
|
1001
|
+
more ways to derive a clause, never remove one that resolution/factoring
|
|
1002
|
+
alone would have produced). For clause sets that use equality, the
|
|
1003
|
+
module docstring's honesty note applies: paramodulation is sound but
|
|
1004
|
+
this implementation does not claim the additional completeness
|
|
1005
|
+
refinements a serious equality prover needs. A tautology
|
|
1006
|
+
is valid in every model, so it can never itself contribute to deriving the
|
|
1007
|
+
empty clause (which certifies unsatisfiability) — it can only ever resolve
|
|
1008
|
+
away to something already derivable without it. A subsumed clause D is a
|
|
1009
|
+
strict logical consequence of the subsuming clause C alone (Cσ ⊆ D means
|
|
1010
|
+
every model of C is a model of D), so anything the saturation could
|
|
1011
|
+
eventually derive using D remains derivable using C in D's place; dropping
|
|
1012
|
+
D (or clauses D itself later subsumes) cannot make an unsatisfiable set
|
|
1013
|
+
appear satisfiable to the search. This is the standard redundancy-elimination
|
|
1014
|
+
result for resolution (tautology deletion and subsumption are both
|
|
1015
|
+
special cases of the general redundancy criterion under which saturation
|
|
1016
|
+
remains complete). The bool return, signature, and step-counting semantics
|
|
1017
|
+
are unchanged from the plain-saturation version.
|
|
1018
|
+
|
|
1019
|
+
``clauses`` is any iterable of frozensets of literals; it is not mutated.
|
|
1020
|
+
"""
|
|
1021
|
+
deadline = _instant(timeout)
|
|
1022
|
+
finished, refuted = _run_until(deadline, lambda: _saturate(clauses, max_steps, deadline))
|
|
1023
|
+
return bool(finished and refuted)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _saturate(clauses, max_steps: int, deadline: Optional[float]) -> bool:
|
|
1027
|
+
"""The saturation of :func:`refute`, which runs it under ``deadline`` (a ``perf_counter``
|
|
1028
|
+
instant, or ``None``): it reads the clock itself for every candidate and every kept
|
|
1029
|
+
clause, and the whole run is cut off at the deadline besides, so the sorting and the
|
|
1030
|
+
subsumption of the seed clauses, which no step accounts for, cannot outlast it."""
|
|
1031
|
+
# Insertion-ordered working structures (kept_list mirrors the kept set):
|
|
1032
|
+
# processing order must be a function of the INPUT, not of hash seeds, or
|
|
1033
|
+
# "proved within max_steps" varies between runs of the same call. Seed
|
|
1034
|
+
# tautologies are filtered before dedup/sort so they never enter kept.
|
|
1035
|
+
seed = sorted((c for c in dict.fromkeys(clauses) if not _is_tautology(c)),
|
|
1036
|
+
key=lambda c: (len(c), sorted(_lit_key(l) for l in c)))
|
|
1037
|
+
if frozenset() in seed:
|
|
1038
|
+
return True
|
|
1039
|
+
|
|
1040
|
+
counter = [0]
|
|
1041
|
+
kept = set()
|
|
1042
|
+
kept_list = []
|
|
1043
|
+
# Kept clauses bucketed by literal count: subsumption is only ever
|
|
1044
|
+
# attempted between a pattern no longer than its target (len(C) <=
|
|
1045
|
+
# len(D)), so indexing by length lets both sweeps below visit only the
|
|
1046
|
+
# buckets that could possibly participate, instead of the whole kept set
|
|
1047
|
+
# — the necessary optimisation once kept grows into the thousands.
|
|
1048
|
+
kept_by_length = {}
|
|
1049
|
+
agenda = []
|
|
1050
|
+
steps = 0
|
|
1051
|
+
|
|
1052
|
+
def _forward_subsumed(clause) -> bool:
|
|
1053
|
+
"""True iff some short kept clause (no longer than clause) subsumes it."""
|
|
1054
|
+
target_len = len(clause)
|
|
1055
|
+
for bucket_len, bucket in kept_by_length.items():
|
|
1056
|
+
if bucket_len <= target_len and bucket_len <= _SUBSUMPTION_PATTERN_CAP:
|
|
1057
|
+
for c in bucket:
|
|
1058
|
+
if _subsumes(c, clause):
|
|
1059
|
+
return True
|
|
1060
|
+
return False
|
|
1061
|
+
|
|
1062
|
+
def _keep(clause):
|
|
1063
|
+
# Backward subsumption FIRST: retire every currently kept clause that
|
|
1064
|
+
# the new clause subsumes (only those no shorter than the new
|
|
1065
|
+
# clause). ``clause`` is not yet indexed, so no self-comparison risk.
|
|
1066
|
+
# Skipped entirely when the new clause itself is too long to be a
|
|
1067
|
+
# capped subsuming pattern (see _SUBSUMPTION_PATTERN_CAP below).
|
|
1068
|
+
pattern_len = len(clause)
|
|
1069
|
+
if pattern_len <= _SUBSUMPTION_PATTERN_CAP:
|
|
1070
|
+
for bucket_len in list(kept_by_length):
|
|
1071
|
+
if bucket_len < pattern_len:
|
|
1072
|
+
continue
|
|
1073
|
+
bucket = kept_by_length[bucket_len]
|
|
1074
|
+
subsumed = [c for c in bucket if _subsumes(clause, c)]
|
|
1075
|
+
for other in subsumed:
|
|
1076
|
+
kept.discard(other)
|
|
1077
|
+
kept_list.remove(other)
|
|
1078
|
+
bucket.remove(other)
|
|
1079
|
+
if not bucket:
|
|
1080
|
+
del kept_by_length[bucket_len]
|
|
1081
|
+
kept.add(clause)
|
|
1082
|
+
kept_list.append(clause)
|
|
1083
|
+
kept_by_length.setdefault(pattern_len, []).append(clause)
|
|
1084
|
+
agenda.append(clause)
|
|
1085
|
+
|
|
1086
|
+
def _consider(clause) -> bool:
|
|
1087
|
+
"""Try to add a newly generated clause; True iff it is the empty
|
|
1088
|
+
clause (the caller must report refutation immediately)."""
|
|
1089
|
+
if clause == frozenset():
|
|
1090
|
+
return True
|
|
1091
|
+
if not _is_tautology(clause) and clause not in kept and not _forward_subsumed(clause):
|
|
1092
|
+
_keep(clause)
|
|
1093
|
+
return False
|
|
1094
|
+
|
|
1095
|
+
def _process(candidate) -> bool:
|
|
1096
|
+
"""Account one freshly generated candidate clause against the step
|
|
1097
|
+
budget, demodulate it to a simplification fixpoint under the
|
|
1098
|
+
CURRENTLY kept unit equations (forward demodulation only — see the
|
|
1099
|
+
module docstring; ``kept_list`` at call time is exactly "currently
|
|
1100
|
+
kept"), and try to keep the result. Returns True iff the empty
|
|
1101
|
+
clause was reached. The caller must still check ``steps >=
|
|
1102
|
+
max_steps`` right after calling this, exactly like every existing
|
|
1103
|
+
per-candidate site below — demodulation's own rewrites are charged
|
|
1104
|
+
into the same nonlocal ``steps`` counter, so a candidate that takes
|
|
1105
|
+
many rewrites to simplify can itself exhaust the budget.
|
|
1106
|
+
"""
|
|
1107
|
+
nonlocal steps
|
|
1108
|
+
steps += 1
|
|
1109
|
+
if deadline is not None and time.perf_counter() > deadline:
|
|
1110
|
+
steps = max_steps # the callers' step check then ends the search
|
|
1111
|
+
return False
|
|
1112
|
+
rules = _unit_rewrite_rules(kept_list)
|
|
1113
|
+
simplified, rewrite_count = _demodulate_to_fixpoint(
|
|
1114
|
+
candidate, rules, cap=min(_DEMODULATION_ITERATION_CAP, max(0, max_steps - steps)))
|
|
1115
|
+
steps += rewrite_count
|
|
1116
|
+
return _consider(simplified)
|
|
1117
|
+
|
|
1118
|
+
for clause in seed:
|
|
1119
|
+
# Empty-clause seeds already returned above; ordinary seeds only need
|
|
1120
|
+
# forward subsumption (nothing kept yet can be backward-subsumed by a
|
|
1121
|
+
# clause not yet added, so _keep's own sweep is what matters here).
|
|
1122
|
+
# Seeds are NOT demodulated (see the module docstring) — only clauses
|
|
1123
|
+
# generated during saturation pass through _process.
|
|
1124
|
+
if clause not in kept and not _forward_subsumed(clause):
|
|
1125
|
+
_keep(clause)
|
|
1126
|
+
|
|
1127
|
+
while agenda:
|
|
1128
|
+
if deadline is not None and time.perf_counter() > deadline:
|
|
1129
|
+
return False
|
|
1130
|
+
given = agenda.pop(0)
|
|
1131
|
+
if given not in kept:
|
|
1132
|
+
continue # removed by backward subsumption after being queued
|
|
1133
|
+
|
|
1134
|
+
# Factor the given clause against itself.
|
|
1135
|
+
for factor in _factors(given):
|
|
1136
|
+
if _process(factor):
|
|
1137
|
+
return True
|
|
1138
|
+
if steps >= max_steps:
|
|
1139
|
+
return False
|
|
1140
|
+
|
|
1141
|
+
# Reflexivity-resolve the given clause against itself (shared
|
|
1142
|
+
# variables, no renaming — see its docstring). Paramodulation of a
|
|
1143
|
+
# clause into itself deliberately has NO shared-variable shortcut
|
|
1144
|
+
# (unsound — see the module docstring); it happens below via the
|
|
1145
|
+
# renamed-copy cross path only.
|
|
1146
|
+
for candidate in _reflexivity_resolvents(given):
|
|
1147
|
+
if _process(candidate):
|
|
1148
|
+
return True
|
|
1149
|
+
if steps >= max_steps:
|
|
1150
|
+
return False
|
|
1151
|
+
|
|
1152
|
+
# Resolve/paramodulate the given clause against every kept clause
|
|
1153
|
+
# (including itself, via renaming).
|
|
1154
|
+
for other in list(kept_list):
|
|
1155
|
+
if other not in kept:
|
|
1156
|
+
continue # backward-subsumed by an earlier resolvent this round
|
|
1157
|
+
if deadline is not None and time.perf_counter() > deadline:
|
|
1158
|
+
return False
|
|
1159
|
+
r_given = _rename_clause(given, counter)
|
|
1160
|
+
r_other = _rename_clause(other, counter)
|
|
1161
|
+
for resolvent in _resolvents(r_given, r_other):
|
|
1162
|
+
if _process(resolvent):
|
|
1163
|
+
return True
|
|
1164
|
+
if steps >= max_steps:
|
|
1165
|
+
return False
|
|
1166
|
+
# Paramodulation is directional (which clause supplies the
|
|
1167
|
+
# equation versus the rewrite target), unlike resolution's
|
|
1168
|
+
# symmetric literal scan, so both roles are tried explicitly.
|
|
1169
|
+
for candidate in _paramodulants_cross(r_given, r_other):
|
|
1170
|
+
if _process(candidate):
|
|
1171
|
+
return True
|
|
1172
|
+
if steps >= max_steps:
|
|
1173
|
+
return False
|
|
1174
|
+
for candidate in _paramodulants_cross(r_other, r_given):
|
|
1175
|
+
if _process(candidate):
|
|
1176
|
+
return True
|
|
1177
|
+
if steps >= max_steps:
|
|
1178
|
+
return False
|
|
1179
|
+
|
|
1180
|
+
return False # saturated without deriving the empty clause
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
# ---------------------------------------------------------------------------
|
|
1184
|
+
# Entailment and validity
|
|
1185
|
+
# ---------------------------------------------------------------------------
|
|
1186
|
+
|
|
1187
|
+
def _translate_modal_inputs(premises, conclusion: Node):
|
|
1188
|
+
"""Return ``(lowered, first_order)`` for a modal entailment, or None.
|
|
1189
|
+
|
|
1190
|
+
``lowered`` is the classical FOL image; ``first_order`` says the quantified
|
|
1191
|
+
(qml) route produced it, so :func:`prove` can scale its step budget to the
|
|
1192
|
+
larger image. ``None`` means the input was classical all along.
|
|
1193
|
+
|
|
1194
|
+
When any input carries a modal/temporal/hybrid operator, the whole LOCAL
|
|
1195
|
+
consequence ``premises ⊢ conclusion`` is folded into one implication sharing
|
|
1196
|
+
a single free world variable and lowered with ``standard_translation`` — the
|
|
1197
|
+
same local reading :func:`~unicode_logic_kit.atp.modal_tableau.modal_prove`
|
|
1198
|
+
decides. The universal closure that :func:`prove` applies afterwards then
|
|
1199
|
+
quantifies that ONE world over the implication as a whole, which is exactly
|
|
1200
|
+
K-validity of the local consequence. Returns ``None`` for classical input.
|
|
1201
|
+
|
|
1202
|
+
A QUANTIFIED modal input (object quantifiers mixed with modalities) is
|
|
1203
|
+
lowered with the first-order shallow embedding instead
|
|
1204
|
+
(:func:`unicode_logic_kit.fol.qml.qml_translate` via its validity formula),
|
|
1205
|
+
under the same K frame and the ``constant``-domain regime — the FO-modal
|
|
1206
|
+
default of ``qml_is_valid``. For other frames or domain regimes
|
|
1207
|
+
(varying/increasing/decreasing) call ``qml_is_valid`` directly.
|
|
1208
|
+
|
|
1209
|
+
A SORTED constant ``c:S`` is an element of ``S`` at every world (a constant is a rigid
|
|
1210
|
+
designator), and the standard translation's guard atom ``S(c, w)`` does not say so. The
|
|
1211
|
+
propositional route therefore lowers the problem to ``membership → image``, with the
|
|
1212
|
+
rigid, unguarded membership ``∀v0 S(c, v0)`` of every sorted constant of the input taken
|
|
1213
|
+
from :func:`~unicode_logic_kit.fol.modal_translation.frame_axioms` — so ``□Human(carl:Human)``
|
|
1214
|
+
is proved, ``◇Human(carl:Human)`` is not (a world without successor) and
|
|
1215
|
+
``□Mortal(carl:Human)`` is not. The quantified route already carries the same fact among
|
|
1216
|
+
``qml``'s axioms.
|
|
1217
|
+
|
|
1218
|
+
Counterfactuals are guarded first: ``standard_translation`` predates them and
|
|
1219
|
+
its generic error would not name the sphere tools.
|
|
1220
|
+
|
|
1221
|
+
Raises:
|
|
1222
|
+
NotImplementedError: from ``standard_translation`` / ``qml`` on the
|
|
1223
|
+
genuinely non-first-order residue (Until/Since, hybrid nominals
|
|
1224
|
+
under quantifiers), with their documented reasons; or here for
|
|
1225
|
+
``□→``/``◇→`` with a pointer at the sphere semantics.
|
|
1226
|
+
"""
|
|
1227
|
+
from .modal_tableau import has_modal, _contains_counterfactual
|
|
1228
|
+
inputs = list(premises) + [conclusion]
|
|
1229
|
+
if not any(has_modal(f) for f in inputs):
|
|
1230
|
+
return None
|
|
1231
|
+
for f in inputs:
|
|
1232
|
+
if _contains_counterfactual(f):
|
|
1233
|
+
raise NotImplementedError(
|
|
1234
|
+
"resolution: the counterfactuals □→/◇→ have no first-order "
|
|
1235
|
+
"standard translation (they read a similarity ordering, not an "
|
|
1236
|
+
"accessibility relation); use cf_valid / cf_satisfies or "
|
|
1237
|
+
"isabelle_decide_counterfactual.")
|
|
1238
|
+
combined = conclusion
|
|
1239
|
+
for p in reversed(list(premises)):
|
|
1240
|
+
combined = Implies(p, combined)
|
|
1241
|
+
from ..fol.nodes import Quantifier, SortedQuantifier
|
|
1242
|
+
if any(isinstance(n, (Quantifier, SortedQuantifier)) for n in combined.walk()):
|
|
1243
|
+
# First-order modal logic: the propositional standard translation cannot
|
|
1244
|
+
# express object domains, but the FO shallow embedding can — same K
|
|
1245
|
+
# reading, constant domains (qml_is_valid's default).
|
|
1246
|
+
from ..fol.qml import _validity_formula
|
|
1247
|
+
return (_validity_formula(combined, "constant", "K"), True)
|
|
1248
|
+
from ..fol.modal_translation import standard_translation, frame_axioms
|
|
1249
|
+
image = standard_translation(combined)
|
|
1250
|
+
# The membership axioms are what frame_axioms returns for a formula that mentions no
|
|
1251
|
+
# relation and exactly these sorted constants; they join the image as hypotheses (never
|
|
1252
|
+
# conjoined onto it), in the translation's own vocabulary.
|
|
1253
|
+
constants = tuple(n for n in combined.walk() if isinstance(n, SortedConstant))
|
|
1254
|
+
if constants:
|
|
1255
|
+
for axiom in reversed(frame_axioms(Atom("P", constants))):
|
|
1256
|
+
image = Implies(axiom, image)
|
|
1257
|
+
return (image, False)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _refuse_cardinality(formulas) -> None:
|
|
1261
|
+
"""Refuse a problem that holds a cardinality term, by name.
|
|
1262
|
+
|
|
1263
|
+
``|{v : φ}|`` is a natural number that is counted in a structure. It is not a term of
|
|
1264
|
+
first-order logic, so it has no clause form, and reading it as an uninterpreted term would
|
|
1265
|
+
answer another question (``|{x : P(x)}| = |{x : Q(x)}|`` would not follow from
|
|
1266
|
+
``∀x (P(x) ↔ Q(x))``).
|
|
1267
|
+
|
|
1268
|
+
Raises:
|
|
1269
|
+
NotImplementedError: a cardinality term occurs in one of ``formulas``.
|
|
1270
|
+
"""
|
|
1271
|
+
for formula in formulas:
|
|
1272
|
+
for node in formula.walk():
|
|
1273
|
+
if isinstance(node, (Cardinality, SortedCardinality)):
|
|
1274
|
+
raise NotImplementedError(
|
|
1275
|
+
f"atp.resolution: the cardinality {node.to_unicode_str()} has no clause "
|
|
1276
|
+
"form. A cardinality |{v : φ}| is a natural number that is counted in a "
|
|
1277
|
+
"structure, not a term of first-order logic, so resolution does not decide "
|
|
1278
|
+
"a problem that holds one. A counting quantifier (∃≥n x φ, ∃≤n x φ, "
|
|
1279
|
+
"∃=n x φ) states a bound that resolution reads.")
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def prove(premises, conclusion: Node, max_steps: int = 10000,
|
|
1283
|
+
timeout: Optional[float] = None) -> bool:
|
|
1284
|
+
"""Return True iff ``premises`` entail ``conclusion`` (premises ⊨ conclusion).
|
|
1285
|
+
|
|
1286
|
+
``timeout`` (milliseconds, default none) bounds the whole call: the lowering of a
|
|
1287
|
+
modal input, the clausification of every source formula (a normal form can be
|
|
1288
|
+
exponentially larger than its formula, and that work is cut off at the deadline like
|
|
1289
|
+
any other) and then the saturation, which is handed what is left of it (see
|
|
1290
|
+
:func:`refute`). A call that ran out of time returns False, "not proved within the
|
|
1291
|
+
bound".
|
|
1292
|
+
|
|
1293
|
+
Decided by refutation. A variable that is free in a premise or in the
|
|
1294
|
+
conclusion is a PARAMETER of the problem: one unknown element, the same in
|
|
1295
|
+
every premise and in the conclusion (the consequence relation of the textbooks,
|
|
1296
|
+
``Γ ⊨ φ`` iff every structure AND assignment that satisfies ``Γ`` satisfies
|
|
1297
|
+
``φ``). It is replaced by a constant that no symbol of the problem has
|
|
1298
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`) before anything
|
|
1299
|
+
else is done, so ``P(x) ⊢ P(alpha)`` is not proved (universe ``{0, 1}``,
|
|
1300
|
+
``x`` ↦ 1, ``alpha`` ↦ 0, ``P`` = ``{1}``), while ``P(x) ⊢ ∃y P(y)`` and
|
|
1301
|
+
``∀y P(y) ⊢ P(x)`` are. A premise is never closed universally. For a problem
|
|
1302
|
+
without a premise the reading is the universal closure of the conclusion. The
|
|
1303
|
+
negation of the conclusion then has no free variable, so a parameter of the
|
|
1304
|
+
conclusion stays one fixed element under the negation. Each source formula is
|
|
1305
|
+
clausified independently and every clause is renamed apart, so the variable
|
|
1306
|
+
names from one source cannot collide with another's, and a Skolem symbol is
|
|
1307
|
+
fresh against every name of the whole problem. The clause sets
|
|
1308
|
+
are unioned and saturation is run. Returns True iff the empty clause is
|
|
1309
|
+
derived; False if the union saturates without it; and False (conservatively,
|
|
1310
|
+
"not proved within the bound") if ``max_steps`` is reached first — never
|
|
1311
|
+
reporting a non-theorem as proved.
|
|
1312
|
+
|
|
1313
|
+
Many-sorted input is read with the guard reading of
|
|
1314
|
+
:func:`~unicode_logic_kit.fol.nodes.to_fol` and gets the background facts that
|
|
1315
|
+
reading needs, as ordinary premises: :func:`~unicode_logic_kit.fol.nodes.sort_axioms`
|
|
1316
|
+
of the premises and the conclusion -- every sort is non-empty, and a sorted
|
|
1317
|
+
constant ``c:S`` lies in ``S``. A sorted constant is the same constant in
|
|
1318
|
+
every formula (``P(carl:S), ∀x (P(x) → Q(x)) ⊢ Q(carl:S)`` is proved). The
|
|
1319
|
+
facts are premises, never part of the negated conclusion. Still incomplete,
|
|
1320
|
+
like everything here: ``False`` means "not proved within the bound".
|
|
1321
|
+
|
|
1322
|
+
Raises:
|
|
1323
|
+
NotImplementedError: a premise or the conclusion holds a cardinality term
|
|
1324
|
+
``|{v : φ}|``, which has no clause form (the ``resolution`` backend answers
|
|
1325
|
+
``unknown`` / ``unsupported``).
|
|
1326
|
+
"""
|
|
1327
|
+
deadline = _instant(timeout)
|
|
1328
|
+
premises = list(premises)
|
|
1329
|
+
_refuse_cardinality(premises + [conclusion])
|
|
1330
|
+
finished, translated = _run_until(
|
|
1331
|
+
deadline, lambda: _translate_modal_inputs(premises, conclusion))
|
|
1332
|
+
if not finished:
|
|
1333
|
+
return False
|
|
1334
|
+
if translated is not None:
|
|
1335
|
+
lowered, first_order = translated
|
|
1336
|
+
# The FO shallow embedding's image (guard predicates + domain/frame
|
|
1337
|
+
# axioms as hypotheses) is an order of magnitude larger than the
|
|
1338
|
+
# propositional ST image; scale the step budget so the textbook
|
|
1339
|
+
# quantified-modal validities (Barcan / converse Barcan under constant
|
|
1340
|
+
# domains, ~200k steps) close under the default budget. False remains
|
|
1341
|
+
# "not proved within the bound", as everywhere in this module.
|
|
1342
|
+
return prove([], lowered, max_steps=max_steps * (20 if first_order else 1),
|
|
1343
|
+
timeout=_remaining_ms(deadline))
|
|
1344
|
+
|
|
1345
|
+
def clausify() -> set:
|
|
1346
|
+
counter = [0]
|
|
1347
|
+
sk_counter = [0]
|
|
1348
|
+
clause_set = set()
|
|
1349
|
+
closed, _ = parameterize(premises + [conclusion])
|
|
1350
|
+
given, goal = closed[:-1], closed[-1]
|
|
1351
|
+
sources = list(given)
|
|
1352
|
+
sources.extend(sort_axioms(*given, goal))
|
|
1353
|
+
sources.append(Not(goal))
|
|
1354
|
+
problem_names = symbol_names(*sources)
|
|
1355
|
+
for source in sources:
|
|
1356
|
+
for clause in to_clauses(source, sk_counter=sk_counter, avoid=problem_names):
|
|
1357
|
+
clause_set.add(_rename_clause(clause, counter))
|
|
1358
|
+
return clause_set
|
|
1359
|
+
|
|
1360
|
+
finished, clause_set = _run_until(deadline, clausify)
|
|
1361
|
+
if not finished or clause_set is None:
|
|
1362
|
+
return False
|
|
1363
|
+
return refute(clause_set, max_steps=max_steps, timeout=_remaining_ms(deadline))
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def is_valid_resolution(formula: Node, max_steps: int = 10000,
|
|
1367
|
+
timeout: Optional[int] = None) -> bool:
|
|
1368
|
+
"""Return True iff ``formula`` is valid (its negation is refutable).
|
|
1369
|
+
|
|
1370
|
+
Equivalent to ``prove([], formula)``: a formula is valid exactly when
|
|
1371
|
+
¬formula is unsatisfiable, which resolution decides by deriving the empty
|
|
1372
|
+
clause from the clauses of ¬formula. Incomplete under the bound: a return of
|
|
1373
|
+
False means "not shown valid within ``max_steps``", never a false claim of
|
|
1374
|
+
invalidity-as-validity.
|
|
1375
|
+
"""
|
|
1376
|
+
return prove([], formula, max_steps=max_steps, timeout=timeout)
|