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,913 @@
|
|
|
1
|
+
r"""Free logic — first-order logic without the existence assumption.
|
|
2
|
+
|
|
3
|
+
Classical FOL assumes every term denotes an *existing* individual, so universal
|
|
4
|
+
instantiation ``∀x φ → φ(c)`` and existential generalisation ``φ(c) → ∃x φ`` are
|
|
5
|
+
valid. **Free logic** drops that assumption: quantifiers range only over an *inner
|
|
6
|
+
domain* of existing objects ``E``, while constants and function terms may denote an
|
|
7
|
+
object of the wider outer domain, or fail to denote at all. An existence predicate
|
|
8
|
+
``E!(t)`` says "``t`` denotes an existing object", and the classical inference rules
|
|
9
|
+
hold only in their *guarded* forms ``(∀x φ ∧ E!(c)) → φ(c)`` and ``(φ(c) ∧ E!(c)) → ∃x φ``.
|
|
10
|
+
|
|
11
|
+
A :class:`FreeModel` carries an ``outer`` domain, the ``existing`` inner subset, a
|
|
12
|
+
(possibly partial) constant/function interpretation, and predicate tables over the
|
|
13
|
+
outer domain. Three policies for an atom that contains a **non-denoting** term:
|
|
14
|
+
|
|
15
|
+
- ``"negative"`` (default) — the atom is simply **false** (negative free logic;
|
|
16
|
+
``t = t`` then also fails when ``t`` does not denote);
|
|
17
|
+
- ``"positive"`` — self-identity ``t = t`` is **true** for any term, while every other
|
|
18
|
+
atom with a non-denoting term is false (the common positive-free-logic convention
|
|
19
|
+
for identity);
|
|
20
|
+
- ``"supervaluation"`` — treats every ground atom with a non-denoting term as a
|
|
21
|
+
genuine truth-value **gap** rather than forcing it false, then asks whether the
|
|
22
|
+
*whole formula* comes out true under **every** classical way of filling the gaps
|
|
23
|
+
(a *precisification*), false under every one, or neither. A formula is true
|
|
24
|
+
(*supertrue*) iff every precisification makes it true, false (*superfalse*) iff
|
|
25
|
+
every precisification makes it false, and otherwise it is itself a gap — reported
|
|
26
|
+
as ``False``, the same convention ``"negative"`` already uses for a single gappy
|
|
27
|
+
atom (``free_satisfies`` always returns a plain ``bool``; there is no third value
|
|
28
|
+
in the return type). The distinguishing case is a classical tautology such as
|
|
29
|
+
``P(e) ∨ ¬P(e)`` for non-denoting ``e``: each disjunct is individually a gap, yet
|
|
30
|
+
*every* precisification of ``P(e)`` (true or false) makes the disjunction true, so
|
|
31
|
+
the whole formula is supertrue — unlike naively combining each disjunct's own
|
|
32
|
+
gappy verdict, which would stay a gap. See :func:`free_satisfies` for the bounded
|
|
33
|
+
precisification search this policy runs (:data:`SUPERVALUATION_MAX_GAPS`).
|
|
34
|
+
|
|
35
|
+
Beyond single-model checking, :func:`free_find_model`, :func:`free_countermodel`,
|
|
36
|
+
:func:`free_is_valid` and :func:`free_entails` add a Mace4-style **bounded
|
|
37
|
+
search** over ``FreeModel``\ s, in the style of
|
|
38
|
+
:mod:`~unicode_logic_kit.semantics.modelfinder`: every outer domain size up to a bound,
|
|
39
|
+
every existing/outer split, every partial constant/function assignment, and every
|
|
40
|
+
predicate extension. See their docstrings for the honest bounded-search contract.
|
|
41
|
+
By default (``symmetry_breaking=True``) the partial constant assignment is
|
|
42
|
+
LNH-canonical rather than exhaustive — the same symmetry-breaking pass
|
|
43
|
+
:mod:`~unicode_logic_kit.semantics.modelfinder` applies (roadmap C23); see
|
|
44
|
+
``_search``'s docstring.
|
|
45
|
+
|
|
46
|
+
**Numerals.** A numeral is a constant identified by its VALUE (``Number(1) == Number(1.0)``:
|
|
47
|
+
one constant, named ``'1'`` in ``FreeModel.constants``), so it may fail to denote like any
|
|
48
|
+
constant, and nothing else is known about it: ``P(1) ⊢ P(1.0)`` is valid and ``⊢ 1 ≠ 2`` is
|
|
49
|
+
not. ``+ - * /`` are partial function symbols and ``< > ≤ ≥`` ordinary predicates. A
|
|
50
|
+
numeral and a constant spelled like its value (``Number(1)`` next to ``Constant('1')``) would
|
|
51
|
+
share one entry of ``constants``, so the pair is refused by name.
|
|
52
|
+
|
|
53
|
+
**A free variable is a parameter.** The search routes read a free variable as ONE unknown
|
|
54
|
+
EXISTING object, the same in every formula handed to a call (the assignment-wise consequence
|
|
55
|
+
relation: a variable ranges over the inner domain, as a bound one does). It is replaced, in all
|
|
56
|
+
the formulas of the call together, by a constant of its own name
|
|
57
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`) and the model must satisfy
|
|
58
|
+
``E!`` of that constant, so a model whose ``existing`` domain is empty is no model of a
|
|
59
|
+
problem that has a free variable, and the constant is reported in ``FreeModel.constants`` under
|
|
60
|
+
the variable's name. No formula is closed universally, and a conclusion is never negated
|
|
61
|
+
before its variables are replaced: ``P(x) ⊢ P(x)`` and ``P(x) ⊢ ∃y P(y)`` are valid,
|
|
62
|
+
``P(x) ⊢ P(alpha)`` is not. A free variable spelled like a constant of the problem is refused
|
|
63
|
+
(``NotImplementedError``): ``FreeModel.constants`` holds one entry per name.
|
|
64
|
+
|
|
65
|
+
Public API: :class:`FreeModel`, :data:`NONDENOTING`, :data:`SUPERVALUATION_MAX_GAPS`,
|
|
66
|
+
:func:`free_satisfies`, :func:`free_holds`, :func:`free_find_model`,
|
|
67
|
+
:func:`free_countermodel`, :func:`free_is_valid`, :func:`free_entails`.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
from dataclasses import dataclass, field
|
|
71
|
+
from itertools import product
|
|
72
|
+
from typing import Any, Dict, FrozenSet, List, Mapping, Optional, Sequence, Tuple
|
|
73
|
+
|
|
74
|
+
from ..fol.nodes import (
|
|
75
|
+
Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
|
|
76
|
+
Variable, Constant, Number, Function,
|
|
77
|
+
)
|
|
78
|
+
from ..fol._fol_nodes import numeral_key
|
|
79
|
+
from ..fol._truth_constants import truth_value as _truth_value
|
|
80
|
+
from ..fol._free_parameters import parameterize
|
|
81
|
+
from .modelfinder import _lnh_choices
|
|
82
|
+
from .tarski import _refuse_numeral_constant_pair
|
|
83
|
+
|
|
84
|
+
# Sentinel returned by term evaluation when a term has no referent.
|
|
85
|
+
NONDENOTING = object()
|
|
86
|
+
|
|
87
|
+
_FORALL = ("∀", "forall")
|
|
88
|
+
_EXISTS = ("∃", "exists")
|
|
89
|
+
_EXISTS_PRED = "E!" # the existence predicate
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class FreeModel:
|
|
94
|
+
"""A free-logic model: an inner ``existing`` domain inside an ``outer`` domain.
|
|
95
|
+
|
|
96
|
+
``outer`` lists every object (existing or merely possible); ``existing`` is the
|
|
97
|
+
inner domain the quantifiers range over (⊆ ``outer``). ``constants`` maps a name to
|
|
98
|
+
an ``outer`` element — a name absent from the map is **non-denoting**. ``functions``
|
|
99
|
+
maps ``(name, arity)`` to a partial table ``{argtuple: value}`` (a missing entry, or
|
|
100
|
+
any non-denoting argument, makes the application non-denoting). ``predicates`` maps
|
|
101
|
+
``(name, arity)`` to a set of ``outer`` tuples.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
outer: Tuple[Any, ...]
|
|
105
|
+
existing: FrozenSet[Any]
|
|
106
|
+
constants: Mapping[str, Any] = field(default_factory=dict)
|
|
107
|
+
functions: Mapping[Tuple[str, int], Mapping[Tuple[Any, ...], Any]] = field(default_factory=dict)
|
|
108
|
+
predicates: Mapping[Tuple[str, int], FrozenSet[Tuple[Any, ...]]] = field(default_factory=dict)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _term_value(term: Node, model: FreeModel, assignment: Mapping[str, Any]):
|
|
112
|
+
"""Evaluate a term to an outer-domain element, or :data:`NONDENOTING`."""
|
|
113
|
+
if isinstance(term, Variable):
|
|
114
|
+
return assignment.get(term.name, NONDENOTING)
|
|
115
|
+
if isinstance(term, Constant):
|
|
116
|
+
return model.constants.get(term.name, NONDENOTING)
|
|
117
|
+
if isinstance(term, Number):
|
|
118
|
+
# a numeral is the constant of its VALUE (1 and 1.0 are one), absent = non-denoting
|
|
119
|
+
return model.constants.get(numeral_key(term.value), NONDENOTING)
|
|
120
|
+
if isinstance(term, Function):
|
|
121
|
+
args = tuple(_term_value(a, model, assignment) for a in term.args)
|
|
122
|
+
if any(v is NONDENOTING for v in args):
|
|
123
|
+
return NONDENOTING
|
|
124
|
+
table = model.functions.get((term.name, len(term.args)), {})
|
|
125
|
+
return table.get(args, NONDENOTING)
|
|
126
|
+
raise TypeError(f"free_logic: not a term: {type(term).__name__}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def free_satisfies(formula: Node, model: FreeModel,
|
|
130
|
+
assignment: Optional[Mapping[str, Any]] = None,
|
|
131
|
+
policy: str = "negative") -> bool:
|
|
132
|
+
"""Return whether ``model`` satisfies ``formula`` under free-logic semantics.
|
|
133
|
+
|
|
134
|
+
Quantifiers range over ``model.existing``; ``E!(t)`` is true iff ``t`` denotes an
|
|
135
|
+
existing object; an atom with a non-denoting term is handled per ``policy``
|
|
136
|
+
(``"negative"`` / ``"positive"`` / ``"supervaluation"`` — see the module
|
|
137
|
+
docstring). Under ``"supervaluation"`` this delegates to :func:`_supervaluate`,
|
|
138
|
+
a bounded search over every classical completion of the formula's gap atoms
|
|
139
|
+
(capped at :data:`SUPERVALUATION_MAX_GAPS` distinct gaps).
|
|
140
|
+
|
|
141
|
+
A numeral is a constant identified by its VALUE (``1`` and ``1.0`` are one, looked up
|
|
142
|
+
under ``'1'`` in ``model.constants``, and non-denoting when absent there).
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
NotImplementedError: ``formula`` holds a :class:`Number` and a constant spelled like
|
|
146
|
+
its value (``Number(1)`` next to ``Constant('1')``): ``model.constants`` has ONE
|
|
147
|
+
entry ``'1'`` for both, so the kit refuses to merge a numeral with the constant
|
|
148
|
+
of the same spelling.
|
|
149
|
+
"""
|
|
150
|
+
if assignment is None:
|
|
151
|
+
assignment = {}
|
|
152
|
+
if policy not in ("negative", "positive", "supervaluation"):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"free_satisfies: unknown policy {policy!r} (negative / positive / supervaluation).")
|
|
155
|
+
_refuse_numeral_constant_pair([formula], "semantics.free_logic")
|
|
156
|
+
return _satisfies(formula, model, assignment, policy)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _satisfies(formula: Node, model: FreeModel, assignment: Mapping[str, Any],
|
|
160
|
+
policy: str) -> bool:
|
|
161
|
+
""":func:`free_satisfies` without the checks of the formula as a whole, which the
|
|
162
|
+
entry made: every recursive step and the model search land here."""
|
|
163
|
+
if policy == "supervaluation":
|
|
164
|
+
return _supervaluate(formula, model, assignment)
|
|
165
|
+
|
|
166
|
+
if isinstance(formula, Atom):
|
|
167
|
+
return _atom(formula, model, assignment, policy)
|
|
168
|
+
if isinstance(formula, Not):
|
|
169
|
+
return not _satisfies(formula.formula, model, assignment, policy)
|
|
170
|
+
if isinstance(formula, And):
|
|
171
|
+
return (_satisfies(formula.left, model, assignment, policy)
|
|
172
|
+
and _satisfies(formula.right, model, assignment, policy))
|
|
173
|
+
if isinstance(formula, Or):
|
|
174
|
+
return (_satisfies(formula.left, model, assignment, policy)
|
|
175
|
+
or _satisfies(formula.right, model, assignment, policy))
|
|
176
|
+
if isinstance(formula, Xor):
|
|
177
|
+
return (_satisfies(formula.left, model, assignment, policy)
|
|
178
|
+
!= _satisfies(formula.right, model, assignment, policy))
|
|
179
|
+
if isinstance(formula, Implies):
|
|
180
|
+
return ((not _satisfies(formula.left, model, assignment, policy))
|
|
181
|
+
or _satisfies(formula.right, model, assignment, policy))
|
|
182
|
+
if isinstance(formula, Iff):
|
|
183
|
+
return (_satisfies(formula.left, model, assignment, policy)
|
|
184
|
+
== _satisfies(formula.right, model, assignment, policy))
|
|
185
|
+
if isinstance(formula, Quantifier):
|
|
186
|
+
var = formula.variable.name
|
|
187
|
+
results = (
|
|
188
|
+
_satisfies(formula.formula, model, {**assignment, var: d}, policy)
|
|
189
|
+
for d in model.existing
|
|
190
|
+
)
|
|
191
|
+
if formula.type in _FORALL:
|
|
192
|
+
return all(results)
|
|
193
|
+
if formula.type in _EXISTS:
|
|
194
|
+
return any(results)
|
|
195
|
+
raise ValueError(f"free_satisfies: unknown quantifier {formula.type!r}")
|
|
196
|
+
raise TypeError(f"free_satisfies: unsupported node {type(formula).__name__}")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _atom(atom: Atom, model: FreeModel, assignment: Mapping[str, Any], policy: str) -> bool:
|
|
200
|
+
"""Truth value of an atom, applying the non-denoting policy."""
|
|
201
|
+
constant = _truth_value(atom)
|
|
202
|
+
if constant is not None:
|
|
203
|
+
return constant # `$true` / `$false` have no term, so nothing can fail to denote
|
|
204
|
+
values = tuple(_term_value(a, model, assignment) for a in atom.args)
|
|
205
|
+
nondenoting = any(v is NONDENOTING for v in values)
|
|
206
|
+
|
|
207
|
+
if atom.predicate == _EXISTS_PRED and len(atom.args) == 1:
|
|
208
|
+
return (not nondenoting) and values[0] in model.existing
|
|
209
|
+
if atom.predicate in ("=", "≠"):
|
|
210
|
+
if nondenoting:
|
|
211
|
+
# negative: t=t false when t non-denoting; positive: self-identity true.
|
|
212
|
+
eq = (policy == "positive" and atom.args[0] == atom.args[1])
|
|
213
|
+
return eq if atom.predicate == "=" else (not eq)
|
|
214
|
+
eq = values[0] == values[1]
|
|
215
|
+
return eq if atom.predicate == "=" else (not eq)
|
|
216
|
+
if nondenoting:
|
|
217
|
+
return False # negative & positive agree for predicates
|
|
218
|
+
relation = model.predicates.get((atom.predicate, len(atom.args)), frozenset())
|
|
219
|
+
return values in relation
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def free_holds(formula: Node, model: FreeModel, policy: str = "negative") -> bool:
|
|
223
|
+
"""Convenience: ``free_satisfies`` of a closed ``formula`` (empty assignment)."""
|
|
224
|
+
return free_satisfies(formula, model, {}, policy)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Supervaluationism — policy="supervaluation" for free_satisfies.
|
|
229
|
+
#
|
|
230
|
+
# A "gap atom" is a ground occurrence of an ordinary predicate atom (never ``=``,
|
|
231
|
+
# ``≠`` or ``E!`` — those are always decided, per the negative-free-logic reading,
|
|
232
|
+
# regardless of policy: identity/existence are not treated as an additional locus
|
|
233
|
+
# of gap here) that has a non-denoting argument. A *precisification* assigns each
|
|
234
|
+
# distinct gap atom a classical truth value, consistently everywhere it recurs in
|
|
235
|
+
# the formula (the same ground atom under a quantifier, e.g. one instantiated at
|
|
236
|
+
# the same domain element from two different sub-formulas, gets the same value).
|
|
237
|
+
# The formula is supertrue / superfalse / a gap according to whether every, no, or
|
|
238
|
+
# some-but-not-all of the 2**n precisifications make it true — see the module
|
|
239
|
+
# docstring for the running P(e) ∨ ¬P(e) example.
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
#: Hard cap on the number of distinct gap atoms a single supervaluation may
|
|
243
|
+
#: enumerate (2**n precisifications to check, each a full recursive evaluation of
|
|
244
|
+
#: the formula) — exceeding it raises rather than silently truncating the search.
|
|
245
|
+
SUPERVALUATION_MAX_GAPS = 16
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _term_repr(term: Node, assignment: Mapping[str, Any]) -> Tuple[Any, ...]:
|
|
249
|
+
"""A hashable structural key identifying ``term`` under ``assignment``.
|
|
250
|
+
|
|
251
|
+
Unlike :func:`_term_value`, this never collapses to :data:`NONDENOTING` — two
|
|
252
|
+
syntactically different non-denoting terms (e.g. two different constants
|
|
253
|
+
absent from ``model.constants``) get different keys, so they can be
|
|
254
|
+
precisified independently, while the same term recurring (e.g. under a
|
|
255
|
+
quantifier, once per bound variable) gets the same key both times, so
|
|
256
|
+
:func:`_supervaluate` can hold it to a single consistent precisified value.
|
|
257
|
+
"""
|
|
258
|
+
if isinstance(term, Variable):
|
|
259
|
+
return ("var", assignment.get(term.name))
|
|
260
|
+
if isinstance(term, Constant):
|
|
261
|
+
return ("const", term.name)
|
|
262
|
+
if isinstance(term, Number):
|
|
263
|
+
return ("num", numeral_key(term.value))
|
|
264
|
+
if isinstance(term, Function):
|
|
265
|
+
return ("fn", term.name, tuple(_term_repr(a, assignment) for a in term.args))
|
|
266
|
+
raise TypeError(f"free_logic: not a term: {type(term).__name__}")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _is_gap_candidate(atom: Atom) -> bool:
|
|
270
|
+
"""Whether ``atom`` is ever subject to precisification (identity/E!/$true/$false never are)."""
|
|
271
|
+
if _truth_value(atom) is not None:
|
|
272
|
+
return False
|
|
273
|
+
if atom.predicate == _EXISTS_PRED and len(atom.args) == 1:
|
|
274
|
+
return False
|
|
275
|
+
if atom.predicate in ("=", "≠"):
|
|
276
|
+
return False
|
|
277
|
+
return True
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _gap_key(atom: Atom, assignment: Mapping[str, Any]) -> Tuple[Any, ...]:
|
|
281
|
+
"""The precisification-dict key for a gappy occurrence of ``atom``."""
|
|
282
|
+
return (atom.predicate, tuple(_term_repr(a, assignment) for a in atom.args))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _collect_gap_atoms(formula: Node, model: FreeModel,
|
|
286
|
+
assignment: Mapping[str, Any], gaps: set) -> None:
|
|
287
|
+
"""Populate ``gaps`` with the key of every non-denoting gap-candidate atom in
|
|
288
|
+
``formula``, instantiating quantifiers over ``model.existing`` (finite domain)."""
|
|
289
|
+
if isinstance(formula, Atom):
|
|
290
|
+
if not _is_gap_candidate(formula):
|
|
291
|
+
return
|
|
292
|
+
values = tuple(_term_value(a, model, assignment) for a in formula.args)
|
|
293
|
+
if any(v is NONDENOTING for v in values):
|
|
294
|
+
gaps.add(_gap_key(formula, assignment))
|
|
295
|
+
return
|
|
296
|
+
if isinstance(formula, Quantifier):
|
|
297
|
+
var = formula.variable.name
|
|
298
|
+
for d in model.existing:
|
|
299
|
+
_collect_gap_atoms(formula.formula, model, {**assignment, var: d}, gaps)
|
|
300
|
+
return
|
|
301
|
+
for child in formula._child_nodes():
|
|
302
|
+
_collect_gap_atoms(child, model, assignment, gaps)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _atom_precisified(atom: Atom, model: FreeModel, assignment: Mapping[str, Any],
|
|
306
|
+
precisification: Mapping[Tuple[Any, ...], bool]) -> bool:
|
|
307
|
+
"""Truth value of ``atom`` under one precisification (identity/E! bypass it)."""
|
|
308
|
+
if not _is_gap_candidate(atom):
|
|
309
|
+
return _atom(atom, model, assignment, "negative")
|
|
310
|
+
values = tuple(_term_value(a, model, assignment) for a in atom.args)
|
|
311
|
+
if any(v is NONDENOTING for v in values):
|
|
312
|
+
return precisification[_gap_key(atom, assignment)]
|
|
313
|
+
relation = model.predicates.get((atom.predicate, len(atom.args)), frozenset())
|
|
314
|
+
return values in relation
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _eval_precisified(formula: Node, model: FreeModel, assignment: Mapping[str, Any],
|
|
318
|
+
precisification: Mapping[Tuple[Any, ...], bool]) -> bool:
|
|
319
|
+
"""``free_satisfies``'s recursion, but gap atoms resolve via ``precisification``."""
|
|
320
|
+
if isinstance(formula, Atom):
|
|
321
|
+
return _atom_precisified(formula, model, assignment, precisification)
|
|
322
|
+
if isinstance(formula, Not):
|
|
323
|
+
return not _eval_precisified(formula.formula, model, assignment, precisification)
|
|
324
|
+
if isinstance(formula, And):
|
|
325
|
+
return (_eval_precisified(formula.left, model, assignment, precisification)
|
|
326
|
+
and _eval_precisified(formula.right, model, assignment, precisification))
|
|
327
|
+
if isinstance(formula, Or):
|
|
328
|
+
return (_eval_precisified(formula.left, model, assignment, precisification)
|
|
329
|
+
or _eval_precisified(formula.right, model, assignment, precisification))
|
|
330
|
+
if isinstance(formula, Xor):
|
|
331
|
+
return (_eval_precisified(formula.left, model, assignment, precisification)
|
|
332
|
+
!= _eval_precisified(formula.right, model, assignment, precisification))
|
|
333
|
+
if isinstance(formula, Implies):
|
|
334
|
+
return ((not _eval_precisified(formula.left, model, assignment, precisification))
|
|
335
|
+
or _eval_precisified(formula.right, model, assignment, precisification))
|
|
336
|
+
if isinstance(formula, Iff):
|
|
337
|
+
return (_eval_precisified(formula.left, model, assignment, precisification)
|
|
338
|
+
== _eval_precisified(formula.right, model, assignment, precisification))
|
|
339
|
+
if isinstance(formula, Quantifier):
|
|
340
|
+
var = formula.variable.name
|
|
341
|
+
results = (
|
|
342
|
+
_eval_precisified(formula.formula, model, {**assignment, var: d}, precisification)
|
|
343
|
+
for d in model.existing
|
|
344
|
+
)
|
|
345
|
+
if formula.type in _FORALL:
|
|
346
|
+
return all(results)
|
|
347
|
+
if formula.type in _EXISTS:
|
|
348
|
+
return any(results)
|
|
349
|
+
raise ValueError(f"free_satisfies: unknown quantifier {formula.type!r}")
|
|
350
|
+
raise TypeError(f"free_satisfies: unsupported node {type(formula).__name__}")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _supervaluate(formula: Node, model: FreeModel, assignment: Mapping[str, Any]) -> bool:
|
|
354
|
+
"""``policy="supervaluation"`` entry point: enumerate every precisification.
|
|
355
|
+
|
|
356
|
+
Collects the formula's gap atoms (quantifiers instantiated over
|
|
357
|
+
``model.existing``), enumerates all ``2**n`` classical completions, and
|
|
358
|
+
evaluates the formula under each with :func:`_eval_precisified`. Returns
|
|
359
|
+
``True`` iff every completion agrees on ``True`` (supertrue), ``False`` iff
|
|
360
|
+
every completion agrees on ``False`` (superfalse) OR the completions disagree
|
|
361
|
+
(a genuine gap — see the module docstring for why this collapses to the same
|
|
362
|
+
``False`` a lone gappy atom already returns under ``"negative"``).
|
|
363
|
+
"""
|
|
364
|
+
gaps: set = set()
|
|
365
|
+
_collect_gap_atoms(formula, model, assignment, gaps)
|
|
366
|
+
if len(gaps) > SUPERVALUATION_MAX_GAPS:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"free_satisfies: supervaluation over {len(gaps)} distinct gap atom(s) exceeds "
|
|
369
|
+
f"SUPERVALUATION_MAX_GAPS = {SUPERVALUATION_MAX_GAPS} (2**n precisifications to "
|
|
370
|
+
"check, each a full evaluation of the formula); reformulate with fewer "
|
|
371
|
+
"non-denoting ground atoms, or evaluate under policy='negative' / 'positive' "
|
|
372
|
+
"instead.")
|
|
373
|
+
gap_list = sorted(gaps, key=repr) # order just needs to be fixed across the loop below
|
|
374
|
+
|
|
375
|
+
saw_true = False
|
|
376
|
+
saw_false = False
|
|
377
|
+
for bits in product((False, True), repeat=len(gap_list)):
|
|
378
|
+
precisification = dict(zip(gap_list, bits))
|
|
379
|
+
if _eval_precisified(formula, model, assignment, precisification):
|
|
380
|
+
saw_true = True
|
|
381
|
+
else:
|
|
382
|
+
saw_false = True
|
|
383
|
+
if saw_true and saw_false:
|
|
384
|
+
return False # already a gap; no need to check the rest
|
|
385
|
+
return saw_true and not saw_false
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
# Bounded model search — the Mace4-style partner of free_satisfies, in the
|
|
390
|
+
# style of semantics/modelfinder.py (read it first: _Signature, _candidate_count,
|
|
391
|
+
# _interpretations, find_model / find_countermodel / is_valid_finite).
|
|
392
|
+
#
|
|
393
|
+
# Symmetry breaking (roadmap C23, ``symmetry_breaking=True`` below, the default):
|
|
394
|
+
# hand-ports modelfinder's LNH generator to this module's PARTIAL constant tables
|
|
395
|
+
# — see ``_canonical_constant_assignments`` and ``_canonical_candidate_models``,
|
|
396
|
+
# and ``modelfinder._canonical_interpretations``'s docstring for the full
|
|
397
|
+
# soundness argument (and the hand-checked counterexample showing why functions
|
|
398
|
+
# are deliberately NOT also LNH-reduced here, for the identical reason).
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
#: A partial function's table has, per argument tuple, (n+1) choices (one of the n
|
|
402
|
+
#: domain elements, or "undefined"/non-denoting — see _term_value) and n**arity
|
|
403
|
+
#: argument tuples, so the table space is (n+1)**(n**arity): already ~4·10^7 at
|
|
404
|
+
#: n=3, arity=3. Rather than let max_candidates silently starve every domain size
|
|
405
|
+
#: for a function this shape (see _search's "every size skipped" check), functions
|
|
406
|
+
#: above this arity are rejected outright, up front, with a clean message.
|
|
407
|
+
MAX_FUNCTION_ARITY = 2
|
|
408
|
+
|
|
409
|
+
#: Interpretations visited per domain size before that size is skipped (mirrors
|
|
410
|
+
#: modelfinder.MAX_CANDIDATES). Free logic multiplies further by the number of
|
|
411
|
+
#: existing/outer splits tried (2**k under domain_split="any", 1 under "total"), so
|
|
412
|
+
#: a signature that modelfinder would happily search at some k can cost more here.
|
|
413
|
+
MAX_CANDIDATES = 1 << 20
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _free_var_names(node: Node, bound: FrozenSet[str] = frozenset()) -> set:
|
|
417
|
+
"""Return the names of variables occurring free in ``node`` (Quantifier binds).
|
|
418
|
+
|
|
419
|
+
Mirrors modelfinder._free_var_names, restricted to the node types free_satisfies
|
|
420
|
+
supports (no Count/Cardinality/SlashedExists binders exist in this fragment).
|
|
421
|
+
"""
|
|
422
|
+
if isinstance(node, Variable):
|
|
423
|
+
return set() if node.name in bound else {node.name}
|
|
424
|
+
if isinstance(node, Quantifier):
|
|
425
|
+
return _free_var_names(node.formula, bound | {node.variable.name})
|
|
426
|
+
names: set = set()
|
|
427
|
+
for child in node._child_nodes():
|
|
428
|
+
names |= _free_var_names(child, bound)
|
|
429
|
+
return names
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _universal_closure(node: Node) -> Node:
|
|
433
|
+
"""Wrap ``node`` in ∀ for each free variable (deterministic order).
|
|
434
|
+
|
|
435
|
+
∀ ranges over ``existing`` under free-logic semantics (see the module
|
|
436
|
+
docstring), so the closure of ONE sentence says it holds under every assignment of
|
|
437
|
+
its free variables to existing objects. It is a tool for a single sentence that is
|
|
438
|
+
evaluated, never the reading of a problem: the search routes read a free variable as
|
|
439
|
+
a parameter shared by every formula of the call (:func:`_with_parameters`).
|
|
440
|
+
"""
|
|
441
|
+
result = node
|
|
442
|
+
for name in sorted(_free_var_names(node), reverse=True):
|
|
443
|
+
result = Quantifier("∀", Variable(name), result)
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _signature(node: Node) -> Tuple[set, set, set]:
|
|
448
|
+
"""Return ``(constant names, {(name, arity)} functions, {(name, arity)} predicates)``.
|
|
449
|
+
|
|
450
|
+
Mirrors modelfinder._Signature.scan for the node types free_satisfies supports.
|
|
451
|
+
``=``, ``≠`` and the existence predicate ``E!`` are handled natively by
|
|
452
|
+
free_satisfies / _atom and are never collected as ordinary predicates to
|
|
453
|
+
interpret (interpreting ``E!`` would be incoherent: it is defined FROM
|
|
454
|
+
``existing``, not a free table).
|
|
455
|
+
"""
|
|
456
|
+
constants: set = set()
|
|
457
|
+
functions: set = set()
|
|
458
|
+
predicates: set = set()
|
|
459
|
+
|
|
460
|
+
def scan(n: Node) -> None:
|
|
461
|
+
if isinstance(n, Constant):
|
|
462
|
+
constants.add(n.name)
|
|
463
|
+
elif isinstance(n, Number):
|
|
464
|
+
constants.add(numeral_key(n.value))
|
|
465
|
+
elif isinstance(n, Function):
|
|
466
|
+
functions.add((n.name, len(n.args)))
|
|
467
|
+
for a in n.args:
|
|
468
|
+
scan(a)
|
|
469
|
+
elif isinstance(n, Atom):
|
|
470
|
+
if n.predicate not in ("=", "≠", _EXISTS_PRED) and _truth_value(n) is None:
|
|
471
|
+
predicates.add((n.predicate, len(n.args)))
|
|
472
|
+
for a in n.args:
|
|
473
|
+
scan(a)
|
|
474
|
+
else:
|
|
475
|
+
for child in n._child_nodes():
|
|
476
|
+
scan(child)
|
|
477
|
+
|
|
478
|
+
scan(node)
|
|
479
|
+
return constants, functions, predicates
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _candidate_count(n_constants: int, functions: set, predicates: set,
|
|
483
|
+
k: int, domain_split: str) -> int:
|
|
484
|
+
"""The number of distinct FreeModel candidates of outer-domain size ``k``.
|
|
485
|
+
|
|
486
|
+
Existing/outer splits (``2**k`` under ``"any"`` — every subset, INCLUDING the
|
|
487
|
+
empty set: free logic, unlike classical FOL, does not require an existing
|
|
488
|
+
object, so an empty inner domain is a legitimate model, see
|
|
489
|
+
:func:`_existing_subsets`; exactly ``1`` under ``"total"``) times
|
|
490
|
+
``(k+1)**n_constants`` partial constant assignments (k denoting choices +
|
|
491
|
+
non-denoting) times, per function of arity a, ``(k+1)**(k**a)`` partial tables,
|
|
492
|
+
times, per predicate of arity a, ``2**(k**a)`` extensional choices over the
|
|
493
|
+
OUTER domain (predicates apply to outer elements whether or not they exist —
|
|
494
|
+
see ``_atom``).
|
|
495
|
+
"""
|
|
496
|
+
splits = (1 << k) if domain_split == "any" else 1
|
|
497
|
+
total = splits * ((k + 1) ** n_constants)
|
|
498
|
+
for _, arity in functions:
|
|
499
|
+
total *= (k + 1) ** (k ** arity)
|
|
500
|
+
for _, arity in predicates:
|
|
501
|
+
total *= 1 << (k ** arity)
|
|
502
|
+
return total
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _canonical_constant_count(n_slots: int, k: int) -> int:
|
|
506
|
+
"""The EXACT number of LNH-canonical PARTIAL constant assignments
|
|
507
|
+
:func:`_canonical_constant_assignments` yields for ``n_slots`` constant names
|
|
508
|
+
over a ``k``-element domain — computed analytically (``O(n_slots * k)``
|
|
509
|
+
arithmetic, no enumeration), mirroring
|
|
510
|
+
:func:`~unicode_logic_kit.semantics.modelfinder._lnh_constant_count`'s DP but with
|
|
511
|
+
the extra "non-denoting" (``None``) option :func:`_canonical_constant_assignments`
|
|
512
|
+
offers at every cell alongside :func:`~unicode_logic_kit.semantics.modelfinder._lnh_choices`'s
|
|
513
|
+
domain-value range — a cell can go non-denoting, repeat one of the ``j``
|
|
514
|
+
already-used domain values, or (if ``j < k``) introduce the next unused one.
|
|
515
|
+
``None`` is never subject to the relabeling argument (a term either denotes an
|
|
516
|
+
outer-domain element or it does not, independent of which permutation would
|
|
517
|
+
rename that element were it to denote one — see
|
|
518
|
+
:func:`_canonical_constant_assignments`'s docstring), so it is simply one more
|
|
519
|
+
option per cell, orthogonal to ``j``. Hand-checked for n_slots=1,k=1 -> 2 in
|
|
520
|
+
``tests/test_modelfinder_symmetry.py``.
|
|
521
|
+
"""
|
|
522
|
+
if n_slots == 0:
|
|
523
|
+
return 1
|
|
524
|
+
dp = [1] * (k + 1)
|
|
525
|
+
for _ in range(n_slots):
|
|
526
|
+
new_dp = [0] * (k + 1)
|
|
527
|
+
for j in range(k + 1):
|
|
528
|
+
total = (1 + j) * dp[j] # non-denoting, or repeat a used value
|
|
529
|
+
if j < k:
|
|
530
|
+
total += dp[j + 1] # or introduce the next still-unused one
|
|
531
|
+
new_dp[j] = total
|
|
532
|
+
dp = new_dp
|
|
533
|
+
return dp[0]
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _canonical_candidate_count(n_constants: int, functions: set, predicates: set,
|
|
537
|
+
k: int, domain_split: str) -> int:
|
|
538
|
+
"""The EXACT number of FreeModel candidates :func:`_canonical_candidate_models`
|
|
539
|
+
yields for this signature over an outer domain of size ``k`` — the analytic
|
|
540
|
+
pre-flight count backing :func:`_search`'s "skip this size" check when
|
|
541
|
+
``symmetry_breaking=True``, playing the same role :func:`_candidate_count` plays
|
|
542
|
+
for the unbroken enumeration (see
|
|
543
|
+
:func:`~unicode_logic_kit.semantics.modelfinder._canonical_candidate_count` for the
|
|
544
|
+
classical-FOL analogue). Only the constants factor changes, via
|
|
545
|
+
:func:`_canonical_constant_count`; functions and predicates keep the exact same
|
|
546
|
+
raw factors :func:`_candidate_count` uses, because
|
|
547
|
+
:func:`_canonical_candidate_models` leaves those fully exhaustive. Being exact
|
|
548
|
+
means this alone decides whether a size is searched — a signature with few or no
|
|
549
|
+
constants (where LNH cannot reduce anything) is skipped exactly as cheaply as it
|
|
550
|
+
was before symmetry breaking existed, instead of live-enumerating candidates and
|
|
551
|
+
calling :func:`free_satisfies` on each up to ``max_candidates`` before finding
|
|
552
|
+
that out.
|
|
553
|
+
"""
|
|
554
|
+
splits = (1 << k) if domain_split == "any" else 1
|
|
555
|
+
total = splits * _canonical_constant_count(n_constants, k)
|
|
556
|
+
for _, arity in functions:
|
|
557
|
+
total *= (k + 1) ** (k ** arity)
|
|
558
|
+
for _, arity in predicates:
|
|
559
|
+
total *= 1 << (k ** arity)
|
|
560
|
+
return total
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _existing_subsets(domain: Tuple[Any, ...], domain_split: str):
|
|
564
|
+
"""Yield every candidate ``existing`` set for ``domain`` under ``domain_split``.
|
|
565
|
+
|
|
566
|
+
``"any"`` — every subset of the outer domain, INCLUDING the empty set (an
|
|
567
|
+
empty inner domain is a legitimate free-logic model — ∀ vacuously true, ∃
|
|
568
|
+
always false there, per free_satisfies). ``"total"`` — only
|
|
569
|
+
``existing == outer`` (everything denotable exists), the classical-FOL-
|
|
570
|
+
equivalent reading; with no constants/functions in the formula this makes the
|
|
571
|
+
search coincide with :mod:`~unicode_logic_kit.semantics.modelfinder` exactly
|
|
572
|
+
(see the differential test in tests/test_free_logic_search.py).
|
|
573
|
+
"""
|
|
574
|
+
if domain_split == "total":
|
|
575
|
+
yield frozenset(domain)
|
|
576
|
+
return
|
|
577
|
+
if domain_split != "any":
|
|
578
|
+
raise ValueError(
|
|
579
|
+
f"free_logic model search: unknown domain_split {domain_split!r} "
|
|
580
|
+
'("any" / "total").')
|
|
581
|
+
n = len(domain)
|
|
582
|
+
for mask in range(1 << n):
|
|
583
|
+
yield frozenset(domain[i] for i in range(n) if (mask >> i) & 1)
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _constant_assignments(domain: Tuple[Any, ...], const_names: List[str]):
|
|
587
|
+
"""Yield every partial constant assignment: each name to a domain element, or omitted."""
|
|
588
|
+
options = list(domain) + [None] # None = "omit" = non-denoting
|
|
589
|
+
for choice in product(options, repeat=len(const_names)):
|
|
590
|
+
yield {name: val for name, val in zip(const_names, choice) if val is not None}
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _function_interpretations(domain: Tuple[Any, ...], func_sig: List[Tuple[str, int]]):
|
|
594
|
+
"""Yield every partial function-table dict for ``func_sig`` over ``domain``.
|
|
595
|
+
|
|
596
|
+
Each argument tuple maps to a domain element, or is omitted (the application is
|
|
597
|
+
non-denoting on that tuple — see ``_term_value``).
|
|
598
|
+
"""
|
|
599
|
+
if not func_sig:
|
|
600
|
+
yield {}
|
|
601
|
+
return
|
|
602
|
+
per_function = []
|
|
603
|
+
for _, arity in func_sig:
|
|
604
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
605
|
+
value_options = list(domain) + [None]
|
|
606
|
+
per_function.append(list(product(value_options, repeat=len(arg_tuples))))
|
|
607
|
+
for combo in product(*per_function):
|
|
608
|
+
functions = {}
|
|
609
|
+
for (name, arity), values in zip(func_sig, combo):
|
|
610
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
611
|
+
functions[(name, arity)] = {
|
|
612
|
+
at: v for at, v in zip(arg_tuples, values) if v is not None
|
|
613
|
+
}
|
|
614
|
+
yield functions
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _predicate_interpretations(domain: Tuple[Any, ...], pred_sig: List[Tuple[str, int]]):
|
|
618
|
+
"""Yield every predicate-table dict for ``pred_sig``: a subset of outer**arity each."""
|
|
619
|
+
if not pred_sig:
|
|
620
|
+
yield {}
|
|
621
|
+
return
|
|
622
|
+
per_predicate = []
|
|
623
|
+
for _, arity in pred_sig:
|
|
624
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
625
|
+
per_predicate.append(list(product((False, True), repeat=len(arg_tuples))))
|
|
626
|
+
for combo in product(*per_predicate):
|
|
627
|
+
predicates = {}
|
|
628
|
+
for (name, arity), mask in zip(pred_sig, combo):
|
|
629
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
630
|
+
predicates[(name, arity)] = frozenset(t for t, inc in zip(arg_tuples, mask) if inc)
|
|
631
|
+
yield predicates
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _candidate_models(domain, const_names, func_sig, pred_sig, domain_split):
|
|
635
|
+
"""Yield every FreeModel candidate over ``domain`` for the given signature."""
|
|
636
|
+
for existing in _existing_subsets(domain, domain_split):
|
|
637
|
+
for constants in _constant_assignments(domain, const_names):
|
|
638
|
+
for functions in _function_interpretations(domain, func_sig):
|
|
639
|
+
for predicates in _predicate_interpretations(domain, pred_sig):
|
|
640
|
+
yield FreeModel(outer=domain, existing=existing, constants=constants,
|
|
641
|
+
functions=functions, predicates=predicates)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _canonical_constant_assignments(domain: Tuple[Any, ...], const_names: List[str]):
|
|
645
|
+
"""Yield every partial constant assignment, LNH-canonical (roadmap C23).
|
|
646
|
+
|
|
647
|
+
The free-logic analogue of ``modelfinder._canonical_interpretations``,
|
|
648
|
+
restricted — like it — to CONSTANTS only; see that function's docstring for
|
|
649
|
+
the hand-checked counterexample showing why a flat per-cell LNH cap is sound
|
|
650
|
+
for an argument-free cell (a constant) but NOT for a function's table (its
|
|
651
|
+
cells are indexed by argument tuples that are themselves domain elements,
|
|
652
|
+
subject to the same relabeling as the stored value).
|
|
653
|
+
|
|
654
|
+
Each cell is offered, in this order: "non-denoting" first (never subject to
|
|
655
|
+
the relabeling argument at all — a term either denotes some outer-domain
|
|
656
|
+
element or it does not, independent of *which* permutation would rename that
|
|
657
|
+
element were it to denote one), then :func:`~unicode_logic_kit.semantics.modelfinder._lnh_choices`'s
|
|
658
|
+
domain-value range: an already-used domain value, or the smallest still-unused
|
|
659
|
+
one. Functions, predicates and the existing/outer split are exhaustively
|
|
660
|
+
enumerated around this generator by the caller
|
|
661
|
+
(:func:`_canonical_candidate_models`), exactly as
|
|
662
|
+
``modelfinder._canonical_interpretations`` leaves functions and predicates
|
|
663
|
+
untouched — see its docstring for why that keeps the search complete.
|
|
664
|
+
"""
|
|
665
|
+
k = len(domain)
|
|
666
|
+
n = len(const_names)
|
|
667
|
+
|
|
668
|
+
def backtrack(i: int, next_new: int, values: list):
|
|
669
|
+
if i == n:
|
|
670
|
+
yield {name: domain[v] for name, v in zip(const_names, values) if v is not None}
|
|
671
|
+
return
|
|
672
|
+
for v in (None,) + tuple(_lnh_choices(next_new, k)):
|
|
673
|
+
values.append(v)
|
|
674
|
+
new_next = next_new + 1 if (v is not None and v == next_new) else next_new
|
|
675
|
+
yield from backtrack(i + 1, new_next, values)
|
|
676
|
+
values.pop()
|
|
677
|
+
|
|
678
|
+
yield from backtrack(0, 0, [])
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _canonical_candidate_models(domain, const_names, func_sig, pred_sig, domain_split):
|
|
682
|
+
"""Yield every FreeModel candidate with LNH-canonical constants (roadmap C23).
|
|
683
|
+
|
|
684
|
+
Mirrors :func:`_candidate_models`, but the constant assignment comes from
|
|
685
|
+
:func:`_canonical_constant_assignments` instead of the exhaustive
|
|
686
|
+
:func:`_constant_assignments`; functions, predicates and the existing/outer
|
|
687
|
+
split stay fully exhaustive (see that function's docstring, and
|
|
688
|
+
``modelfinder._canonical_interpretations``'s, for why that keeps the search
|
|
689
|
+
sound and complete).
|
|
690
|
+
"""
|
|
691
|
+
for existing in _existing_subsets(domain, domain_split):
|
|
692
|
+
for constants in _canonical_constant_assignments(domain, const_names):
|
|
693
|
+
for functions in _function_interpretations(domain, func_sig):
|
|
694
|
+
for predicates in _predicate_interpretations(domain, pred_sig):
|
|
695
|
+
yield FreeModel(outer=domain, existing=existing, constants=constants,
|
|
696
|
+
functions=functions, predicates=predicates)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def _with_parameters(formulas: Sequence[Node]) -> Tuple[List[Node], List[Node]]:
|
|
700
|
+
"""Read every free variable of ``formulas`` as a PARAMETER: ``(formulas, guards)``.
|
|
701
|
+
|
|
702
|
+
The variable is replaced, in all the formulas together, by a constant of its own name
|
|
703
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`). A variable of free logic
|
|
704
|
+
ranges over the EXISTING objects, so each parameter comes with the guard ``E!(c)``
|
|
705
|
+
that a model has to satisfy too: ``c`` denotes and is in ``existing``. A model with
|
|
706
|
+
no existing object therefore satisfies no problem that has a free variable.
|
|
707
|
+
|
|
708
|
+
Raises:
|
|
709
|
+
NotImplementedError: a free variable has the spelling of a constant of ``formulas``.
|
|
710
|
+
"""
|
|
711
|
+
closed, parameters = parameterize(list(formulas), after_variables=True)
|
|
712
|
+
return closed, [Atom(_EXISTS_PRED, (constant,)) for constant in parameters.values()]
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _search(formulas: Sequence[Node], max_size: int, policy: str, domain_split: str,
|
|
716
|
+
max_candidates: int, symmetry_breaking: bool = True) -> Optional[FreeModel]:
|
|
717
|
+
"""Return the first FreeModel making every one of ``formulas`` true, or None.
|
|
718
|
+
|
|
719
|
+
``formulas`` must not have a free variable: the callers read each one as a parameter
|
|
720
|
+
first (:func:`_with_parameters`), before any formula is negated. The signature
|
|
721
|
+
(constants/functions/predicates) is collected from the formulas jointly, and every
|
|
722
|
+
domain size ``1..max_size`` is tried in turn.
|
|
723
|
+
|
|
724
|
+
``symmetry_breaking`` (default True, roadmap C23) enumerates constants with
|
|
725
|
+
:func:`_canonical_candidate_models` (LNH — see its docstring) instead of the
|
|
726
|
+
plain :func:`_candidate_models`. Because the LNH-reduced candidate count for a
|
|
727
|
+
size can be far below the analytic ``_candidate_count`` estimate, this path
|
|
728
|
+
uses :func:`_canonical_candidate_count` for the pre-flight "skip this size"
|
|
729
|
+
check instead — the EXACT count of what the canonical generator will yield,
|
|
730
|
+
computed analytically (``O(n_constants * domain size)`` arithmetic, no
|
|
731
|
+
enumeration, no ``free_satisfies`` calls), mirroring
|
|
732
|
+
``modelfinder.find_model``'s own analytic pre-flight. This is deliberately an
|
|
733
|
+
O(1)-per-size check, not a live count of the generator: a signature with few or
|
|
734
|
+
no constants (where LNH cannot reduce anything — functions and predicates stay
|
|
735
|
+
fully exhaustive either way) is skipped exactly as cheaply as it was before
|
|
736
|
+
symmetry breaking existed, instead of paying to enumerate and call
|
|
737
|
+
``free_satisfies`` on up to ``max_candidates`` models on every size just to
|
|
738
|
+
discover that a function- or predicate-dominated signature was never going to
|
|
739
|
+
fit the budget. A size counts as genuinely searched (``tried_any_size = True``
|
|
740
|
+
below) only if its exact canonical count is ``<= max_candidates`` (so it is
|
|
741
|
+
always FULLY enumerated, never truncated mid-generator), so the honesty
|
|
742
|
+
contract below is unchanged: if every size still ends up skipped, nothing was
|
|
743
|
+
actually searched, and a bare ``None`` would misleadingly look like a completed
|
|
744
|
+
bounded search — this raises instead (see the arity/message in the
|
|
745
|
+
ValueError). With ``symmetry_breaking=False`` the search is byte-for-byte the
|
|
746
|
+
original exhaustive one (the analytic pre-flight check, then
|
|
747
|
+
:func:`_candidate_models`).
|
|
748
|
+
"""
|
|
749
|
+
if policy not in ("negative", "positive", "supervaluation"):
|
|
750
|
+
raise ValueError(
|
|
751
|
+
f"free_logic model search: unknown policy {policy!r} "
|
|
752
|
+
"(negative / positive / supervaluation).")
|
|
753
|
+
closed = list(formulas)
|
|
754
|
+
_refuse_numeral_constant_pair(closed, "semantics.free_logic")
|
|
755
|
+
|
|
756
|
+
constants: set = set()
|
|
757
|
+
functions: set = set()
|
|
758
|
+
predicates: set = set()
|
|
759
|
+
for f in closed:
|
|
760
|
+
c, fn, p = _signature(f)
|
|
761
|
+
constants |= c
|
|
762
|
+
functions |= fn
|
|
763
|
+
predicates |= p
|
|
764
|
+
|
|
765
|
+
for name, arity in functions:
|
|
766
|
+
if arity > MAX_FUNCTION_ARITY:
|
|
767
|
+
raise ValueError(
|
|
768
|
+
f"free_logic model search: function {name}/{arity} exceeds "
|
|
769
|
+
f"MAX_FUNCTION_ARITY = {MAX_FUNCTION_ARITY}. A partial function table over "
|
|
770
|
+
f"an n-element domain has (n+1)**(n**arity) entries — arity {arity} blows up "
|
|
771
|
+
"too fast to enumerate at any useful domain size. Reformulate with "
|
|
772
|
+
"lower-arity functions/predicates, or curry the function.")
|
|
773
|
+
|
|
774
|
+
const_names = sorted(constants)
|
|
775
|
+
func_sig = sorted(functions)
|
|
776
|
+
pred_sig = sorted(predicates)
|
|
777
|
+
|
|
778
|
+
tried_any_size = False
|
|
779
|
+
for k in range(1, max_size + 1):
|
|
780
|
+
domain = tuple(range(k))
|
|
781
|
+
if symmetry_breaking:
|
|
782
|
+
if _canonical_candidate_count(len(const_names), functions, predicates,
|
|
783
|
+
k, domain_split) > max_candidates:
|
|
784
|
+
continue
|
|
785
|
+
tried_any_size = True
|
|
786
|
+
for model in _canonical_candidate_models(domain, const_names, func_sig,
|
|
787
|
+
pred_sig, domain_split):
|
|
788
|
+
if all(_satisfies(f, model, {}, policy) for f in closed):
|
|
789
|
+
return model
|
|
790
|
+
continue
|
|
791
|
+
if _candidate_count(len(const_names), functions, predicates, k, domain_split) > max_candidates:
|
|
792
|
+
continue
|
|
793
|
+
tried_any_size = True
|
|
794
|
+
for model in _candidate_models(domain, const_names, func_sig, pred_sig, domain_split):
|
|
795
|
+
if all(_satisfies(f, model, {}, policy) for f in closed):
|
|
796
|
+
return model
|
|
797
|
+
if not tried_any_size:
|
|
798
|
+
raise ValueError(
|
|
799
|
+
f"free_logic model search: every domain size 1..{max_size} exceeds "
|
|
800
|
+
f"max_candidates = {max_candidates} for this signature "
|
|
801
|
+
f"({len(const_names)} constant(s), {len(func_sig)} function(s), "
|
|
802
|
+
f"{len(pred_sig)} predicate(s)). Raise max_candidates, lower max_size, or "
|
|
803
|
+
"simplify the formula.")
|
|
804
|
+
return None
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def free_find_model(formula: Node, max_size: int = 3, *, policy: str = "negative",
|
|
808
|
+
domain_split: str = "any",
|
|
809
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
810
|
+
symmetry_breaking: bool = True) -> Optional[FreeModel]:
|
|
811
|
+
"""Return a FreeModel satisfying ``formula``, or None.
|
|
812
|
+
|
|
813
|
+
Mace4-style search (see
|
|
814
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_model`) over outer domain
|
|
815
|
+
sizes ``1..max_size``: for each size, every existing/outer split (``domain_split``
|
|
816
|
+
— ``"any"`` tries every subset including empty ``existing``; ``"total"`` forces
|
|
817
|
+
``existing == outer``), every partial constant/function assignment (a symbol may
|
|
818
|
+
be non-denoting), and every predicate extension over the OUTER domain (predicates
|
|
819
|
+
apply regardless of existence — see ``_atom``). ``formula``'s free variables are
|
|
820
|
+
PARAMETERS (see the module docstring): each is one existing object, reported in
|
|
821
|
+
``constants`` under the variable's name, so ``P(x) ∧ ¬P(y)`` has a model and a model
|
|
822
|
+
with no existing object is none for a formula with a free variable. A domain size
|
|
823
|
+
whose candidate count exceeds ``max_candidates`` is skipped; a function whose
|
|
824
|
+
arity exceeds :data:`MAX_FUNCTION_ARITY` is rejected outright (see :func:`_search`).
|
|
825
|
+
``None`` means "none found within the bounds", not "unsatisfiable" — free FOL is
|
|
826
|
+
exactly as undecidable as classical FOL.
|
|
827
|
+
|
|
828
|
+
``symmetry_breaking`` (default True, roadmap C23) enumerates CONSTANT
|
|
829
|
+
assignments with the LNH generator instead of exhaustively — see
|
|
830
|
+
:func:`_search`'s docstring for the live-counted budget it uses instead of the
|
|
831
|
+
plain pre-flight check, and
|
|
832
|
+
:func:`~unicode_logic_kit.semantics.modelfinder._canonical_interpretations`'s
|
|
833
|
+
docstring for why this stays sound and complete, and why FUNCTIONS are
|
|
834
|
+
deliberately not also LNH-reduced.
|
|
835
|
+
|
|
836
|
+
Raises:
|
|
837
|
+
NotImplementedError: a free variable has the spelling of a constant of ``formula``.
|
|
838
|
+
"""
|
|
839
|
+
closed, guards = _with_parameters([formula])
|
|
840
|
+
return _search(closed + guards, max_size, policy, domain_split, max_candidates,
|
|
841
|
+
symmetry_breaking)
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def free_countermodel(formula: Node, max_size: int = 3, *, policy: str = "negative",
|
|
845
|
+
domain_split: str = "any",
|
|
846
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
847
|
+
symmetry_breaking: bool = True) -> Optional[FreeModel]:
|
|
848
|
+
"""Return a FreeModel where ``formula`` is FALSE (a witness against its validity), or None.
|
|
849
|
+
|
|
850
|
+
Searches for a model of ``¬formula`` exactly like :func:`free_find_model`
|
|
851
|
+
(including its ``symmetry_breaking``); the search's success check
|
|
852
|
+
(``free_satisfies`` of the negation) already verifies the returned model, so a
|
|
853
|
+
non-None result is a definitive refutation of validity — the same
|
|
854
|
+
verify-before-return discipline as
|
|
855
|
+
:func:`~unicode_logic_kit.semantics.relevant.rel_countermodel` /
|
|
856
|
+
:func:`~unicode_logic_kit.semantics.conditional.cf_countermodel`.
|
|
857
|
+
|
|
858
|
+
A free variable is a parameter (see the module docstring) that is read BEFORE the
|
|
859
|
+
formula is negated: the countermodel falsifies ``formula`` under one assignment of
|
|
860
|
+
existing objects, reported in ``constants`` under the variables' names. (Negating
|
|
861
|
+
first would close ``¬formula`` as ``∀x ¬formula``, which an empty ``existing``
|
|
862
|
+
domain satisfies for every formula.)
|
|
863
|
+
|
|
864
|
+
Raises:
|
|
865
|
+
NotImplementedError: a free variable has the spelling of a constant of ``formula``.
|
|
866
|
+
"""
|
|
867
|
+
closed, guards = _with_parameters([formula])
|
|
868
|
+
return _search([Not(closed[0])] + guards, max_size, policy, domain_split, max_candidates,
|
|
869
|
+
symmetry_breaking)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def free_is_valid(formula: Node, max_size: int = 3, *, policy: str = "negative",
|
|
873
|
+
domain_split: str = "any",
|
|
874
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
875
|
+
symmetry_breaking: bool = True) -> bool:
|
|
876
|
+
"""Return True iff no free-logic countermodel to ``formula`` is found within the bound.
|
|
877
|
+
|
|
878
|
+
HONEST CONTRACT (mirroring
|
|
879
|
+
:func:`~unicode_logic_kit.semantics.relevant.rel_valid` /
|
|
880
|
+
:func:`~unicode_logic_kit.semantics.conditional.cf_valid`): ``False`` is
|
|
881
|
+
*definitive* — it is backed by an explicit, :func:`free_satisfies`-verified
|
|
882
|
+
countermodel from :func:`free_countermodel`, so ``formula`` is certainly not
|
|
883
|
+
valid under this free-logic semantics (for this ``policy``). ``True`` means only
|
|
884
|
+
"no countermodel with outer domain size ≤ ``max_size``" — free FOL is as
|
|
885
|
+
undecidable as classical FOL, so this is bounded evidence, not a proof; raising
|
|
886
|
+
``max_size`` never turns a ``False`` into a ``True``, only ever the reverse.
|
|
887
|
+
"""
|
|
888
|
+
return free_countermodel(formula, max_size, policy=policy, domain_split=domain_split,
|
|
889
|
+
max_candidates=max_candidates,
|
|
890
|
+
symmetry_breaking=symmetry_breaking) is None
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def free_entails(premises: Sequence[Node], conclusion: Node, max_size: int = 3, *,
|
|
894
|
+
policy: str = "negative", domain_split: str = "any",
|
|
895
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
896
|
+
symmetry_breaking: bool = True) -> bool:
|
|
897
|
+
"""Return True iff no bounded FreeModel satisfies every premise but not ``conclusion``.
|
|
898
|
+
|
|
899
|
+
Same honest contract as :func:`free_is_valid`: a ``False`` is backed by a
|
|
900
|
+
verified countermodel (every premise true there, ``conclusion`` false there, under
|
|
901
|
+
one assignment); ``True`` means only "none found within the bounds".
|
|
902
|
+
A free variable is a PARAMETER shared by every premise and the conclusion
|
|
903
|
+
(see the module docstring), as in
|
|
904
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_countermodel`: ``P(x) ⊢ P(x)`` and
|
|
905
|
+
``P(x) ⊢ ∃y P(y)`` are valid, ``P(x) ⊢ P(alpha)`` is not, and a premise is never
|
|
906
|
+
closed universally. The variables are read before the conclusion is negated.
|
|
907
|
+
|
|
908
|
+
Raises:
|
|
909
|
+
NotImplementedError: a free variable has the spelling of a constant of the formulas.
|
|
910
|
+
"""
|
|
911
|
+
closed, guards = _with_parameters(list(premises) + [conclusion])
|
|
912
|
+
return _search(closed[:-1] + [Not(closed[-1])] + guards, max_size, policy, domain_split,
|
|
913
|
+
max_candidates, symmetry_breaking) is None
|