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,581 @@
|
|
|
1
|
+
"""Intuitionistic propositional logic — Kripke semantics and a validity/countermodel search.
|
|
2
|
+
|
|
3
|
+
Intuitionistic logic drops the law of excluded middle and double-negation elimination;
|
|
4
|
+
its models are **Kripke models**: a partial order of *worlds* (stages of knowledge)
|
|
5
|
+
with a **monotone** forcing relation (once an atom is forced at a world it is forced at
|
|
6
|
+
every later world). The connectives ``→`` and ``¬`` quantify over future worlds, which
|
|
7
|
+
is exactly what makes ``P ∨ ¬P`` and ``¬¬P → P`` fail.
|
|
8
|
+
|
|
9
|
+
- :class:`IntKripkeModel` — build a model and ask ``model.forces(world, φ)``.
|
|
10
|
+
- :func:`int_valid` — is ``φ`` intuitionistically valid? A propositional formula is
|
|
11
|
+
decided exactly: the search over small Kripke models is tried first, and what it
|
|
12
|
+
does not settle is handed to the G4ip calculus of :func:`~unicode_logic_kit.atp.lj.int_prove`.
|
|
13
|
+
- :func:`int_countermodel` — a model + world where ``φ`` fails (a witness that it is
|
|
14
|
+
*not* intuitionistically valid), or None. ``None`` says that no such model was found
|
|
15
|
+
among the models of at most ``max_worlds`` worlds; it is NOT a proof of validity.
|
|
16
|
+
Intuitionistic propositional logic has the finite-model property, but the size of the
|
|
17
|
+
smallest countermodel grows with the formula: ``(P → Q) ∨ (Q → R) ∨ (R → P)`` is not
|
|
18
|
+
valid and has no countermodel with fewer than four worlds, so at the default bound of
|
|
19
|
+
three worlds the search comes up empty on it. Only :func:`int_valid` (which goes on to
|
|
20
|
+
the calculus) and :func:`~unicode_logic_kit.atp.lj.int_decide` answer for validity.
|
|
21
|
+
|
|
22
|
+
The **propositional** fragment is decided exactly by :func:`int_valid`. The
|
|
23
|
+
**first-order** fragment (``∀x`` / ``∃x``) is handled over *increasing-domain* Kripke
|
|
24
|
+
models — ``w ⊩ ∀x φ`` quantifies over every later world and every individual existing
|
|
25
|
+
there — by a *bounded* search: a counter-model it finds genuinely refutes validity,
|
|
26
|
+
but a clean search does **not** prove validity (first-order intuitionistic logic is
|
|
27
|
+
undecidable, and some non-theorems such as the double-negation shift have only infinite
|
|
28
|
+
counter-models, being valid in every finite model). Every intuitionistic validity is
|
|
29
|
+
also classically valid, which the test-suite cross-checks.
|
|
30
|
+
|
|
31
|
+
**A free variable is a parameter.** ``Γ ⊨ φ`` is the consequence relation of the
|
|
32
|
+
textbooks, over every model, every world and every assignment: a variable that is free
|
|
33
|
+
in a formula names ONE unknown individual, the same in every part of the formula, and an
|
|
34
|
+
assignment gives it an individual that exists at the world of evaluation. In an
|
|
35
|
+
increasing-domain model that individual then exists at every later world too, so the
|
|
36
|
+
search reads the parameter as a CONSTANT named like the variable: rigid, and a member of
|
|
37
|
+
every world's domain. Nothing is lost by it: a formula is checked at every world of
|
|
38
|
+
every model, and the submodel generated by a world has the individual in all of its
|
|
39
|
+
domains. ``P(x) → P(a)`` is not valid (one world, domain ``{x, a}``, ``P`` of ``x``
|
|
40
|
+
only), ``P(x) → P(x)``, ``P(x) → ∃y P(y)`` and ``∀y P(y) → P(x)`` are, ``(P(x) ∧ Q(y)) →
|
|
41
|
+
∀z (P(z) ∧ Q(z))`` is not (``P`` of ``x``, ``Q`` of ``y``, two elements); for a formula
|
|
42
|
+
without a premise the reading coincides with the universal closure. The model that
|
|
43
|
+
:func:`int_countermodel` returns has the individual under the variable's own name; a
|
|
44
|
+
variable and a constant spelled alike would be one individual there, so the pair is
|
|
45
|
+
refused by name. A formula with no quantifier is propositional whatever its terms are:
|
|
46
|
+
an atom is a letter, a variable in it stands for itself, and two atoms whose terms differ
|
|
47
|
+
are two letters, which is what a parameter allows (no quantifier-free formula can tie
|
|
48
|
+
two terms together, and the identity atom is refused).
|
|
49
|
+
|
|
50
|
+
A **numeral** in an atom of the first-order search is the constant of its value
|
|
51
|
+
(``1`` and ``1.0`` are one; a constant spelled like a numeral of the same formula is
|
|
52
|
+
refused by name), and a predicate used at two arities is two predicates.
|
|
53
|
+
|
|
54
|
+
**Many-sorted formulas** (``SortedQuantifier``, ``∀x:S φ`` / ``∃x:S φ``) are decided by
|
|
55
|
+
delegating ONCE, at the top of :func:`int_valid` / :func:`int_countermodel`, to the same
|
|
56
|
+
``_relativize`` reduction :mod:`unicode_logic_kit.fol.to_fol` uses (``∀x:S φ`` → ``∀x (S(x)
|
|
57
|
+
→ φ)``, ``∃x:S φ`` → ``∃x (S(x) ∧ φ)``) — done ONCE for the WHOLE formula, rather than
|
|
58
|
+
node-by-node inside the search, so a ``SortedConstant`` anywhere in the formula (not only
|
|
59
|
+
one directly under a ``SortedQuantifier``) is also turned into a plain ``Constant`` before
|
|
60
|
+
:func:`_fo_countermodel` scans for constants/predicates. The sort guard ``S(x)`` becomes an
|
|
61
|
+
ordinary atom, forced exactly like any other predicate — and every atom's valuation in this
|
|
62
|
+
module is already required to be MONOTONE (up-closed along the order, see
|
|
63
|
+
:func:`_upclosed_subsets` / :func:`_monotone_valuations`), so a sort is "stage-stable" (once
|
|
64
|
+
forced, stays forced at every later stage) but not fully rigid (an individual need not
|
|
65
|
+
already be of sort ``S`` at the initial stage) — the natural reading given the module's
|
|
66
|
+
existing atom semantics, and the same "world/stage-relative, not rigid" choice
|
|
67
|
+
:func:`~unicode_logic_kit.semantics.kripke.satisfies_modal` makes for the modal routes.
|
|
68
|
+
|
|
69
|
+
Non-emptiness and membership: the classical many-sorted routes (``api.prove`` et al.)
|
|
70
|
+
always assume every sort is non-empty and every sorted constant ``c:S`` is an element of
|
|
71
|
+
``S``, by adding :func:`~unicode_logic_kit.fol._msfl_nodes.sort_axioms` as extra premises —
|
|
72
|
+
without the matching assumption here, a schema like ``∀x:S P(x) → ∃x:S P(x)`` would come
|
|
73
|
+
out intuitionistically INVALID (a countermodel with ``S`` empty everywhere) while being
|
|
74
|
+
classically valid, and ``∀x:Human Mortal(x) → Mortal(socrates:Human)`` would have the
|
|
75
|
+
countermodel in which ``socrates`` is no ``Human`` — the "bare relativisation is
|
|
76
|
+
inconsistent with the classical routes" trap the sort-guard-relativisation approach
|
|
77
|
+
warns about. So :func:`int_valid` / :func:`int_countermodel` fold those same sentences
|
|
78
|
+
in as EXTRA PREMISES — checking ``⋀sort_axioms → relativised-formula`` — the
|
|
79
|
+
standard "no separate premise list" trick, sound both classically and intuitionistically
|
|
80
|
+
for a FINITE premise set (the deduction theorem holds in both). A many-sorted formula's
|
|
81
|
+
:func:`int_valid` / :func:`int_countermodel` verdict therefore agrees with
|
|
82
|
+
``api.prove``/Z3's on any MODAL-FREE sorted formula the propositional-or-bounded-first-order
|
|
83
|
+
fragments both decide — see ``tests/test_sorted_modal.py``.
|
|
84
|
+
|
|
85
|
+
Public API: :class:`IntKripkeModel`, :func:`int_valid`, :func:`int_countermodel`.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
from dataclasses import dataclass
|
|
89
|
+
from itertools import combinations, product
|
|
90
|
+
from typing import Dict, FrozenSet, List, Optional, Tuple
|
|
91
|
+
|
|
92
|
+
from ..fol.nodes import (
|
|
93
|
+
Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, SortedQuantifier,
|
|
94
|
+
Constant, Variable, Function, substitute,
|
|
95
|
+
)
|
|
96
|
+
from ..fol._so_nodes import SecondOrderQuantifier
|
|
97
|
+
from ..fol._atom_keys import find_key
|
|
98
|
+
from ..fol._msfl_nodes import key_text, sort_axioms
|
|
99
|
+
from ..fol._free_parameters import parameterize
|
|
100
|
+
from ..fol._numeral_symbols import numerals_as_constants, numeral_name
|
|
101
|
+
from ..fol._truth_constants import truth_value as _truth_value
|
|
102
|
+
from ._modal_reject import reject_equality_in
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_propositional(formula: Node) -> bool:
|
|
106
|
+
"""True iff ``formula`` has no quantifier (the decidable propositional fragment).
|
|
107
|
+
|
|
108
|
+
Checks for ``SortedQuantifier`` too, even though ``int_valid``/
|
|
109
|
+
``int_countermodel`` always relativize a many-sorted formula away (see
|
|
110
|
+
:func:`_prepare_many_sorted`) before this is ever called on it from those
|
|
111
|
+
two entry points -- so in practice this branch never fires there. It is
|
|
112
|
+
kept anyway: a caller may run this directly on a formula that still has
|
|
113
|
+
its ``SortedQuantifier`` nodes, and it must answer correctly there too.
|
|
114
|
+
"""
|
|
115
|
+
return not any(
|
|
116
|
+
isinstance(node, (Quantifier, SortedQuantifier, SecondOrderQuantifier))
|
|
117
|
+
for node in formula.walk())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _prepare_many_sorted(formula: Node) -> Node:
|
|
121
|
+
"""Relativize a many-sorted formula ONCE, for the search entry points.
|
|
122
|
+
|
|
123
|
+
``int_valid``/``int_countermodel`` call this before anything else. Two
|
|
124
|
+
things happen together, both needed for the search to agree with the
|
|
125
|
+
classical many-sorted routes on a many-sorted formula (see the module
|
|
126
|
+
docstring's "Many-sorted formulas" section):
|
|
127
|
+
|
|
128
|
+
1. The WHOLE formula is relativized in one pass (``formula._relativize([])``
|
|
129
|
+
— the same reduction :func:`~unicode_logic_kit.fol.to_fol` uses), not
|
|
130
|
+
node-by-node during the search. Doing it once, up front, also turns a
|
|
131
|
+
``SortedConstant`` occurring ANYWHERE in the formula (not only directly
|
|
132
|
+
under a ``SortedQuantifier``) into a plain ``Constant`` before
|
|
133
|
+
:func:`_fo_countermodel` scans the formula for its constants — a
|
|
134
|
+
per-node fix inside the search would miss that case.
|
|
135
|
+
2. The two facts every classical route assumes of a sort
|
|
136
|
+
(:func:`~unicode_logic_kit.fol._msfl_nodes.sort_axioms`: no sort is empty,
|
|
137
|
+
and a sorted constant ``c:S`` is an element of ``S``) are folded in
|
|
138
|
+
here as an extra premise too — checking
|
|
139
|
+
``⋀sort-axioms → relativized-formula`` in place of the bare relativized
|
|
140
|
+
formula. Sound both classically and intuitionistically for a finite
|
|
141
|
+
premise set (the deduction theorem holds in both), and it is what makes
|
|
142
|
+
e.g. ``∀x:S P(x) → ∃x:S P(x)`` agree with the classical Z3 verdict here
|
|
143
|
+
instead of finding an empty-``S`` countermodel, and
|
|
144
|
+
``∀x:Human Mortal(x) → Mortal(socrates:Human)`` valid (``socrates`` is a
|
|
145
|
+
``Human``) while the same formula with a plain ``socrates`` is not.
|
|
146
|
+
|
|
147
|
+
The membership fact is an ANTECEDENT, not a conjunct of the formula. A
|
|
148
|
+
valuation is up-closed, so ``S(c)`` forced at a stage is forced at every
|
|
149
|
+
later one, and the clause for ``→`` only looks upward: the wrapped
|
|
150
|
+
formula is forced everywhere in every model iff the relativized formula
|
|
151
|
+
is forced in every model in which ``S(c)`` is forced at every stage
|
|
152
|
+
(restrict a refuting model to the up-set of the stage where the
|
|
153
|
+
antecedent holds). That is exactly "the constant is in its sort", with
|
|
154
|
+
the sort a stage-relative atom like every other one here.
|
|
155
|
+
|
|
156
|
+
Returns ``formula`` UNCHANGED (same object, not even copied) when it has
|
|
157
|
+
no many-sorted node at all — ``sort_axioms`` returning nothing IS
|
|
158
|
+
exactly "no ``SortedQuantifier``/``SortedConstant``/``SortedCount``/
|
|
159
|
+
``SortedCardinality`` anywhere in it" (the same four types
|
|
160
|
+
``_relativize`` would otherwise eliminate), so every call site that never
|
|
161
|
+
used many_sorted sees byte-identical behaviour to before this existed.
|
|
162
|
+
"""
|
|
163
|
+
axioms = sort_axioms(formula)
|
|
164
|
+
if not axioms:
|
|
165
|
+
return formula
|
|
166
|
+
relativized = formula._relativize([])
|
|
167
|
+
hyp = axioms[0]
|
|
168
|
+
for ax in axioms[1:]:
|
|
169
|
+
hyp = And(hyp, ax)
|
|
170
|
+
return Implies(hyp, relativized)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
#: Identity is not one of the atoms this search can read. A world's valuation is
|
|
174
|
+
#: a set of atom KEYS (the text of the atom with every constant written by its name),
|
|
175
|
+
#: monotone along the order, so
|
|
176
|
+
#: ``a = b`` would be an unconstrained letter: ``int_valid(a = a)`` came back False
|
|
177
|
+
#: and ``int_countermodel(a = a)`` handed back a one-world model as if it refuted
|
|
178
|
+
#: reflexivity (both measured on 0.28.1). The Gödel–McKinsey–Tarski embedding
|
|
179
|
+
#: (:mod:`unicode_logic_kit.hol.intuitionistic`) already refuses the atom by name, and
|
|
180
|
+
#: THIS module is the oracle it is checked against, so the two have to agree about
|
|
181
|
+
#: what they will not answer. Intuitionistic logic WITH equality needs a term
|
|
182
|
+
#: semantics neither this search nor G4ip has, and it is not part of the GMT theorem.
|
|
183
|
+
_EQUALITY_ROUTE = "the intuitionistic Kripke search"
|
|
184
|
+
_EQUALITY_ATOM_READING = ("a world's valuation is a monotone set of atom keys, "
|
|
185
|
+
"and no term is interpreted")
|
|
186
|
+
_EQUALITY_INSTEAD = (
|
|
187
|
+
"Decide identity classically with unicode_logic_kit.api.prove, or on the "
|
|
188
|
+
"quantified modal route with unicode_logic_kit.fol.qml.qml_is_valid (rigid "
|
|
189
|
+
"identity over the object domain); intuitionistic logic with equality is not "
|
|
190
|
+
"what this search or atp.lj.int_prove decides."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _reject_equality_everywhere(formula: Node, caller: str) -> None:
|
|
195
|
+
"""Refuse an identity atom anywhere in ``formula``, before any search.
|
|
196
|
+
|
|
197
|
+
A whole-tree scan at the entry point, not a check inside the evaluator: the
|
|
198
|
+
search stops at the first refuting model, so a formula whose countermodel is
|
|
199
|
+
found before the identity atom is reached would come back with a verdict that
|
|
200
|
+
never read it — ``(p ∧ ¬p) → (a = b)`` is the clean case, intuitionistically
|
|
201
|
+
valid without the atom mattering at all.
|
|
202
|
+
"""
|
|
203
|
+
reject_equality_in(formula, caller, _EQUALITY_ROUTE,
|
|
204
|
+
atom_reading=_EQUALITY_ATOM_READING,
|
|
205
|
+
instead=_EQUALITY_INSTEAD)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _reject_second_order(formula: Node) -> None:
|
|
209
|
+
"""Reject a second-order quantifier or a function term in the FO search.
|
|
210
|
+
|
|
211
|
+
A sorted quantifier/constant needs no rejection here any more: by the
|
|
212
|
+
time :func:`_fo_countermodel` calls this, ``int_valid``/
|
|
213
|
+
``int_countermodel`` have already relativized it away (see
|
|
214
|
+
:func:`_prepare_many_sorted`), so it is an ordinary (unsorted)
|
|
215
|
+
first-order formula.
|
|
216
|
+
"""
|
|
217
|
+
for node in formula.walk():
|
|
218
|
+
if isinstance(node, SecondOrderQuantifier):
|
|
219
|
+
raise ValueError("intuitionistic: second-order quantifiers are not supported.")
|
|
220
|
+
if isinstance(node, Function):
|
|
221
|
+
raise ValueError(
|
|
222
|
+
"intuitionistic: function terms are not supported in the first-order "
|
|
223
|
+
"Kripke search; use predicates over variables and constants.")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass(frozen=True)
|
|
227
|
+
class IntKripkeModel:
|
|
228
|
+
"""A finite intuitionistic Kripke model over the worlds ``0`` to ``n-1``.
|
|
229
|
+
|
|
230
|
+
``upset[w]`` is the set of worlds accessible from ``w`` (its up-set in the partial
|
|
231
|
+
order, reflexive and transitive — including ``w`` itself). ``valuation[key]`` is the
|
|
232
|
+
up-closed set of worlds forcing the atom ``key`` (its text with every constant written by
|
|
233
|
+
its name, ``P(a)`` for the atom ``P`` of the constant ``a``; the text of the atom as a
|
|
234
|
+
formula, ``P('a')``, is read as the same key, and a table that holds both spellings with
|
|
235
|
+
different sets of worlds is refused). Build directly, or let :func:`int_countermodel`
|
|
236
|
+
produce one, keyed by the names.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
upset: Dict[int, FrozenSet[int]]
|
|
240
|
+
valuation: Dict[str, FrozenSet[int]]
|
|
241
|
+
domains: Optional[Dict[int, FrozenSet]] = None
|
|
242
|
+
|
|
243
|
+
def forces(self, world: int, formula: Node) -> bool:
|
|
244
|
+
"""Return whether ``world`` forces ``formula`` (the intuitionistic clauses).
|
|
245
|
+
|
|
246
|
+
First-order clauses (when the model carries per-world ``domains``, which must
|
|
247
|
+
be **increasing** along the order): ``w ⊩ ∀x φ`` iff for every later ``w' ≥ w``
|
|
248
|
+
and every ``d ∈ D_{w'}``, ``w' ⊩ φ[x:=d]``; ``w ⊩ ∃x φ`` iff some ``d ∈ D_w``
|
|
249
|
+
has ``w ⊩ φ[x:=d]``.
|
|
250
|
+
"""
|
|
251
|
+
if isinstance(formula, Quantifier):
|
|
252
|
+
if self.domains is None:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
"intuitionistic forcing: a quantifier needs per-world domains "
|
|
255
|
+
"(build the model with domains={world: [...]}).")
|
|
256
|
+
var = formula.variable
|
|
257
|
+
if formula.type in ("∀", "forall"):
|
|
258
|
+
return all(
|
|
259
|
+
self.forces(w2, substitute(formula.formula, var, Constant(d)))
|
|
260
|
+
for w2 in self.upset[world] for d in self.domains[w2])
|
|
261
|
+
if formula.type in ("∃", "exists"):
|
|
262
|
+
return any(
|
|
263
|
+
self.forces(world, substitute(formula.formula, var, Constant(d)))
|
|
264
|
+
for d in self.domains[world])
|
|
265
|
+
raise ValueError(f"intuitionistic forcing: unknown quantifier {formula.type!r}")
|
|
266
|
+
if isinstance(formula, Atom):
|
|
267
|
+
constant = _truth_value(formula)
|
|
268
|
+
if constant is not None:
|
|
269
|
+
return constant # `$true` is forced at every world, `$false` at none
|
|
270
|
+
# The key first, then the formula text: the elements of the domain are substituted
|
|
271
|
+
# as constants (``Constant("a")`` is ``'a'`` in the text of a formula), the
|
|
272
|
+
# valuation of a model is keyed ``P(a)``, and a caller who keyed it by the text of
|
|
273
|
+
# a hand-built atom, ``P('a')``, is read the same.
|
|
274
|
+
found = find_key(self.valuation, formula)
|
|
275
|
+
return found is not None and world in self.valuation[found]
|
|
276
|
+
if isinstance(formula, And):
|
|
277
|
+
return self.forces(world, formula.left) and self.forces(world, formula.right)
|
|
278
|
+
if isinstance(formula, Or):
|
|
279
|
+
return self.forces(world, formula.left) or self.forces(world, formula.right)
|
|
280
|
+
if isinstance(formula, Implies):
|
|
281
|
+
# w ⊩ A→B iff for every w' ≥ w, w' ⊩ A implies w' ⊩ B.
|
|
282
|
+
return all(self.forces(w2, formula.right)
|
|
283
|
+
for w2 in self.upset[world] if self.forces(w2, formula.left))
|
|
284
|
+
if isinstance(formula, Not):
|
|
285
|
+
# w ⊩ ¬A iff no w' ≥ w forces A.
|
|
286
|
+
return not any(self.forces(w2, formula.formula) for w2 in self.upset[world])
|
|
287
|
+
if isinstance(formula, Iff):
|
|
288
|
+
return (self.forces(world, Implies(formula.left, formula.right))
|
|
289
|
+
and self.forces(world, Implies(formula.right, formula.left)))
|
|
290
|
+
if isinstance(formula, Xor):
|
|
291
|
+
# Defined intuitionistically as (A ∨ B) ∧ ¬(A ∧ B).
|
|
292
|
+
a, b = formula.left, formula.right
|
|
293
|
+
return self.forces(world, And(Or(a, b), Not(And(a, b))))
|
|
294
|
+
raise ValueError(f"intuitionistic forcing: unsupported node "
|
|
295
|
+
f"{type(formula).__name__}")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _atom_keys(formula: Node) -> List[str]:
|
|
299
|
+
"""Distinct atom keys (propositional variables) in ``formula``.
|
|
300
|
+
|
|
301
|
+
A letter is named by the text its atom prints as, so two different atoms that print
|
|
302
|
+
alike (the numeral ``1`` and a constant named ``1``, a free variable ``x`` and a constant
|
|
303
|
+
named ``x``) would be one letter and the formula another problem: such a pair is refused
|
|
304
|
+
by name (``NotImplementedError``). ``$true`` / ``$false`` are constants, not letters.
|
|
305
|
+
"""
|
|
306
|
+
from ..fol._atom_keys import AtomKeys
|
|
307
|
+
return AtomKeys("int_countermodel").letters([formula])
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _partial_orders(n: int):
|
|
311
|
+
"""Yield each partial order on ``{0..n-1}`` as a dict ``world -> up-set frozenset``.
|
|
312
|
+
|
|
313
|
+
Enumerates reflexive relations and keeps the transitive (and antisymmetric) ones;
|
|
314
|
+
the up-set ``upset[w] = {w' : w ≤ w'}`` is what the forcing clauses consult.
|
|
315
|
+
"""
|
|
316
|
+
worlds = list(range(n))
|
|
317
|
+
off_diagonal = [(i, j) for i in worlds for j in worlds if i != j]
|
|
318
|
+
for mask in product((False, True), repeat=len(off_diagonal)):
|
|
319
|
+
leq = {(i, i) for i in worlds}
|
|
320
|
+
leq |= {pair for pair, inc in zip(off_diagonal, mask) if inc}
|
|
321
|
+
# transitivity
|
|
322
|
+
if any((a, b) in leq and (b, c) in leq and (a, c) not in leq
|
|
323
|
+
for a in worlds for b in worlds for c in worlds):
|
|
324
|
+
continue
|
|
325
|
+
# antisymmetry (a genuine partial order; preorders collapse to these for forcing)
|
|
326
|
+
if any(a != b and (a, b) in leq and (b, a) in leq for a in worlds for b in worlds):
|
|
327
|
+
continue
|
|
328
|
+
yield {w: frozenset(j for j in worlds if (w, j) in leq) for w in worlds}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _monotone_valuations(upset: Dict[int, FrozenSet[int]], keys: List[str]):
|
|
332
|
+
"""Yield every monotone (up-closed) valuation of ``keys`` over the order ``upset``."""
|
|
333
|
+
worlds = list(upset)
|
|
334
|
+
# Up-closed subsets: a set S with w∈S ⇒ upset[w] ⊆ S.
|
|
335
|
+
upclosed = []
|
|
336
|
+
for r in range(len(worlds) + 1):
|
|
337
|
+
for combo in combinations(worlds, r):
|
|
338
|
+
s = frozenset(combo)
|
|
339
|
+
if all(upset[w] <= s for w in s):
|
|
340
|
+
upclosed.append(s)
|
|
341
|
+
for choice in product(upclosed, repeat=len(keys)):
|
|
342
|
+
yield dict(zip(keys, choice))
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _upclosed_subsets(upset: Dict[int, FrozenSet[int]], allowed) -> List[FrozenSet[int]]:
|
|
346
|
+
"""Every up-closed subset of the (itself up-closed) world set ``allowed``."""
|
|
347
|
+
worlds = sorted(allowed)
|
|
348
|
+
out: List[FrozenSet[int]] = []
|
|
349
|
+
for r in range(len(worlds) + 1):
|
|
350
|
+
for combo in combinations(worlds, r):
|
|
351
|
+
s = frozenset(combo)
|
|
352
|
+
if all(upset[w] <= s for w in s):
|
|
353
|
+
out.append(s)
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _increasing_domains(upset: Dict[int, FrozenSet[int]], consts: List[str], n_fresh: int):
|
|
358
|
+
"""Yield each increasing per-world domain over ``consts`` + up to ``n_fresh`` elements.
|
|
359
|
+
|
|
360
|
+
Constants exist at every world (rigid). A base fresh element exists everywhere
|
|
361
|
+
so no world is empty; each further fresh element appears on an up-closed set of
|
|
362
|
+
worlds (so the domains grow along the order). The fresh elements are named
|
|
363
|
+
``_e0``, ``_e1``, … and skip every name of ``consts``, so a constant or a parameter
|
|
364
|
+
spelled like one of them is never merged with it.
|
|
365
|
+
"""
|
|
366
|
+
worlds = sorted(upset)
|
|
367
|
+
base = set(consts)
|
|
368
|
+
fresh: List[str] = []
|
|
369
|
+
index = 0
|
|
370
|
+
while len(fresh) < n_fresh:
|
|
371
|
+
candidate = f"_e{index}"
|
|
372
|
+
index += 1
|
|
373
|
+
if candidate not in base:
|
|
374
|
+
fresh.append(candidate)
|
|
375
|
+
if not fresh:
|
|
376
|
+
if base:
|
|
377
|
+
yield {w: frozenset(base) for w in worlds}
|
|
378
|
+
return
|
|
379
|
+
always = frozenset(worlds)
|
|
380
|
+
var_fresh = fresh[1:]
|
|
381
|
+
options = _upclosed_subsets(upset, worlds)
|
|
382
|
+
for appearances in product(options, repeat=len(var_fresh)):
|
|
383
|
+
appear = {fresh[0]: always}
|
|
384
|
+
appear.update(zip(var_fresh, appearances))
|
|
385
|
+
domains = {}
|
|
386
|
+
for w in worlds:
|
|
387
|
+
elems = set(base) | {fresh[0]}
|
|
388
|
+
elems.update(e for e in var_fresh if w in appear[e])
|
|
389
|
+
domains[w] = frozenset(elems)
|
|
390
|
+
yield domains
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _read_first_order_terms(formula: Node) -> Node:
|
|
394
|
+
"""``formula`` with every term of an atom a constant, a function term or a bound variable.
|
|
395
|
+
|
|
396
|
+
The search gives a valuation to the ground atoms over the individuals of the model
|
|
397
|
+
and to nothing else, so a term of any other kind would leave its atom forced at no
|
|
398
|
+
world at all: ``P(y)`` with ``y`` free would be false in every model, as the
|
|
399
|
+
antecedent of ``P(y) → ∃x Q(x)`` and as the conclusion of ``∀x P(x) → P(y)``. Three
|
|
400
|
+
readings put every such term in the search's reach:
|
|
401
|
+
|
|
402
|
+
* a numeral is the constant of its value (``1`` and ``1.0`` are one constant), and a
|
|
403
|
+
constant spelled like a numeral of the formula is refused by name;
|
|
404
|
+
* a variable that is free is a parameter, read as a constant named like the variable
|
|
405
|
+
(see the module docstring), and a free variable spelled like a constant of the
|
|
406
|
+
formula is refused by name;
|
|
407
|
+
* any other kind of term is refused by name.
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
NotImplementedError: a numeral and a constant are spelled alike, or a free
|
|
411
|
+
variable and a constant are.
|
|
412
|
+
ValueError: an atom holds a term of a kind the search does not read.
|
|
413
|
+
"""
|
|
414
|
+
[formula], _ = numerals_as_constants([formula], where="int_countermodel",
|
|
415
|
+
spell=numeral_name)
|
|
416
|
+
[formula], _ = parameterize([formula], after_variables=True)
|
|
417
|
+
for node in formula.walk():
|
|
418
|
+
if isinstance(node, Atom):
|
|
419
|
+
for term in node.args:
|
|
420
|
+
if not isinstance(term, (Variable, Constant, Function)):
|
|
421
|
+
raise ValueError(
|
|
422
|
+
f"intuitionistic: the term {term.to_unicode_str()!r} of "
|
|
423
|
+
f"{node.to_unicode_str()!r} is not a variable, a constant or a "
|
|
424
|
+
"numeral, and the first-order Kripke search gives no atom over "
|
|
425
|
+
"such a term a valuation; use a constant or a variable.")
|
|
426
|
+
return formula
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _fo_countermodel(formula: Node, max_worlds: int, domain_elements: int,
|
|
430
|
+
max_steps: int) -> Optional[Tuple[IntKripkeModel, int]]:
|
|
431
|
+
"""Bounded first-order intuitionistic counter-model search (sound; not a decision).
|
|
432
|
+
|
|
433
|
+
Enumerates Kripke models up to ``max_worlds`` worlds with increasing domains drawn
|
|
434
|
+
from the formula's constants plus ``domain_elements`` fresh individuals, and
|
|
435
|
+
monotone predicate valuations respecting element existence. A returned model
|
|
436
|
+
genuinely refutes intuitionistic validity; ``None`` means "no counter-model within
|
|
437
|
+
the bounds" — NOT a proof of validity (first-order intuitionistic logic is
|
|
438
|
+
undecidable). The formula's terms are constants and bound variables only (see
|
|
439
|
+
:func:`_read_first_order_terms`).
|
|
440
|
+
"""
|
|
441
|
+
_reject_second_order(formula)
|
|
442
|
+
consts = sorted({n.name for n in formula.walk() if isinstance(n, Constant)})
|
|
443
|
+
preds: Dict[Tuple[str, int], None] = {} # one predicate per (name, arity)
|
|
444
|
+
for node in formula.walk():
|
|
445
|
+
if isinstance(node, Atom) and _truth_value(node) is None:
|
|
446
|
+
preds[(node.predicate, len(node.args))] = None
|
|
447
|
+
steps = 0
|
|
448
|
+
for n in range(1, max_worlds + 1):
|
|
449
|
+
for upset in _partial_orders(n):
|
|
450
|
+
for domains in _increasing_domains(upset, consts, domain_elements):
|
|
451
|
+
pool = sorted(set().union(*domains.values()))
|
|
452
|
+
ground, seen = [], set()
|
|
453
|
+
for name, ar in preds:
|
|
454
|
+
for tup in product(pool, repeat=ar):
|
|
455
|
+
key = key_text(Atom(name, [Constant(t) for t in tup]))
|
|
456
|
+
if key not in seen:
|
|
457
|
+
seen.add(key)
|
|
458
|
+
allowed = frozenset(w for w in upset if set(tup) <= domains[w])
|
|
459
|
+
ground.append((key, _upclosed_subsets(upset, allowed)))
|
|
460
|
+
keys = [k for k, _ in ground]
|
|
461
|
+
option_lists = [opts for _, opts in ground]
|
|
462
|
+
for choice in (product(*option_lists) if ground else [()]):
|
|
463
|
+
steps += 1
|
|
464
|
+
if steps > max_steps:
|
|
465
|
+
return None
|
|
466
|
+
model = IntKripkeModel(upset, dict(zip(keys, choice)), domains)
|
|
467
|
+
for w in range(n):
|
|
468
|
+
if not model.forces(w, formula):
|
|
469
|
+
return model, w
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def int_countermodel(formula: Node, max_worlds: int = 3,
|
|
474
|
+
domain_elements: int = 2,
|
|
475
|
+
max_steps: int = 300000) -> Optional[Tuple[IntKripkeModel, int]]:
|
|
476
|
+
"""Return ``(model, world)`` where ``formula`` fails intuitionistically, or None.
|
|
477
|
+
|
|
478
|
+
A returned model genuinely refutes validity: it is a witness that ``formula`` is
|
|
479
|
+
NOT intuitionistically valid. ``None`` never proves validity, for either fragment.
|
|
480
|
+
It says that the bounded search found no countermodel, and the bound is a real one.
|
|
481
|
+
For a **propositional** formula the search runs over every Kripke model of at most
|
|
482
|
+
``max_worlds`` worlds (intuitionistic propositional logic has the finite-model
|
|
483
|
+
property, but the smallest countermodel of a formula can have more worlds than the
|
|
484
|
+
default allows: ``(P → Q) ∨ (Q → R) ∨ (R → P)`` is not valid, yet ``None`` comes
|
|
485
|
+
back at ``max_worlds=3`` and a model at ``max_worlds=4``). Ask
|
|
486
|
+
:func:`int_valid`, or :func:`~unicode_logic_kit.atp.lj.int_decide`, for a verdict on
|
|
487
|
+
validity: both are exact on a propositional formula. For a **first-order** formula
|
|
488
|
+
(containing ∀/∃) it is a *bounded* search over increasing-domain Kripke models (the
|
|
489
|
+
formula's constants plus ``domain_elements`` fresh individuals, up to ``max_steps``
|
|
490
|
+
valuations), and first-order intuitionistic logic is undecidable, so ``None`` only
|
|
491
|
+
means "no counter-model within the bounds".
|
|
492
|
+
|
|
493
|
+
A variable that is free in ``formula`` is a PARAMETER: one unknown individual,
|
|
494
|
+
the same everywhere in the formula, which exists in every world of the model (the
|
|
495
|
+
module docstring has the derivation). The search reads it as a constant named like
|
|
496
|
+
the variable, so ``∀x P(x) → P(y)`` has no countermodel, ``P(y) → ∃x Q(x)`` has one
|
|
497
|
+
(the individual ``y`` with ``P`` of it and ``Q`` of nothing) and ``y`` is a member of
|
|
498
|
+
every domain of the model it returns; evaluate it with
|
|
499
|
+
``model.forces(world, substitute(formula, Variable('y'), Constant('y')))``. A
|
|
500
|
+
numeral in an atom is the constant of its value. Both are read in the first-order
|
|
501
|
+
search only: a formula with no quantifier is propositional and keeps its atoms as
|
|
502
|
+
letters. Every parameter and every constant is one individual of each model, so a
|
|
503
|
+
formula with many of them searches larger models within the same ``max_steps``, and
|
|
504
|
+
a countermodel that needs a fresh individual beyond ``domain_elements`` (or more
|
|
505
|
+
valuations than ``max_steps``) is not found.
|
|
506
|
+
|
|
507
|
+
Many-sorted (``∀x:S`` / ``∃x:S`` / ``c:S``): relativized and given the classical
|
|
508
|
+
sort assumptions (no sort is empty, a sorted constant is in its sort) before the
|
|
509
|
+
search runs — see
|
|
510
|
+
:func:`_prepare_many_sorted` and the module docstring.
|
|
511
|
+
|
|
512
|
+
Raises:
|
|
513
|
+
NotImplementedError: a free variable is spelled like a constant of the formula
|
|
514
|
+
(the two would be one individual of the model), or a numeral like a
|
|
515
|
+
constant, or an identity atom occurs.
|
|
516
|
+
ValueError: second-order quantifiers or function terms occur, or an atom holds a
|
|
517
|
+
term that is neither a variable, a constant nor a numeral.
|
|
518
|
+
"""
|
|
519
|
+
_reject_equality_everywhere(formula, "int_countermodel")
|
|
520
|
+
formula = _prepare_many_sorted(formula)
|
|
521
|
+
if _is_propositional(formula):
|
|
522
|
+
keys = _atom_keys(formula)
|
|
523
|
+
for n in range(1, max_worlds + 1):
|
|
524
|
+
for upset in _partial_orders(n):
|
|
525
|
+
for valuation in _monotone_valuations(upset, keys):
|
|
526
|
+
model = IntKripkeModel(upset, valuation)
|
|
527
|
+
for w in range(n):
|
|
528
|
+
if not model.forces(w, formula):
|
|
529
|
+
return model, w
|
|
530
|
+
return None
|
|
531
|
+
formula = _read_first_order_terms(formula)
|
|
532
|
+
return _fo_countermodel(formula, max_worlds, domain_elements, max_steps)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def int_valid(formula: Node, max_worlds: int = 3,
|
|
536
|
+
domain_elements: int = 2, max_steps: int = 300000) -> bool:
|
|
537
|
+
"""Return True iff ``formula`` is intuitionistically valid.
|
|
538
|
+
|
|
539
|
+
**Split contract** — the two fragments are decided differently, and honestly:
|
|
540
|
+
|
|
541
|
+
- **Propositional**: a genuine decision procedure, full stop, REGARDLESS of
|
|
542
|
+
``max_worlds``. The bounded Kripke search (:func:`int_countermodel`) is tried
|
|
543
|
+
first as a fast path — a countermodel it finds within ``max_worlds`` worlds is a
|
|
544
|
+
real witness, so a quick ``False`` is returned without going further. But
|
|
545
|
+
propositional IPL's finite-model-property bound *grows with the formula* (e.g.
|
|
546
|
+
``(p→q)∨(q→r)∨(r→p)`` needs 4 worlds — 3, the default, finds nothing), so
|
|
547
|
+
"no countermodel within the bound" is NOT by itself proof of validity. When the
|
|
548
|
+
bounded search comes up empty, the verdict is instead handed to
|
|
549
|
+
:func:`~unicode_logic_kit.atp.lj.int_prove` (Dyckhoff's contraction-free G4ip, a
|
|
550
|
+
terminating decision procedure with no bound to exhaust) for a DEFINITIVE
|
|
551
|
+
True-or-False answer. Net effect: both the ``True`` and the ``False`` verdicts on
|
|
552
|
+
a propositional formula are exact, at every ``max_worlds`` — including the
|
|
553
|
+
default. (Before this delegation, ``int_valid`` of the formula above wrongly
|
|
554
|
+
returned ``True`` at ``max_worlds=3``; see ``tests/test_lj_search.py`` for the
|
|
555
|
+
pinned regression.)
|
|
556
|
+
- **First-order**: a sound but *incomplete* check. ``True`` means "no
|
|
557
|
+
counter-model within the bounds" (not a proof, since first-order intuitionistic
|
|
558
|
+
validity is undecidable), while ``False`` is always backed by a real counter-model
|
|
559
|
+
from :func:`int_countermodel`. A free variable is a parameter, an individual that
|
|
560
|
+
exists in every world of the model, so ``∀x P(x) → P(y)`` is valid and
|
|
561
|
+
``P(y) → ∃x Q(x)`` is not (see :func:`int_countermodel` and the module docstring).
|
|
562
|
+
|
|
563
|
+
Many-sorted (``∀x:S`` / ``∃x:S`` / ``c:S``): relativized and given the classical
|
|
564
|
+
sort assumptions (no sort is empty, a sorted constant is in its sort) before
|
|
565
|
+
either fragment's check runs (so a
|
|
566
|
+
formula that is many-sorted always takes the first-order branch above,
|
|
567
|
+
even if it would otherwise look propositional — see
|
|
568
|
+
:func:`_prepare_many_sorted` and the module docstring).
|
|
569
|
+
|
|
570
|
+
Raises:
|
|
571
|
+
NotImplementedError: as :func:`int_countermodel`.
|
|
572
|
+
ValueError: as :func:`int_countermodel`.
|
|
573
|
+
"""
|
|
574
|
+
_reject_equality_everywhere(formula, "int_valid")
|
|
575
|
+
formula = _prepare_many_sorted(formula)
|
|
576
|
+
if not _is_propositional(formula):
|
|
577
|
+
return int_countermodel(formula, max_worlds, domain_elements, max_steps) is None
|
|
578
|
+
if int_countermodel(formula, max_worlds, domain_elements, max_steps) is not None:
|
|
579
|
+
return False
|
|
580
|
+
from ..atp.lj import int_prove # lazy: avoids a semantics<->atp import cycle
|
|
581
|
+
return int_prove([], formula)
|