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,1036 @@
|
|
|
1
|
+
"""Finite model finder — search for a finite model (or countermodel) of a theory.
|
|
2
|
+
|
|
3
|
+
The complement of the provers: where ``prove`` / ``is_valid`` answer *"does it
|
|
4
|
+
follow?"*, this answers *"is there a structure where it holds?"* by brute-force
|
|
5
|
+
enumeration of finite :class:`~unicode_logic_kit.semantics.tarski.Structure`\\ s over a
|
|
6
|
+
domain ``{0, 1, …, k-1}`` for increasing ``k``, checking each with the Tarskian
|
|
7
|
+
evaluator. It is the Mace4-style partner of the resolution prover: a valid entailment
|
|
8
|
+
has *no* countermodel, an invalid one usually has a small finite one.
|
|
9
|
+
|
|
10
|
+
- :func:`find_model` — a finite model satisfying every formula of a theory, or None.
|
|
11
|
+
- :func:`find_countermodel` — a finite structure satisfying ``premises`` but not
|
|
12
|
+
``conclusion`` (a witness that the entailment fails), or None.
|
|
13
|
+
- :func:`is_satisfiable_finite` / :func:`is_valid_finite` — the boolean wrappers.
|
|
14
|
+
|
|
15
|
+
A free variable is a PARAMETER of the problem: one unknown element, the same in every
|
|
16
|
+
formula handed to a call (the assignment-wise consequence relation, ``Γ ⊨ φ`` iff every
|
|
17
|
+
structure AND assignment that satisfies ``Γ`` satisfies ``φ``). It is replaced, in all the
|
|
18
|
+
formulas of the call together, by a constant of its own name
|
|
19
|
+
(:func:`~unicode_logic_kit.fol._free_parameters.parameterize`), so a structure that is found
|
|
20
|
+
interprets it like any constant: ``constants['x']`` is the element ``x`` stands for, and
|
|
21
|
+
``satisfies(formula, structure, {'x': structure.constants['x']})`` evaluates a formula
|
|
22
|
+
with the free variable ``x`` there. ``P(x) ⊢ P(alpha)`` has a countermodel (domain
|
|
23
|
+
``{0, 1}``, ``x`` ↦ 1, ``alpha`` ↦ 0, ``P`` = ``{1}``) and ``P(x) ⊢ ∃y P(y)`` has none;
|
|
24
|
+
``P(x), ¬P(y)`` has a model, in which ``x`` and ``y`` are two elements. A premise is
|
|
25
|
+
never closed universally. A free variable and a constant that have one spelling cannot
|
|
26
|
+
be told apart in a table keyed by name, so such a problem is refused
|
|
27
|
+
(``NotImplementedError``). A cardinality term ``|{v : φ}|`` is a natural number counted
|
|
28
|
+
by the evaluator, not an element of the domain: it is read only as an operand of a
|
|
29
|
+
comparison with a number (``= ≠ < > ≤ ≥`` against a numeral or another cardinality). As the
|
|
30
|
+
argument of a predicate or a function, or compared with an individual, it is refused
|
|
31
|
+
(``NotImplementedError``) rather than read as the element that shares its value, so
|
|
32
|
+
``∀y R(y) ⊢ R(|{x : P(x)}|)`` is refused and not answered with a countermodel. The search
|
|
33
|
+
is **bounded**: a domain size whose interpretation
|
|
34
|
+
space exceeds
|
|
35
|
+
``max_candidates`` is skipped, so ``None`` means "no model found within the bounds",
|
|
36
|
+
not "unsatisfiable" (first-order satisfiability is undecidable, and some satisfiable
|
|
37
|
+
sentences have only infinite models).
|
|
38
|
+
|
|
39
|
+
**Symmetry breaking (``symmetry_breaking=True``, the default).** The raw enumeration
|
|
40
|
+
below revisits every one of a domain's ``k!`` relabelings of the same underlying
|
|
41
|
+
structure — a named-constant-heavy signature spends almost its whole budget on
|
|
42
|
+
interpretations that differ only by *which* domain element happens to be called
|
|
43
|
+
``0`` versus ``1``. :func:`_canonical_interpretations` applies the classical
|
|
44
|
+
**least-number heuristic (LNH)** to CONSTANT assignments (a constant has no
|
|
45
|
+
argument tuple — it is simply "choose one of ``k`` interchangeable labels" — so a
|
|
46
|
+
global relabeling acts on it cleanly): they are filled by backtracking in
|
|
47
|
+
canonical (sorted) name order, each one taking either an already-used domain value
|
|
48
|
+
or the smallest still-unused one — never skipping ahead to a "fresh" element out of
|
|
49
|
+
order. That visits exactly one representative per isomorphism class of constant
|
|
50
|
+
assignments instead of ``k!`` of them. FUNCTION and PREDICATE tables are still
|
|
51
|
+
enumerated exhaustively per accepted constant skeleton — see
|
|
52
|
+
:func:`_canonical_interpretations`'s docstring both for why the search stays
|
|
53
|
+
complete regardless, and for why functions are NOT also LNH-reduced (a roadmap
|
|
54
|
+
draft called for that too, and it is unsound — a hand-checked counterexample is in
|
|
55
|
+
the same docstring). Pass ``symmetry_breaking=False`` to fall back to the plain
|
|
56
|
+
:func:`_interpretations` enumeration (kept unchanged, e.g. for differential
|
|
57
|
+
testing against the canonical generator). MSFOL (sorted) search is unaffected either
|
|
58
|
+
way — :func:`_sorted_interpretations` has no LNH pass (see its own scope note).
|
|
59
|
+
|
|
60
|
+
**Many-sorted (MSFOL)** input is handled directly, and what the search enumerates is
|
|
61
|
+
exactly the structures of one definition. There is ONE domain. Each named sort gets a
|
|
62
|
+
non-empty universe (a non-empty subset of the domain) enumerated alongside the rest of
|
|
63
|
+
the interpretation, and a ``SortedQuantifier`` ranges over its sort — so a found
|
|
64
|
+
:class:`Structure` carries the ``sorts`` mapping. Sorts may overlap (the relativisation
|
|
65
|
+
reading), and nothing makes two of them disjoint.
|
|
66
|
+
|
|
67
|
+
- A sorted constant ``c:S`` is an element of ``S``. A constant written with several
|
|
68
|
+
sorts (``c:A`` here, ``c:B`` there) lies in ALL of them: it is drawn from the
|
|
69
|
+
intersection of their universes, so the verdict does not depend on which annotation
|
|
70
|
+
is read first, and a choice of universes whose intersection is empty has no
|
|
71
|
+
structure. ``c:S`` in one place and plain ``c`` in another are ONE constant.
|
|
72
|
+
- An unsorted constant, an unsorted variable and the value of a function may be ANY
|
|
73
|
+
element of the domain (a function has no declared result sort); a predicate is a
|
|
74
|
+
relation over the whole domain.
|
|
75
|
+
- A sort and the unary predicate of the same name are ONE symbol, the guard reading
|
|
76
|
+
``∀x:S φ ≡ ∀x (S(x) → φ)``. The predicate ``S/1`` is therefore not enumerated on
|
|
77
|
+
its own: its extension IS the universe of ``S``, and the returned structure holds
|
|
78
|
+
that one extension in both tables (``sorts`` and ``predicates``). So
|
|
79
|
+
``⊢ ∃y:Car Car(y)`` has no countermodel, and ``Mortal(socrates:Human) ⊢
|
|
80
|
+
Human(socrates)`` has none either.
|
|
81
|
+
|
|
82
|
+
Every structure returned for sorted input passes
|
|
83
|
+
:func:`~unicode_logic_kit.semantics.tarski.check_structure`.
|
|
84
|
+
|
|
85
|
+
**Subsorting** (``subsorts``, optional, every public function below). A subsort
|
|
86
|
+
edge ``S -> {T}`` (child sort name -> its DIRECT parent sort names — the same
|
|
87
|
+
shape as :attr:`unicode_logic_kit.fol.signature.Signature.subsorts`, and typically
|
|
88
|
+
passed as exactly that attribute) means every accepted universe assignment must
|
|
89
|
+
satisfy ``set(sorts[S]) <= set(sorts[T])`` — the SAME subset-semantics reading
|
|
90
|
+
``fol.to_fol``'s emitted axioms and ``Signature.validate`` enforce elsewhere (see
|
|
91
|
+
that module's "Subsorting" docstring section), so all three routes agree on what
|
|
92
|
+
a subsort edge means. :func:`_sorted_interpretations` enforces this by filtering,
|
|
93
|
+
not by a new search algorithm — but, UNLIKE ``fol.to_fol``, it cannot rely on
|
|
94
|
+
DIRECT edges alone: this module's :class:`_Signature` only gives a sort its own
|
|
95
|
+
universe when that sort is actually USED by a sorted binder somewhere in the
|
|
96
|
+
theory, so a chain ``S < T < U`` where ``T`` is never otherwise mentioned would
|
|
97
|
+
silently lose the ``S ⊆ U`` consequence if only direct parents were checked
|
|
98
|
+
(``to_fol`` avoids this because it always emits a guard predicate for every
|
|
99
|
+
declared sort regardless of usage, letting direct-edge implications chain on
|
|
100
|
+
their own — see :func:`_subsort_closure`'s docstring for the full contrast).
|
|
101
|
+
:func:`_sorted_interpretations` instead computes the FULL TRANSITIVE closure of
|
|
102
|
+
``subsorts`` once per search (:func:`_subsort_closure`) and skips a
|
|
103
|
+
``sort_choice`` combination whose universes violate ANY closure pair present in
|
|
104
|
+
the theory's own signature, before its constants/functions/predicates are even
|
|
105
|
+
enumerated (see :func:`_respects_subsorts`). Only names occurring in this
|
|
106
|
+
theory's OWN signature are checked; an
|
|
107
|
+
edge naming an irrelevant sort is silently ignored, mirroring ``to_fol``'s own
|
|
108
|
+
"omitting it is byte-identical" convention. A name counts as occurring when the
|
|
109
|
+
theory uses it as a sort OR as a unary predicate: the sort and the predicate of one
|
|
110
|
+
name are one symbol, so an edge end that the theory uses only as a predicate is
|
|
111
|
+
bounded too (it is enumerated with the sorts, and may be empty — see
|
|
112
|
+
:func:`_edge_predicate_names`). An edge ``S < T`` is ``∀x (S(x) → T(x))`` whether or
|
|
113
|
+
not the theory has a sorted node, so an UNSORTED theory is held to every edge whose
|
|
114
|
+
two ends it uses as unary predicates (:func:`_unsorted_edge_pairs`), by the same
|
|
115
|
+
closure. The filter never affects the ``max_candidates`` pre-flight
|
|
116
|
+
check's honesty contract (see ``find_model``'s docstring) — a size is still
|
|
117
|
+
either fully searched or fully skipped, never partially.
|
|
118
|
+
|
|
119
|
+
**Deadline** (``timeout``, optional, on :func:`find_model`, :func:`find_countermodel`,
|
|
120
|
+
:func:`search_model` and :func:`search_countermodel`). ``timeout`` is a number of milliseconds,
|
|
121
|
+
counted from the moment the call starts; the default ``None`` is no limit, so a call without it
|
|
122
|
+
searches exactly as before. The clock is read before each candidate structure is built and
|
|
123
|
+
tested (and before each universe assignment of a many-sorted search is taken up), so a search
|
|
124
|
+
ends within the cost of one candidate of its deadline, however large the interpretation space
|
|
125
|
+
of the size it is in. A search cut off there gives the answer of one that found nothing,
|
|
126
|
+
``None``, and :func:`search_model` / :func:`search_countermodel` return a :class:`ModelSearch`
|
|
127
|
+
whose ``timed_out`` tells a deadline from a bound that was exhausted. A single evaluation of a
|
|
128
|
+
formula in a structure is not interrupted. :func:`is_satisfiable_finite` and
|
|
129
|
+
:func:`is_valid_finite` take no ``timeout``: a boolean has no value for "no answer", and a
|
|
130
|
+
search cut off would read as "no model" or as "valid".
|
|
131
|
+
|
|
132
|
+
Public API: :func:`find_model`, :func:`find_countermodel`, :func:`search_model`,
|
|
133
|
+
:func:`search_countermodel`, :class:`ModelSearch`,
|
|
134
|
+
:func:`is_satisfiable_finite`, :func:`is_valid_finite`, :func:`is_size_exhaustive`.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
from dataclasses import dataclass
|
|
138
|
+
from itertools import product
|
|
139
|
+
from typing import Dict, FrozenSet, Iterable, List, Mapping, Optional, Tuple
|
|
140
|
+
|
|
141
|
+
from .._deadline import instant as _instant, passed as _passed
|
|
142
|
+
from ..fol._free_parameters import parameterize
|
|
143
|
+
from ..fol.nodes import Node, Atom, Not, Quantifier, Variable, Constant, Number, Function
|
|
144
|
+
from ..fol.nodes import SortedConstant, SortedQuantifier
|
|
145
|
+
from ..fol.nodes import Count, Cardinality, SortedCount, SortedCardinality
|
|
146
|
+
from ..fol.nodes import SlashedExists, Measure
|
|
147
|
+
from .tarski import (
|
|
148
|
+
Structure, models, _MEASURE_FUNC, _numeral_constant_clash, _refuse_cardinality_as_individual,
|
|
149
|
+
)
|
|
150
|
+
from ..fol._fol_nodes import numeral_key
|
|
151
|
+
from ..fol._numeral_symbols import _is_counting_comparison
|
|
152
|
+
from ..fol._tptp_symbols import is_tptp_boolean_atom as _is_tptp_boolean_atom
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
#: Node types that bind a logical variable over a ``formula`` scope — the quantifiers,
|
|
156
|
+
#: the counting quantifiers and cardinality terms (and their sorted variants), and the
|
|
157
|
+
#: IF-logic slashed existential.
|
|
158
|
+
_VAR_BINDERS = (Quantifier, SortedQuantifier, Count, Cardinality,
|
|
159
|
+
SortedCount, SortedCardinality, SlashedExists)
|
|
160
|
+
|
|
161
|
+
#: The subset of the above that additionally names a sort the structures must interpret.
|
|
162
|
+
_SORTED_BINDERS = (SortedQuantifier, SortedCount, SortedCardinality)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
MAX_CANDIDATES = 1 << 20 # ~1M structures per domain size before a size is skipped
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Free-variable closure and signature collection
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def _free_var_names(node: Node, bound: frozenset = frozenset()) -> set:
|
|
173
|
+
"""Return the names of variables occurring free in ``node``."""
|
|
174
|
+
if isinstance(node, Variable):
|
|
175
|
+
return set() if node.name in bound else {node.name}
|
|
176
|
+
if isinstance(node, SlashedExists):
|
|
177
|
+
# The slash set names ENCLOSING binders, so those names are free here (and
|
|
178
|
+
# are not shadowed by this binder's own variable).
|
|
179
|
+
inner = _free_var_names(node.formula, bound | {node.variable.name})
|
|
180
|
+
return inner | {n for n in node.slashed if n not in bound}
|
|
181
|
+
if isinstance(node, _VAR_BINDERS):
|
|
182
|
+
return _free_var_names(node.formula, bound | {node.variable.name})
|
|
183
|
+
names: set = set()
|
|
184
|
+
for child in node._child_nodes():
|
|
185
|
+
names |= _free_var_names(child, bound)
|
|
186
|
+
return names
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _universal_closure(node: Node) -> Node:
|
|
190
|
+
"""Wrap ``node`` in ∀ for each free variable (deterministic order)."""
|
|
191
|
+
result = node
|
|
192
|
+
for name in sorted(_free_var_names(node), reverse=True):
|
|
193
|
+
result = Quantifier("∀", Variable(name), result)
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class _Signature:
|
|
198
|
+
"""The constants, functions, predicates, and sorts a theory's structures interpret."""
|
|
199
|
+
|
|
200
|
+
def __init__(self):
|
|
201
|
+
self.constants: set = set() # names
|
|
202
|
+
self.functions: set = set() # (name, arity)
|
|
203
|
+
self.predicates: set = set() # (name, arity)
|
|
204
|
+
self.sorts: set = set() # sort names (MSFOL)
|
|
205
|
+
# constant name -> EVERY sort it is annotated with (a constant written c:A in
|
|
206
|
+
# one place and c:B in another lies in both, so nothing here is overwritten)
|
|
207
|
+
self.sorted_constants: Dict[str, set] = {}
|
|
208
|
+
# The names ``constants`` holds because of a Constant, and the names it holds because of a
|
|
209
|
+
# numeral read as an individual (the key of its VALUE): a name in both would be one entry of
|
|
210
|
+
# a structure for two symbols, which is refused (see :meth:`_constant` / :meth:`_numeral`).
|
|
211
|
+
self._plain_constants: set = set()
|
|
212
|
+
self._numeral_keys: set = set()
|
|
213
|
+
|
|
214
|
+
def _constant(self, name: str) -> None:
|
|
215
|
+
"""Register the constant ``name``; refuse it when a numeral has that name."""
|
|
216
|
+
if name in self._numeral_keys:
|
|
217
|
+
raise _numeral_constant_clash(name, "semantics.modelfinder")
|
|
218
|
+
self._plain_constants.add(name)
|
|
219
|
+
self.constants.add(name)
|
|
220
|
+
|
|
221
|
+
def _numeral(self, value) -> None:
|
|
222
|
+
"""Register the numeral ``value`` as ONE constant per value (``1`` and ``1.0`` are the
|
|
223
|
+
same numeral); refuse it when a constant is named like it: a structure would hold one
|
|
224
|
+
entry for the two."""
|
|
225
|
+
key = numeral_key(value)
|
|
226
|
+
if key in self._plain_constants:
|
|
227
|
+
raise _numeral_constant_clash(key, "semantics.modelfinder")
|
|
228
|
+
self._numeral_keys.add(key)
|
|
229
|
+
self.constants.add(key)
|
|
230
|
+
|
|
231
|
+
def sort_predicates(self) -> set:
|
|
232
|
+
"""The unary predicates ``(S, 1)`` whose name ``S`` is also a sort of the theory.
|
|
233
|
+
|
|
234
|
+
A sort and the unary predicate of the same name are ONE symbol, so these are
|
|
235
|
+
not enumerated as predicates of their own: their extension IS the sort's
|
|
236
|
+
universe (see :func:`_sorted_interpretations`).
|
|
237
|
+
"""
|
|
238
|
+
return {(name, 1) for name in self.sorts} & self.predicates
|
|
239
|
+
|
|
240
|
+
def scan(self, node: Node) -> None:
|
|
241
|
+
if isinstance(node, SortedConstant):
|
|
242
|
+
self._constant(node.name)
|
|
243
|
+
self.sorts.add(node.sort)
|
|
244
|
+
self.sorted_constants.setdefault(node.name, set()).add(node.sort)
|
|
245
|
+
elif isinstance(node, Constant):
|
|
246
|
+
self._constant(node.name)
|
|
247
|
+
elif isinstance(node, Number):
|
|
248
|
+
# one constant per VALUE: ``1`` and ``1.0`` are the same numeral
|
|
249
|
+
self._numeral(node.value)
|
|
250
|
+
elif isinstance(node, Function):
|
|
251
|
+
self.functions.add((node.name, len(node.args)))
|
|
252
|
+
for a in node.args:
|
|
253
|
+
self.scan(a)
|
|
254
|
+
elif isinstance(node, Measure):
|
|
255
|
+
# μ(entity, dimension) denotes the binary function ``measure`` — the same
|
|
256
|
+
# symbol Measure.to_z3 / to_prover9 emit, so a structure found here
|
|
257
|
+
# interprets what the provers see. The dimension is an ordinary term.
|
|
258
|
+
self.functions.add(_MEASURE_FUNC)
|
|
259
|
+
self.scan(node.entity)
|
|
260
|
+
self.scan(node.dimension)
|
|
261
|
+
elif isinstance(node, Atom):
|
|
262
|
+
# identity is built in, and so are TPTP's defined propositions
|
|
263
|
+
# `$true` / `$false`: a structure does not get to choose their value
|
|
264
|
+
if node.predicate not in ("=", "≠") and not _is_tptp_boolean_atom(node):
|
|
265
|
+
self.predicates.add((node.predicate, len(node.args)))
|
|
266
|
+
counting = _is_counting_comparison(node)
|
|
267
|
+
for a in node.args:
|
|
268
|
+
if counting and isinstance(a, Number):
|
|
269
|
+
# the number a cardinality is compared with is no individual: it is read as
|
|
270
|
+
# itself, never as the entry of a structure, so it clashes with no constant
|
|
271
|
+
self.constants.add(numeral_key(a.value))
|
|
272
|
+
else:
|
|
273
|
+
self.scan(a)
|
|
274
|
+
elif isinstance(node, _VAR_BINDERS):
|
|
275
|
+
# Scan only the scope: a binder's ``variable`` is not a signature symbol,
|
|
276
|
+
# and a Count's ``n`` is a cardinality bound, not an individual — walking
|
|
277
|
+
# the children generically would register it as a domain constant.
|
|
278
|
+
if isinstance(node, _SORTED_BINDERS):
|
|
279
|
+
self.sorts.add(node.sort)
|
|
280
|
+
self.scan(node.formula)
|
|
281
|
+
else:
|
|
282
|
+
for child in node._child_nodes():
|
|
283
|
+
self.scan(child)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ---------------------------------------------------------------------------
|
|
287
|
+
# Enumeration of finite interpretations
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
def _candidate_count(sig: "_Signature", k: int) -> int:
|
|
291
|
+
"""The number of distinct interpretations of ``sig`` over a ``k``-element domain.
|
|
292
|
+
|
|
293
|
+
For a many-sorted signature this is an upper bound on what
|
|
294
|
+
:func:`_sorted_interpretations` yields (it does not subtract the sorted
|
|
295
|
+
constants' restriction to their sorts, nor the subsort filter), and exact in
|
|
296
|
+
the factors it does count: a unary predicate that is also a sort is NOT a
|
|
297
|
+
factor of its own, because its extension is the sort's universe.
|
|
298
|
+
"""
|
|
299
|
+
total = k ** len(sig.constants)
|
|
300
|
+
for _, arity in sig.functions:
|
|
301
|
+
total *= k ** (k ** arity)
|
|
302
|
+
merged = sig.sort_predicates() if sig.sorts else ()
|
|
303
|
+
for predicate in sig.predicates:
|
|
304
|
+
if predicate not in merged:
|
|
305
|
+
total *= 1 << (k ** predicate[1])
|
|
306
|
+
# Each sort ranges over the non-empty subsets of the domain (its universe).
|
|
307
|
+
for _ in sig.sorts:
|
|
308
|
+
total *= (1 << k) - 1
|
|
309
|
+
return total
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _lnh_constant_count(n_slots: int, k: int) -> int:
|
|
313
|
+
"""The EXACT number of LNH-canonical constant assignments for ``n_slots``
|
|
314
|
+
constants over a ``k``-element domain — the count :func:`_canonical_interpretations`
|
|
315
|
+
actually yields for its constant part, computed analytically (``O(n_slots * k)``
|
|
316
|
+
arithmetic, no enumeration) so :func:`_canonical_candidate_count` never needs to
|
|
317
|
+
fall back to a live count just to decide whether a size fits ``max_candidates``.
|
|
318
|
+
|
|
319
|
+
Equivalently, this counts the "restricted growth strings" :func:`_lnh_choices`
|
|
320
|
+
generates: a value may repeat any of the ``j`` already-used domain indices, or
|
|
321
|
+
introduce exactly the next unused one (if ``j < k``). ``dp[j]`` tracks, working
|
|
322
|
+
backward from the last slot, "ways to fill the remaining slots given ``j``
|
|
323
|
+
distinct values already committed" — the same recursion the backtracking
|
|
324
|
+
generator walks, just summed instead of enumerated. (This is the number of set
|
|
325
|
+
partitions of an ``n_slots``-element set into at most ``k`` blocks, i.e.
|
|
326
|
+
``sum_{j=1}^{min(n_slots, k)} S(n_slots, j)`` in Stirling-number-of-the-second-kind
|
|
327
|
+
terms — hand-checked for n_slots=2,k=2 -> 2 and n_slots=3,k=2 -> 4 in
|
|
328
|
+
``tests/test_modelfinder_symmetry.py``.)
|
|
329
|
+
"""
|
|
330
|
+
if n_slots == 0:
|
|
331
|
+
return 1
|
|
332
|
+
dp = [1] * (k + 1) # boundary: 0 slots left to fill is 1 way, for every j
|
|
333
|
+
for _ in range(n_slots):
|
|
334
|
+
new_dp = [0] * (k + 1)
|
|
335
|
+
for j in range(k + 1):
|
|
336
|
+
total = j * dp[j] # repeat one of the j already-used values
|
|
337
|
+
if j < k:
|
|
338
|
+
total += dp[j + 1] # or introduce the next still-unused one
|
|
339
|
+
new_dp[j] = total
|
|
340
|
+
dp = new_dp
|
|
341
|
+
return dp[0]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _canonical_candidate_count(sig: "_Signature", k: int) -> int:
|
|
345
|
+
"""The EXACT number of interpretations :func:`_canonical_interpretations` yields
|
|
346
|
+
for ``sig`` over a ``k``-element domain — the analytic pre-flight count backing
|
|
347
|
+
``find_model``'s "skip this size" check when ``symmetry_breaking=True``, playing
|
|
348
|
+
the same role :func:`_candidate_count` plays for the unbroken enumeration.
|
|
349
|
+
|
|
350
|
+
Only the constants factor changes, from ``k ** len(sig.constants)`` to
|
|
351
|
+
:func:`_lnh_constant_count`'s LNH-reduced count; functions, predicates and sorts
|
|
352
|
+
keep the exact same raw factors :func:`_candidate_count` uses, because
|
|
353
|
+
:func:`_canonical_interpretations` leaves those fully exhaustive (see its
|
|
354
|
+
docstring). Being exact (not an estimate) means the pre-flight check alone
|
|
355
|
+
decides whether a size is searched — no live count-and-break loop is needed
|
|
356
|
+
inside the search itself, which is what makes the check safe to run
|
|
357
|
+
unconditionally: a signature with few or no constants (where LNH gives little
|
|
358
|
+
or no reduction) is skipped exactly as cheaply, and exactly as correctly, as it
|
|
359
|
+
was under :func:`_candidate_count` before symmetry breaking existed.
|
|
360
|
+
"""
|
|
361
|
+
total = _lnh_constant_count(len(sig.constants), k)
|
|
362
|
+
for _, arity in sig.functions:
|
|
363
|
+
total *= k ** (k ** arity)
|
|
364
|
+
for _, arity in sig.predicates:
|
|
365
|
+
total *= 1 << (k ** arity)
|
|
366
|
+
# sig.sorts is always empty here (_canonical_interpretations raises otherwise),
|
|
367
|
+
# kept parallel to _candidate_count's shape for clarity and future-proofing.
|
|
368
|
+
for _ in sig.sorts:
|
|
369
|
+
total *= (1 << k) - 1
|
|
370
|
+
return total
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _nonempty_subsets(domain: tuple):
|
|
374
|
+
"""Yield every non-empty subset of ``domain`` as a tuple (a sort universe)."""
|
|
375
|
+
items = list(domain)
|
|
376
|
+
for mask in range(1, 1 << len(items)):
|
|
377
|
+
yield tuple(items[i] for i in range(len(items)) if (mask >> i) & 1)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _subsets(domain: tuple):
|
|
381
|
+
"""Yield every subset of ``domain`` as a tuple, the empty one first."""
|
|
382
|
+
yield ()
|
|
383
|
+
yield from _nonempty_subsets(domain)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _func_pred_interpretations(sig: "_Signature", domain: tuple):
|
|
387
|
+
"""Yield ``(functions, predicates)`` for every interpretation of the func/pred part."""
|
|
388
|
+
func_sig = sorted(sig.functions)
|
|
389
|
+
pred_sig = sorted(sig.predicates)
|
|
390
|
+
func_options = []
|
|
391
|
+
for _, arity in func_sig:
|
|
392
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
393
|
+
func_options.append(list(product(domain, repeat=len(arg_tuples))))
|
|
394
|
+
pred_options = []
|
|
395
|
+
for _, arity in pred_sig:
|
|
396
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
397
|
+
pred_options.append(list(product((False, True), repeat=len(arg_tuples))))
|
|
398
|
+
for func_choice in (product(*func_options) if func_options else [()]):
|
|
399
|
+
functions = {}
|
|
400
|
+
for (name, arity), values in zip(func_sig, func_choice):
|
|
401
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
402
|
+
functions[(name, arity)] = dict(zip(arg_tuples, values))
|
|
403
|
+
for pred_choice in (product(*pred_options) if pred_options else [()]):
|
|
404
|
+
predicates = {}
|
|
405
|
+
for (name, arity), mask in zip(pred_sig, pred_choice):
|
|
406
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
407
|
+
predicates[(name, arity)] = {t for t, inc in zip(arg_tuples, mask) if inc}
|
|
408
|
+
yield functions, predicates
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _subsort_closure(subsorts: Mapping[str, FrozenSet[str]]) -> Dict[str, FrozenSet[str]]:
|
|
412
|
+
"""The reflexive-free TRANSITIVE closure of a DIRECT subsort-edges
|
|
413
|
+
mapping — child sort name -> every ancestor reachable by chaining one or
|
|
414
|
+
more edges, regardless of whether that ancestor (or any sort along the
|
|
415
|
+
chain) is otherwise mentioned anywhere in the theory being searched.
|
|
416
|
+
|
|
417
|
+
This is NOT the same "direct edges are enough" story ``fol.to_fol``'s
|
|
418
|
+
axiom emission tells: ``to_fol`` can rely on direct edges alone because
|
|
419
|
+
it ALWAYS emits one implication axiom per declared edge into the SAME
|
|
420
|
+
classical signature, so an intermediate sort's guard predicate ``B`` is
|
|
421
|
+
still present (as an ordinary, if otherwise-unconstrained, predicate
|
|
422
|
+
symbol) for the chain ``A(x) → B(x)`` and ``B(x) → C(x)`` to compose
|
|
423
|
+
into ``A(x) → C(x)``. This module's :class:`_Signature` (see
|
|
424
|
+
:meth:`_Signature.scan`), by contrast, only gives a sort ITS OWN universe
|
|
425
|
+
in a searched :class:`~unicode_logic_kit.semantics.tarski.Structure` when
|
|
426
|
+
that sort is actually USED (by a sorted binder) somewhere in the theory
|
|
427
|
+
— a theory built from ``∃x:A ∀y:C (x ≠ y)`` alone never mentions ``B`` at
|
|
428
|
+
all, so a check limited to DIRECT edges would silently miss the
|
|
429
|
+
``A ⊆ C`` consequence whenever the chain passes through such an unused
|
|
430
|
+
intermediate sort (review-confirmed: this is exactly the gap that made
|
|
431
|
+
an earlier, direct-edges-only version of this filter disagree with the
|
|
432
|
+
``to_fol`` + Z3 route on a hand-built two-level-chain case). Computing
|
|
433
|
+
the full closure and checking every ancestor PRESENT in the theory's own
|
|
434
|
+
signature — not just DIRECT parents — closes that gap while still
|
|
435
|
+
reading ``subsorts`` in the same DIRECT-edges shape
|
|
436
|
+
:attr:`~unicode_logic_kit.fol.signature.Signature.subsorts` stores it in.
|
|
437
|
+
|
|
438
|
+
Assumed acyclic (never independently re-checked here — a caller passing
|
|
439
|
+
a real :class:`~unicode_logic_kit.fol.signature.Signature`'s own
|
|
440
|
+
``.subsorts`` already had it refused at construction time on a cycle);
|
|
441
|
+
a cycle in a hand-built raw mapping simply stops contributing further
|
|
442
|
+
ancestors once revisited, rather than raising or looping forever.
|
|
443
|
+
"""
|
|
444
|
+
memo: Dict[str, FrozenSet[str]] = {}
|
|
445
|
+
|
|
446
|
+
def closure_of(node: str, visiting: FrozenSet[str]) -> FrozenSet[str]:
|
|
447
|
+
if node in memo:
|
|
448
|
+
return memo[node]
|
|
449
|
+
if node in visiting:
|
|
450
|
+
return frozenset()
|
|
451
|
+
ancestors: set = set()
|
|
452
|
+
for parent in subsorts.get(node, frozenset()):
|
|
453
|
+
ancestors.add(parent)
|
|
454
|
+
ancestors |= closure_of(parent, visiting | {node})
|
|
455
|
+
memo[node] = frozenset(ancestors)
|
|
456
|
+
return memo[node]
|
|
457
|
+
|
|
458
|
+
for node in subsorts:
|
|
459
|
+
closure_of(node, frozenset())
|
|
460
|
+
return memo
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _respects_subsorts(sorts: dict, closure: Mapping[str, FrozenSet[str]]) -> bool:
|
|
464
|
+
"""True iff every ancestor edge in the subsort CLOSURE (see
|
|
465
|
+
:func:`_subsort_closure`) is honoured by ``sorts`` (sort name -> its
|
|
466
|
+
universe, as one ``sort_choice`` assigns it):
|
|
467
|
+
``set(sorts[child]) <= set(sorts[ancestor])`` for every
|
|
468
|
+
``(child, ancestor)`` pair the closure names.
|
|
469
|
+
|
|
470
|
+
A pair naming a sort absent from ``sorts`` (this theory never mentions
|
|
471
|
+
it) is silently skipped — there is no universe to compare, and a theory
|
|
472
|
+
that never uses that sort must decide identically whether or not the
|
|
473
|
+
caller's full :class:`~unicode_logic_kit.fol.signature.Signature` happens
|
|
474
|
+
to declare an edge for it (mirrors ``fol.to_fol``'s own "irrelevant
|
|
475
|
+
edges change nothing" convention).
|
|
476
|
+
"""
|
|
477
|
+
for child, ancestors in closure.items():
|
|
478
|
+
if child not in sorts:
|
|
479
|
+
continue
|
|
480
|
+
child_universe = set(sorts[child])
|
|
481
|
+
for ancestor in ancestors:
|
|
482
|
+
if ancestor not in sorts:
|
|
483
|
+
continue
|
|
484
|
+
if not child_universe <= set(sorts[ancestor]):
|
|
485
|
+
return False
|
|
486
|
+
return True
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _edge_predicate_names(sig: "_Signature",
|
|
490
|
+
closure: Mapping[str, FrozenSet[str]]) -> List[str]:
|
|
491
|
+
"""The unary predicates of the theory, not sorts of it, that a subsort edge constrains.
|
|
492
|
+
|
|
493
|
+
A subsort edge ``S < T`` is ``S ⊆ T`` where ``S`` and ``T`` are each the sort
|
|
494
|
+
AND the unary predicate of that name. When the theory uses ``T`` only as a
|
|
495
|
+
predicate (``∀x:S φ`` and ``T(c)``, say), the edge still bounds ``T``'s
|
|
496
|
+
extension from below, so ``T`` is enumerated together with the sorts (it may
|
|
497
|
+
be empty, unlike a sort). An edge is checked only between names the theory
|
|
498
|
+
uses, as a sort or as a unary predicate — see :func:`_respects_subsorts`.
|
|
499
|
+
"""
|
|
500
|
+
present = set(sig.sorts) | {name for name, arity in sig.predicates if arity == 1}
|
|
501
|
+
tied = set()
|
|
502
|
+
for child, ancestors in closure.items():
|
|
503
|
+
if child in present:
|
|
504
|
+
for ancestor in ancestors:
|
|
505
|
+
if ancestor in present:
|
|
506
|
+
tied.add(child)
|
|
507
|
+
tied.add(ancestor)
|
|
508
|
+
return sorted(tied - sig.sorts)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _unsorted_edge_pairs(sig: "_Signature",
|
|
512
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]]) -> List[Tuple[str, str]]:
|
|
513
|
+
"""The subsort edges an UNSORTED theory is held to, as ``(child, ancestor)`` pairs.
|
|
514
|
+
|
|
515
|
+
An edge ``S < T`` is ``∀x (S(x) → T(x))`` whether or not the theory has a sorted
|
|
516
|
+
node. Where it has none, a name is an ordinary unary predicate, and an edge
|
|
517
|
+
constrains the theory when BOTH of its ends are unary predicates the theory
|
|
518
|
+
uses; an edge with an end the theory never mentions is not checked (it can
|
|
519
|
+
always be satisfied by choosing that end), exactly as for a sorted theory. The
|
|
520
|
+
pairs are taken from the transitive closure of ``subsorts``, so a chain
|
|
521
|
+
``S < T < U`` whose middle is unused still bounds ``S`` by ``U``. Empty when no
|
|
522
|
+
``subsorts`` are given (and for a sorted theory, which
|
|
523
|
+
:func:`_sorted_interpretations` handles): the search is then unchanged.
|
|
524
|
+
"""
|
|
525
|
+
if not subsorts or sig.sorts:
|
|
526
|
+
return []
|
|
527
|
+
closure = _subsort_closure(subsorts)
|
|
528
|
+
unary = {name for name, arity in sig.predicates if arity == 1}
|
|
529
|
+
return sorted((child, ancestor)
|
|
530
|
+
for child, ancestors in closure.items() if child in unary
|
|
531
|
+
for ancestor in ancestors if ancestor in unary)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _predicate_edges_hold(predicates: Mapping, edges: List[Tuple[str, str]]) -> bool:
|
|
535
|
+
"""True iff every ``(child, ancestor)`` edge holds between the unary predicate tables."""
|
|
536
|
+
return all(predicates[(child, 1)] <= predicates[(ancestor, 1)] for child, ancestor in edges)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _sorted_interpretations(sig: "_Signature", domain: tuple,
|
|
540
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
|
|
541
|
+
deadline: Optional[float] = None):
|
|
542
|
+
"""Yield ``(constants, functions, predicates, sorts)`` for an MSFOL signature.
|
|
543
|
+
|
|
544
|
+
The generator ends early, without yielding more, once ``deadline`` (a
|
|
545
|
+
``perf_counter`` instant; ``None`` for no limit) has passed: it is read before each
|
|
546
|
+
universe assignment is taken up, so a long run of assignments that a subsort edge
|
|
547
|
+
rejects cannot outlast it.
|
|
548
|
+
|
|
549
|
+
The structures yielded are exactly the structures of the definition for ``sig``
|
|
550
|
+
(see the module docstring): one domain; each sort a non-empty subset of it,
|
|
551
|
+
sorts overlapping freely; a sorted constant ``c:S`` an element of ``S`` — of
|
|
552
|
+
EVERY sort it is annotated with, so a constant written with two sorts is drawn
|
|
553
|
+
from their intersection (and a sort choice whose intersection is empty yields
|
|
554
|
+
nothing); an unsorted constant, and every function value, any element; the
|
|
555
|
+
rest is the classical enumeration.
|
|
556
|
+
|
|
557
|
+
A unary predicate whose name is also a sort is the sort: it is not enumerated,
|
|
558
|
+
its extension is the sort's universe, and the yielded ``predicates`` carry it
|
|
559
|
+
so that the structure is consistent in both tables.
|
|
560
|
+
|
|
561
|
+
Sorts may overlap — UNLESS ``subsorts`` (or its transitive closure — see
|
|
562
|
+
:func:`_subsort_closure`) declares an edge between them, in which case every
|
|
563
|
+
yielded ``sort_choice`` must additionally satisfy that edge's subset
|
|
564
|
+
constraint (see :func:`_respects_subsorts` and the module docstring's
|
|
565
|
+
"Subsorting" section); a combination that fails is skipped before its
|
|
566
|
+
constants/functions/predicates are enumerated at all. An edge end that the
|
|
567
|
+
theory uses only as a unary predicate takes part as well (see
|
|
568
|
+
:func:`_edge_predicate_names`).
|
|
569
|
+
"""
|
|
570
|
+
sort_names = sorted(sig.sorts)
|
|
571
|
+
const_names = sorted(sig.constants)
|
|
572
|
+
closure = _subsort_closure(subsorts) if subsorts else None
|
|
573
|
+
edge_names = _edge_predicate_names(sig, closure) if closure else []
|
|
574
|
+
set_names = sort_names + edge_names
|
|
575
|
+
set_options = ([list(_nonempty_subsets(domain)) for _ in sort_names]
|
|
576
|
+
+ [list(_subsets(domain)) for _ in edge_names])
|
|
577
|
+
# The unary predicates that ARE a sort (or an edge-constrained predicate) are
|
|
578
|
+
# the set itself, so only the others are enumerated.
|
|
579
|
+
shared = [(name, 1) for name in set_names if (name, 1) in sig.predicates]
|
|
580
|
+
free = _Signature()
|
|
581
|
+
free.functions = sig.functions
|
|
582
|
+
free.predicates = sig.predicates - set(shared)
|
|
583
|
+
for set_choice in (product(*set_options) if set_options else [()]):
|
|
584
|
+
if _passed(deadline):
|
|
585
|
+
return
|
|
586
|
+
extensions = dict(zip(set_names, set_choice))
|
|
587
|
+
if closure and not _respects_subsorts(extensions, closure):
|
|
588
|
+
continue
|
|
589
|
+
sorts = {name: extensions[name] for name in sort_names}
|
|
590
|
+
const_option_lists = []
|
|
591
|
+
for name in const_names:
|
|
592
|
+
annotated = sig.sorted_constants.get(name)
|
|
593
|
+
if annotated:
|
|
594
|
+
const_option_lists.append(
|
|
595
|
+
[d for d in domain if all(d in sorts[s] for s in annotated)])
|
|
596
|
+
else:
|
|
597
|
+
const_option_lists.append(list(domain))
|
|
598
|
+
fixed_predicates = {key: {(d,) for d in extensions[key[0]]} for key in shared}
|
|
599
|
+
for const_choice in (product(*const_option_lists) if const_option_lists else [()]):
|
|
600
|
+
constants = dict(zip(const_names, const_choice))
|
|
601
|
+
for functions, predicates in _func_pred_interpretations(free, domain):
|
|
602
|
+
if fixed_predicates:
|
|
603
|
+
predicates.update(fixed_predicates)
|
|
604
|
+
yield constants, functions, predicates, sorts
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _interpretations(sig: "_Signature", domain: tuple):
|
|
608
|
+
"""Yield ``(constants, functions, predicates)`` dicts for every interpretation."""
|
|
609
|
+
k = len(domain)
|
|
610
|
+
const_names = sorted(sig.constants)
|
|
611
|
+
func_sig = sorted(sig.functions)
|
|
612
|
+
pred_sig = sorted(sig.predicates)
|
|
613
|
+
|
|
614
|
+
# Per-symbol option lists.
|
|
615
|
+
const_options = list(product(domain, repeat=len(const_names)))
|
|
616
|
+
func_options = []
|
|
617
|
+
for _, arity in func_sig:
|
|
618
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
619
|
+
func_options.append(list(product(domain, repeat=len(arg_tuples))))
|
|
620
|
+
pred_options = []
|
|
621
|
+
for _, arity in pred_sig:
|
|
622
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
623
|
+
pred_options.append(list(product((False, True), repeat=len(arg_tuples))))
|
|
624
|
+
|
|
625
|
+
for const_choice in const_options:
|
|
626
|
+
constants = dict(zip(const_names, const_choice))
|
|
627
|
+
for func_choice in (product(*func_options) if func_options else [()]):
|
|
628
|
+
functions = {}
|
|
629
|
+
for (name, arity), values in zip(func_sig, func_choice):
|
|
630
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
631
|
+
functions[(name, arity)] = dict(zip(arg_tuples, values))
|
|
632
|
+
for pred_choice in (product(*pred_options) if pred_options else [()]):
|
|
633
|
+
predicates = {}
|
|
634
|
+
for (name, arity), mask in zip(pred_sig, pred_choice):
|
|
635
|
+
arg_tuples = list(product(domain, repeat=arity))
|
|
636
|
+
predicates[(name, arity)] = {
|
|
637
|
+
t for t, inc in zip(arg_tuples, mask) if inc
|
|
638
|
+
}
|
|
639
|
+
yield constants, functions, predicates
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
# ---------------------------------------------------------------------------
|
|
643
|
+
# LNH symmetry breaking (roadmap C23)
|
|
644
|
+
# ---------------------------------------------------------------------------
|
|
645
|
+
|
|
646
|
+
def _lnh_choices(next_new: int, k: int):
|
|
647
|
+
"""The domain-VALUE choices for one least-number-heuristic (LNH) table cell.
|
|
648
|
+
|
|
649
|
+
``next_new`` is how many distinct domain values earlier cells (in canonical
|
|
650
|
+
symbol order) have already used. A cell may repeat any of them (indices
|
|
651
|
+
``0 .. next_new - 1``) or introduce exactly the next still-unused one (index
|
|
652
|
+
``next_new``, if the ``k``-element domain has one left) — never a value further
|
|
653
|
+
out, which would let two candidates differ only in *which* arbitrary index a
|
|
654
|
+
fresh domain element happens to receive. Every ``k!`` relabeling of an
|
|
655
|
+
argument-free assignment (a constant, or free_logic's partial constant) collapses
|
|
656
|
+
to the single one built by always picking the least eligible index at each cell
|
|
657
|
+
(a restricted-growth-string argument), which is exactly what makes
|
|
658
|
+
:func:`_canonical_interpretations` visit one candidate per isomorphism class of
|
|
659
|
+
constant assignments instead of ``k!`` (this cap is safe ONLY for argument-free
|
|
660
|
+
cells — see :func:`_canonical_interpretations`'s docstring for why a function's
|
|
661
|
+
table cells, indexed by an argument tuple, are a different, unsound case).
|
|
662
|
+
|
|
663
|
+
Shared, single-source bookkeeping for both
|
|
664
|
+
:func:`_canonical_interpretations` here and
|
|
665
|
+
:func:`~unicode_logic_kit.semantics.free_logic._canonical_partial_interpretations`
|
|
666
|
+
(which additionally offers a non-denoting choice alongside this range — see its
|
|
667
|
+
own docstring for why that choice is exempt from the relabeling argument).
|
|
668
|
+
"""
|
|
669
|
+
return range(min(next_new + 1, k))
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _canonical_interpretations(sig: "_Signature", domain: tuple):
|
|
673
|
+
"""Yield ``(constants, functions, predicates)`` with LNH symmetry breaking.
|
|
674
|
+
|
|
675
|
+
A drop-in structural match for :func:`_interpretations` (same yielded shape,
|
|
676
|
+
same scanned signature): CONSTANT assignments are filled by backtracking in
|
|
677
|
+
canonical (sorted) name order, each cell's choice capped by
|
|
678
|
+
:func:`_lnh_choices` — any already-used domain value, or exactly the next
|
|
679
|
+
still-unused one. A constant has no argument tuple, so it is exactly "choose
|
|
680
|
+
one of ``k`` interchangeable labels", and LNH's restricted-growth-string
|
|
681
|
+
argument applies to it cleanly: every ``k!`` relabeling of a constant
|
|
682
|
+
assignment collapses to the single one built by always picking the least
|
|
683
|
+
eligible index, so this generator visits one candidate per isomorphism class
|
|
684
|
+
of constant assignments instead of (up to) ``k!`` of them.
|
|
685
|
+
|
|
686
|
+
FUNCTION and PREDICATE tables are still enumerated exhaustively per accepted
|
|
687
|
+
constant skeleton, via :func:`_func_pred_interpretations` (the same helper
|
|
688
|
+
:func:`_sorted_interpretations` already uses) — **deliberately**, not as an
|
|
689
|
+
afterthought. A roadmap draft for this generator (C23) described applying the
|
|
690
|
+
identical per-cell LNH cap to FUNCTION table cells too, in canonical
|
|
691
|
+
``(name, arity, arg_tuple)`` order; that is UNSOUND and was caught by this
|
|
692
|
+
module's own differential test (``tests/test_modelfinder_symmetry.py``), so it
|
|
693
|
+
is deliberately not implemented — see the deviation note below. Predicates
|
|
694
|
+
were always intended to stay exhaustive (codomain ``{0, 1}``, not the domain),
|
|
695
|
+
so leaving BOTH functions and predicates exhaustive keeps this generator
|
|
696
|
+
trivially sound and COMPLETE by the same argument the roadmap already gives for
|
|
697
|
+
predicates: for any raw interpretation ``I``, some domain permutation ``π``
|
|
698
|
+
makes ``π(I)``'s constant part LNH-canonical, and because the function and
|
|
699
|
+
predicate parts here range over their FULL, unrestricted space regardless of
|
|
700
|
+
what the constants happen to be, ``π(I)``'s function/predicate part is
|
|
701
|
+
generated too — so this generator yields a structure isomorphic to ``I`` for
|
|
702
|
+
every ``I`` :func:`_interpretations` would yield.
|
|
703
|
+
|
|
704
|
+
**Deviation from the roadmap draft — why functions are NOT LNH-reduced here.**
|
|
705
|
+
A function's table cell is indexed by an ARGUMENT TUPLE, which is itself made
|
|
706
|
+
of domain elements — so a domain permutation ``π`` does not just relabel a
|
|
707
|
+
cell's *value* (as for a constant), it also moves WHICH cell holds an entry
|
|
708
|
+
(the cell at argument tuple ``t`` moves to argument tuple ``π(t)``). A flat,
|
|
709
|
+
per-cell LNH cap in a FIXED argument order (as the draft described) ignores
|
|
710
|
+
that second effect and is provably incomplete: on a 2-element domain with a
|
|
711
|
+
single unary function ``f`` and no constants, the "swap" table ``f(0)=1,
|
|
712
|
+
f(1)=0`` is its own isomorphism class (both domain permutations fix it — try
|
|
713
|
+
them), yet the draft's scheme forces cell ``f(0)`` to be ``0`` on the very
|
|
714
|
+
first choice, so that legitimate, non-isomorphic-to-anything-else table is
|
|
715
|
+
never produced by *any* permutation of it. A sound LNH pass for functions
|
|
716
|
+
would need to process argument tuples in a REACHABILITY order tied to element
|
|
717
|
+
discovery (walk the term structure generated from the constants through the
|
|
718
|
+
functions, à la classical Mace-/Paradox-style model builders), not a fixed
|
|
719
|
+
domain-lexicographic argument order — a materially larger undertaking left to
|
|
720
|
+
future work, alongside the predicate-aware automorphism group the roadmap
|
|
721
|
+
itself already scoped out. Leaving functions exhaustive is the SOUND choice;
|
|
722
|
+
reducing them incorrectly would silently turn "no model found" into a false
|
|
723
|
+
negative, which this module refuses to risk.
|
|
724
|
+
|
|
725
|
+
``sig.sorts`` must be empty — MSFOL search has no LNH pass in v1 (a sort's
|
|
726
|
+
universe is an arbitrary domain SUBSET, so its automorphism group depends on
|
|
727
|
+
the sort partition — a materially harder variant, left to
|
|
728
|
+
:func:`_sorted_interpretations`, unchanged).
|
|
729
|
+
"""
|
|
730
|
+
if sig.sorts:
|
|
731
|
+
raise ValueError(
|
|
732
|
+
"_canonical_interpretations: sig.sorts is non-empty; MSFOL (sorted) "
|
|
733
|
+
"search has no LNH pass — use _sorted_interpretations instead."
|
|
734
|
+
)
|
|
735
|
+
k = len(domain)
|
|
736
|
+
const_names = sorted(sig.constants)
|
|
737
|
+
n_slots = len(const_names)
|
|
738
|
+
|
|
739
|
+
def backtrack(i: int, next_new: int, values: list):
|
|
740
|
+
if i == n_slots:
|
|
741
|
+
constants = dict(zip(const_names, (domain[v] for v in values)))
|
|
742
|
+
for functions, predicates in _func_pred_interpretations(sig, domain):
|
|
743
|
+
yield constants, functions, predicates
|
|
744
|
+
return
|
|
745
|
+
for v in _lnh_choices(next_new, k):
|
|
746
|
+
values.append(v)
|
|
747
|
+
new_next = next_new + 1 if v == next_new else next_new
|
|
748
|
+
yield from backtrack(i + 1, new_next, values)
|
|
749
|
+
values.pop()
|
|
750
|
+
|
|
751
|
+
yield from backtrack(0, 0, [])
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
# ---------------------------------------------------------------------------
|
|
755
|
+
# Public API
|
|
756
|
+
# ---------------------------------------------------------------------------
|
|
757
|
+
|
|
758
|
+
@dataclass(frozen=True)
|
|
759
|
+
class ModelSearch:
|
|
760
|
+
"""The outcome of :func:`search_model` and :func:`search_countermodel`.
|
|
761
|
+
|
|
762
|
+
Fields:
|
|
763
|
+
|
|
764
|
+
- ``structure``: the :class:`~unicode_logic_kit.semantics.tarski.Structure` found, or
|
|
765
|
+
``None`` when none was found.
|
|
766
|
+
- ``timed_out``: ``True`` iff the search was cut off by its ``timeout`` before it had
|
|
767
|
+
covered what it was asked to cover (so ``structure`` is ``None``); ``False`` for a
|
|
768
|
+
search that found a structure, or that ended at its size and candidate bounds.
|
|
769
|
+
"""
|
|
770
|
+
|
|
771
|
+
structure: Optional[Structure]
|
|
772
|
+
timed_out: bool = False
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def find_model(formulas, max_size: int = 4,
|
|
776
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
777
|
+
symmetry_breaking: bool = True,
|
|
778
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
|
|
779
|
+
timeout: Optional[float] = None) -> Optional[Structure]:
|
|
780
|
+
"""Return a finite :class:`Structure` satisfying every formula, or None.
|
|
781
|
+
|
|
782
|
+
Searches domains of size ``1 .. max_size`` in turn, enumerating every
|
|
783
|
+
interpretation of the theory's signature and returning the first structure in
|
|
784
|
+
which all formulas hold.
|
|
785
|
+
|
|
786
|
+
``subsorts`` (default ``None``, no constraint) additionally requires every
|
|
787
|
+
searched universe assignment to honour the given DIRECT subsort edges — see
|
|
788
|
+
the module docstring's "Subsorting" section. Typically the ``.subsorts`` of a
|
|
789
|
+
:class:`~unicode_logic_kit.fol.signature.Signature` the caller already has.
|
|
790
|
+
An edge ``S < T`` is ``∀x (S(x) → T(x))`` whether or not the theory has a
|
|
791
|
+
sorted node: an unsorted theory is held to every edge whose two ends it uses
|
|
792
|
+
as unary predicates, and an edge with an end the theory never mentions
|
|
793
|
+
changes nothing. With no ``subsorts`` the search is unchanged.
|
|
794
|
+
|
|
795
|
+
``symmetry_breaking`` (default True) enumerates CONSTANT assignments with the
|
|
796
|
+
LNH generator, :func:`_canonical_interpretations`, instead of the plain
|
|
797
|
+
:func:`_interpretations` — see the module docstring. Because the LNH-reduced
|
|
798
|
+
candidate count for a size can be far below the ANALYTIC (unbroken) count
|
|
799
|
+
:func:`_candidate_count` computes, the pre-flight "skip this size" check uses
|
|
800
|
+
:func:`_canonical_candidate_count` instead — the EXACT count of what the
|
|
801
|
+
canonical generator will yield, computed analytically in
|
|
802
|
+
``O(n_constants * domain size)`` arithmetic, with no enumeration and no model
|
|
803
|
+
checking. This is deliberately an O(1)-per-size analytic check, not a live
|
|
804
|
+
count of the generator: a signature with few or no constants (where LNH cannot
|
|
805
|
+
reduce anything — functions and predicates stay fully exhaustive either way)
|
|
806
|
+
is skipped exactly as cheaply as it was before symmetry breaking existed,
|
|
807
|
+
instead of paying to enumerate and model-check up to ``max_candidates``
|
|
808
|
+
structures on every size just to discover that a function- or
|
|
809
|
+
predicate-dominated signature was never going to fit the budget. A size still
|
|
810
|
+
only counts as "searched" if its exact canonical count is ``<= max_candidates``
|
|
811
|
+
(so it is always FULLY enumerated, never truncated mid-generator), preserving
|
|
812
|
+
the existing "no model found within the bounds" (never a false negative)
|
|
813
|
+
honesty contract. With ``symmetry_breaking=False`` the search is byte-for-byte
|
|
814
|
+
the original exhaustive one (the analytic pre-flight check, then
|
|
815
|
+
:func:`_interpretations`), kept available for differential testing against the
|
|
816
|
+
canonical generator.
|
|
817
|
+
Many-sorted (MSFOL) signatures are unaffected by this flag either way — sorted
|
|
818
|
+
search always uses the unbroken :func:`_sorted_interpretations` (see its own
|
|
819
|
+
scope note in the module docstring).
|
|
820
|
+
|
|
821
|
+
``timeout`` (milliseconds, default ``None``: no limit) is one more bound next to
|
|
822
|
+
``max_size`` and ``max_candidates``. The clock is read before every candidate
|
|
823
|
+
structure, so the search ends within the cost of one candidate of the deadline, and
|
|
824
|
+
answers ``None`` like any search that found nothing. A caller that must tell a
|
|
825
|
+
deadline from an exhausted bound uses :func:`search_model`.
|
|
826
|
+
"""
|
|
827
|
+
return search_model(formulas, max_size, max_candidates, symmetry_breaking,
|
|
828
|
+
subsorts, timeout).structure
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def search_model(formulas, max_size: int = 4,
|
|
832
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
833
|
+
symmetry_breaking: bool = True,
|
|
834
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
|
|
835
|
+
timeout: Optional[float] = None) -> ModelSearch:
|
|
836
|
+
""":func:`find_model`, returning a :class:`ModelSearch` that also says whether the
|
|
837
|
+
``timeout`` cut the search off.
|
|
838
|
+
|
|
839
|
+
The arguments are those of :func:`find_model`. ``timed_out`` is ``True`` when the
|
|
840
|
+
deadline had passed by the time the search ended without a structure: a search that
|
|
841
|
+
covered every size up to ``max_size`` that fits ``max_candidates`` before the deadline
|
|
842
|
+
is not reported as timed out, and one whose very last candidate was tested after it
|
|
843
|
+
is.
|
|
844
|
+
"""
|
|
845
|
+
deadline = _instant(timeout)
|
|
846
|
+
sentences, _ = parameterize(list(formulas), after_variables=True)
|
|
847
|
+
return _search_sentences(sentences, max_size, max_candidates, symmetry_breaking,
|
|
848
|
+
subsorts, deadline)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _search_sentences(sentences, max_size: int, max_candidates: int,
|
|
852
|
+
symmetry_breaking: bool,
|
|
853
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]],
|
|
854
|
+
deadline: Optional[float]) -> ModelSearch:
|
|
855
|
+
"""The search of :func:`search_model` for ``sentences``, which have no free variable.
|
|
856
|
+
|
|
857
|
+
``deadline`` is a ``perf_counter`` instant, or ``None`` for no limit.
|
|
858
|
+
|
|
859
|
+
Raises:
|
|
860
|
+
NotImplementedError: a cardinality term ``|{v : φ}|`` is not an operand of a
|
|
861
|
+
comparison with a number (see :func:`~unicode_logic_kit.semantics.tarski.satisfies`).
|
|
862
|
+
"""
|
|
863
|
+
_refuse_cardinality_as_individual(sentences, "semantics.modelfinder")
|
|
864
|
+
sig = _Signature()
|
|
865
|
+
for s in sentences:
|
|
866
|
+
sig.scan(s)
|
|
867
|
+
edges = _unsorted_edge_pairs(sig, subsorts)
|
|
868
|
+
for k in range(1, max_size + 1):
|
|
869
|
+
domain = tuple(range(k))
|
|
870
|
+
if sig.sorts:
|
|
871
|
+
if _candidate_count(sig, k) > max_candidates:
|
|
872
|
+
continue
|
|
873
|
+
for constants, functions, predicates, sorts in _sorted_interpretations(
|
|
874
|
+
sig, domain, subsorts, deadline):
|
|
875
|
+
if _passed(deadline):
|
|
876
|
+
return ModelSearch(None, True)
|
|
877
|
+
structure = Structure(domain, constants=constants, functions=functions,
|
|
878
|
+
predicates=predicates, sorts=sorts)
|
|
879
|
+
if all(models(s, structure) for s in sentences):
|
|
880
|
+
return ModelSearch(structure)
|
|
881
|
+
elif symmetry_breaking:
|
|
882
|
+
if _canonical_candidate_count(sig, k) > max_candidates:
|
|
883
|
+
continue
|
|
884
|
+
for constants, functions, predicates in _canonical_interpretations(sig, domain):
|
|
885
|
+
if _passed(deadline):
|
|
886
|
+
return ModelSearch(None, True)
|
|
887
|
+
if edges and not _predicate_edges_hold(predicates, edges):
|
|
888
|
+
continue
|
|
889
|
+
structure = Structure(domain, constants=constants,
|
|
890
|
+
functions=functions, predicates=predicates)
|
|
891
|
+
if all(models(s, structure) for s in sentences):
|
|
892
|
+
return ModelSearch(structure)
|
|
893
|
+
else:
|
|
894
|
+
if _candidate_count(sig, k) > max_candidates:
|
|
895
|
+
continue
|
|
896
|
+
for constants, functions, predicates in _interpretations(sig, domain):
|
|
897
|
+
if _passed(deadline):
|
|
898
|
+
return ModelSearch(None, True)
|
|
899
|
+
if edges and not _predicate_edges_hold(predicates, edges):
|
|
900
|
+
continue
|
|
901
|
+
structure = Structure(domain, constants=constants,
|
|
902
|
+
functions=functions, predicates=predicates)
|
|
903
|
+
if all(models(s, structure) for s in sentences):
|
|
904
|
+
return ModelSearch(structure)
|
|
905
|
+
if _passed(deadline):
|
|
906
|
+
return ModelSearch(None, True)
|
|
907
|
+
return ModelSearch(None)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def find_countermodel(premises, conclusion: Node, max_size: int = 4,
|
|
911
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
912
|
+
symmetry_breaking: bool = True,
|
|
913
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
|
|
914
|
+
timeout: Optional[float] = None) -> Optional[Structure]:
|
|
915
|
+
"""Return a finite structure satisfying ``premises`` but not ``conclusion``, or None.
|
|
916
|
+
|
|
917
|
+
A countermodel witnesses that ``premises`` do **not** entail ``conclusion``. A free
|
|
918
|
+
variable of the premises or of the conclusion is a parameter of the problem (see
|
|
919
|
+
the module docstring): the structure interprets it as a constant under its own
|
|
920
|
+
name, and the premises hold and the conclusion fails under that one assignment.
|
|
921
|
+
See :func:`find_model` for ``symmetry_breaking``, ``subsorts`` and ``timeout``.
|
|
922
|
+
"""
|
|
923
|
+
return search_countermodel(premises, conclusion, max_size, max_candidates,
|
|
924
|
+
symmetry_breaking, subsorts, timeout).structure
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def search_countermodel(premises, conclusion: Node, max_size: int = 4,
|
|
928
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
929
|
+
symmetry_breaking: bool = True,
|
|
930
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None,
|
|
931
|
+
timeout: Optional[float] = None) -> ModelSearch:
|
|
932
|
+
""":func:`find_countermodel`, returning a :class:`ModelSearch` that also says whether
|
|
933
|
+
the ``timeout`` cut the search off (see :func:`search_model`)."""
|
|
934
|
+
deadline = _instant(timeout)
|
|
935
|
+
closed, _ = parameterize(list(premises) + [conclusion], after_variables=True)
|
|
936
|
+
return _search_sentences(closed[:-1] + [Not(closed[-1])], max_size, max_candidates,
|
|
937
|
+
symmetry_breaking, subsorts, deadline)
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def is_satisfiable_finite(formula: Node, max_size: int = 4,
|
|
941
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
942
|
+
symmetry_breaking: bool = True,
|
|
943
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None) -> bool:
|
|
944
|
+
"""True iff ``formula`` has a finite model of size ≤ ``max_size`` (bounded)."""
|
|
945
|
+
return find_model([formula], max_size, max_candidates,
|
|
946
|
+
symmetry_breaking, subsorts) is not None
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
def is_valid_finite(formula: Node, max_size: int = 4,
|
|
950
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
951
|
+
symmetry_breaking: bool = True,
|
|
952
|
+
subsorts: Optional[Mapping[str, FrozenSet[str]]] = None) -> bool:
|
|
953
|
+
"""True iff no finite countermodel of ``formula`` exists up to ``max_size`` (bounded).
|
|
954
|
+
|
|
955
|
+
Bounded and one-sided: True means "no countermodel found within the bounds"
|
|
956
|
+
(strong evidence of validity, not a proof); a False is a genuine refutation —
|
|
957
|
+
:func:`find_countermodel` returns the witnessing structure.
|
|
958
|
+
"""
|
|
959
|
+
return find_countermodel([], formula, max_size, max_candidates,
|
|
960
|
+
symmetry_breaking, subsorts) is None
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def is_size_exhaustive(formulas: Iterable[Node], k: int,
|
|
964
|
+
max_candidates: int = MAX_CANDIDATES,
|
|
965
|
+
symmetry_breaking: bool = True) -> bool:
|
|
966
|
+
"""True iff :func:`find_model` would FULLY enumerate domain size ``k`` for
|
|
967
|
+
this theory's signature, rather than skip it.
|
|
968
|
+
|
|
969
|
+
``find_model`` conflates two reasons a given size ``k`` contributes nothing
|
|
970
|
+
to its ``None`` result: either every interpretation at size ``k`` was tried
|
|
971
|
+
and none satisfied the theory (a genuine refutation of that size), or the
|
|
972
|
+
interpretation space at size ``k`` exceeded ``max_candidates`` and the whole
|
|
973
|
+
size was skipped via ``continue`` without a single structure being built or
|
|
974
|
+
checked (see the module docstring's "honesty contract" and ``find_model``'s
|
|
975
|
+
own per-size ``continue``). Both look identical from the outside — a caller
|
|
976
|
+
cannot tell "refuted" from "never asked" just by seeing that ``find_model``
|
|
977
|
+
returned ``None`` — so a claim like "size ``k`` has no model" (e.g. to
|
|
978
|
+
certify a *minimal* model size found at some larger size) needs this
|
|
979
|
+
function alongside ``find_model`` to rule the "skipped" case out. A skipped
|
|
980
|
+
size is NEVER reported as "no model" by this module; it is this function's
|
|
981
|
+
job to let a caller detect that case explicitly instead of silently trusting
|
|
982
|
+
the absence.
|
|
983
|
+
|
|
984
|
+
This is a pure COUNT-ONLY pre-flight check — it mirrors exactly the
|
|
985
|
+
candidate-count arithmetic ``find_model`` itself runs before searching a
|
|
986
|
+
size, and does not run the search or the Tarskian evaluator at all, so it
|
|
987
|
+
costs O(1) arithmetic regardless of ``k``.
|
|
988
|
+
|
|
989
|
+
What is being counted (this matters for what "exhaustive" promises):
|
|
990
|
+
|
|
991
|
+
- For a many-sorted (MSFOL) signature (any formula uses a
|
|
992
|
+
:class:`~unicode_logic_kit.fol.nodes.SortedQuantifier` or sorted constant),
|
|
993
|
+
the count is the RAW, unreduced number of interpretations
|
|
994
|
+
(:func:`_candidate_count`) — ``symmetry_breaking`` is ignored, exactly as
|
|
995
|
+
``find_model`` itself ignores it for sorted search (see the module
|
|
996
|
+
docstring's "Subsorting" section: sorted search always uses the unbroken
|
|
997
|
+
:func:`_sorted_interpretations`). A unary predicate that has the name of
|
|
998
|
+
a sort is not a factor of its own: it is the sort.
|
|
999
|
+
- Otherwise, with ``symmetry_breaking=True`` (the default, matching
|
|
1000
|
+
``find_model``'s own default), the count is
|
|
1001
|
+
:func:`_canonical_candidate_count`: ONE representative per isomorphism
|
|
1002
|
+
class of CONSTANT assignments (the LNH reduction), crossed with the FULL,
|
|
1003
|
+
unreduced enumeration of function and predicate tables — this is NOT "one
|
|
1004
|
+
representative per isomorphism class of the whole structure", only of the
|
|
1005
|
+
constant-naming part (see the module docstring's LNH section for why
|
|
1006
|
+
functions/predicates stay exhaustive).
|
|
1007
|
+
- With ``symmetry_breaking=False``, the count is the raw, unreduced
|
|
1008
|
+
:func:`_candidate_count` — every structure, no isomorphism reduction at
|
|
1009
|
+
all.
|
|
1010
|
+
|
|
1011
|
+
A size is exhaustive iff its count is ``<= max_candidates``; ``find_model``
|
|
1012
|
+
always fully enumerates such a size (never truncates mid-generator), so
|
|
1013
|
+
"exhaustive" here means exactly what it means there.
|
|
1014
|
+
|
|
1015
|
+
``subsorts`` has no parameter here because it never changes this count:
|
|
1016
|
+
:func:`find_model` applies a subsort edge as a FILTER during enumeration
|
|
1017
|
+
(:func:`_respects_subsorts`), which only discards candidates the unfiltered
|
|
1018
|
+
count already includes — it can only make an exhaustive search find fewer
|
|
1019
|
+
or equal models, never fall outside the ``max_candidates`` bound the
|
|
1020
|
+
unfiltered count already decided.
|
|
1021
|
+
|
|
1022
|
+
Raises:
|
|
1023
|
+
NotImplementedError: a cardinality term ``|{v : φ}|`` is not an operand of a
|
|
1024
|
+
comparison with a number: the search of such a theory is refused, so its size
|
|
1025
|
+
is not reported as exhaustive either.
|
|
1026
|
+
"""
|
|
1027
|
+
sentences, _ = parameterize(list(formulas), after_variables=True)
|
|
1028
|
+
_refuse_cardinality_as_individual(sentences, "semantics.modelfinder")
|
|
1029
|
+
sig = _Signature()
|
|
1030
|
+
for s in sentences:
|
|
1031
|
+
sig.scan(s)
|
|
1032
|
+
if sig.sorts:
|
|
1033
|
+
return _candidate_count(sig, k) <= max_candidates
|
|
1034
|
+
if symmetry_breaking:
|
|
1035
|
+
return _canonical_candidate_count(sig, k) <= max_candidates
|
|
1036
|
+
return _candidate_count(sig, k) <= max_candidates
|