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,533 @@
|
|
|
1
|
+
"""Constructively-generated, independently-checked practice exercises.
|
|
2
|
+
|
|
3
|
+
This is teaching infrastructure, not an evaluator: everything else in
|
|
4
|
+
:mod:`unicode_logic_kit.eval` assumes an LLM (or a student) already produced a
|
|
5
|
+
formula that needs SCORING against a gold answer. This module runs the other
|
|
6
|
+
direction — it MANUFACTURES the exercise (formula, proof, or theory) itself,
|
|
7
|
+
purely constructively, with **no LLM involved anywhere**. Every answer key is
|
|
8
|
+
either decided by a genuine decision procedure or assembled by hand from sound
|
|
9
|
+
primitives and then re-checked by an INDEPENDENT route before it is returned,
|
|
10
|
+
so a bug here can make a generator *refuse* or *retry* but never ship a wrong
|
|
11
|
+
answer key.
|
|
12
|
+
|
|
13
|
+
Three generators, each deliberately narrower than the roadmap's original
|
|
14
|
+
framing, to avoid two claims this kit's own primitives cannot actually
|
|
15
|
+
support (see each function's docstring for the full argument):
|
|
16
|
+
|
|
17
|
+
* :func:`generate_valid_invalid_pair` — samples a quantifier-free formula
|
|
18
|
+
over the **propositional fragment** (0-ary predicates from ``signature``,
|
|
19
|
+
each one an opaque proposition) and classifies it with
|
|
20
|
+
:func:`~unicode_logic_kit.semantics.truthtable.truth_table` — a genuine,
|
|
21
|
+
complete decision procedure for that fragment, no caveats. A first-order
|
|
22
|
+
variant (quantified formulas, classified by countermodel-search-plus-proof)
|
|
23
|
+
is explicitly OUT OF SCOPE here: it would need both a countermodel miss
|
|
24
|
+
(bounded, hence never a proof of validity on its own) AND a positive proof
|
|
25
|
+
from a refutation-complete backend before a "valid" label was honest, and
|
|
26
|
+
the roadmap itself flags this as a stretch goal, not this module's job.
|
|
27
|
+
|
|
28
|
+
* :func:`generate_entailment_with_proof` — does **not** search for a proof of
|
|
29
|
+
a given depth. :func:`~unicode_logic_kit.atp.fitch_search.find_fitch_proof`'s
|
|
30
|
+
own docstring is explicit that a depth-bounded search returning ``None``
|
|
31
|
+
means "no proof found within the bound", never "not a theorem" — so "no
|
|
32
|
+
proof found at depth d−1" can never certify that a proof of depth d is
|
|
33
|
+
*minimal*. Instead this generator builds the answer key CONSTRUCTIVELY,
|
|
34
|
+
composing :mod:`unicode_logic_kit.atp.fitch`'s ``Proof``/``Subproof``/``Line``
|
|
35
|
+
primitives bottom-up into a derivation with EXACTLY the requested number of
|
|
36
|
+
nested subproof levels, then verifies it with the existing checker
|
|
37
|
+
(``verify_proof`` — "soundness is free", per that module's own docstring).
|
|
38
|
+
The claim shipped is "the worked solution has depth N", never "the minimal
|
|
39
|
+
proof has depth N" — exactly how a textbook exercise is actually authored.
|
|
40
|
+
|
|
41
|
+
* :func:`generate_theory_with_model_size` — samples a small finite theory (a
|
|
42
|
+
strict total order over one binary relation from ``signature``, forced to
|
|
43
|
+
contain a chain of the requested length) and confirms BOTH that a model of
|
|
44
|
+
exactly the target size exists AND that no smaller one does. The second
|
|
45
|
+
half is the subtle part: :func:`~unicode_logic_kit.semantics.modelfinder.find_model`
|
|
46
|
+
conflates "this size was searched and refuted" with "this size was SKIPPED
|
|
47
|
+
because its interpretation space exceeded ``max_candidates``" — both look
|
|
48
|
+
like plain absence from outside. This generator therefore also calls the
|
|
49
|
+
new :func:`~unicode_logic_kit.semantics.modelfinder.is_size_exhaustive` helper
|
|
50
|
+
for every size below the target and refuses (``ValueError``, never a
|
|
51
|
+
silent wrong "minimal size" claim) if any of them was only skipped.
|
|
52
|
+
|
|
53
|
+
Determinism. Every generator takes an optional ``seed``; the SAME seed always
|
|
54
|
+
produces a BYTE-IDENTICAL exercise (same AST, same ``to_unicode_str()``
|
|
55
|
+
rendering, same proof, same witness structure) — see
|
|
56
|
+
``tests/test_exercise_gen.py``'s reproducibility tests, which pin this
|
|
57
|
+
directly. ``seed=None`` draws from unseeded system randomness instead.
|
|
58
|
+
Caveat for :class:`ModelSizeExercise`: "byte-identical" describes its
|
|
59
|
+
*content*, not what ``==`` reports — its ``witness`` field is a
|
|
60
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure`, which defines no
|
|
61
|
+
``__eq__`` and so compares by object identity. Two exercises built from the
|
|
62
|
+
same seed are content-identical but ``exercise1 == exercise2`` is always
|
|
63
|
+
``False``; compare ``witness.domain`` / ``witness.constants`` /
|
|
64
|
+
``witness.predicates`` / ``witness.sorts`` field-by-field instead (see
|
|
65
|
+
``ModelSizeExercise``'s own docstring, and how the reproducibility test for
|
|
66
|
+
this generator does it).
|
|
67
|
+
|
|
68
|
+
Deviations from the roadmap draft that produced this module (recorded here
|
|
69
|
+
rather than silently "fixed", per this kit's convention):
|
|
70
|
+
|
|
71
|
+
* The draft's test-oracle text names ``semantics.evaluator.models`` as the
|
|
72
|
+
independent countermodel checker; no ``unicode_logic_kit.semantics.evaluator``
|
|
73
|
+
module exists. The actual function, used throughout this module and its
|
|
74
|
+
tests, is :func:`unicode_logic_kit.semantics.tarski.models`.
|
|
75
|
+
* The draft names ``docs/guide/teaching.md`` as the surfacing page; the
|
|
76
|
+
accepted filename for this batch is ``docs/guide/exercises.md`` (see that
|
|
77
|
+
file).
|
|
78
|
+
* No MCP tool is added here: ``unicode_logic_kit/mcp/server.py`` is outside
|
|
79
|
+
this module's ownership for this change.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
import random
|
|
83
|
+
from dataclasses import dataclass
|
|
84
|
+
from functools import reduce
|
|
85
|
+
from typing import Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
|
86
|
+
|
|
87
|
+
from ..fol.nodes import Atom, And, Implies, Node, Not, Or, Quantifier, Variable
|
|
88
|
+
from ..fol.signature import Signature
|
|
89
|
+
from ..semantics import modelfinder
|
|
90
|
+
from ..semantics.tarski import Structure, models
|
|
91
|
+
from ..semantics.truthtable import TruthTable, truth_table
|
|
92
|
+
from ..atp.fitch import Line, Proof, Subproof, assume, line, verify_proof
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"ValidInvalidPair", "generate_valid_invalid_pair",
|
|
96
|
+
"EntailmentExercise", "generate_entailment_with_proof",
|
|
97
|
+
"ModelSizeExercise", "generate_theory_with_model_size",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
#: How many random samples a generator tries before falling back to a
|
|
101
|
+
#: deterministic, formula-shape-guaranteed construction (see
|
|
102
|
+
#: ``generate_valid_invalid_pair``). Generous relative to how quickly a small
|
|
103
|
+
#: random propositional formula tends to land on either side of "tautology",
|
|
104
|
+
#: so the fallback is rarely exercised in practice; it exists purely so every
|
|
105
|
+
#: call TERMINATES, never so a returned label is trusted without the
|
|
106
|
+
#: post-construction self-check that follows it either way.
|
|
107
|
+
_RETRY_BUDGET = 60
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Shared: the propositional atom pool a signature offers, and a small
|
|
112
|
+
# seeded quantifier-free grammar over it.
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
def _nullary_atoms(signature: Signature) -> Tuple[Atom, ...]:
|
|
116
|
+
"""The 0-ary predicates ``signature`` declares, as bare :class:`Atom` nodes.
|
|
117
|
+
|
|
118
|
+
Sorted by name first so the pool itself is deterministic across calls
|
|
119
|
+
with the same ``signature`` regardless of dict iteration order; the
|
|
120
|
+
caller's ``seed`` then governs which of them are actually picked.
|
|
121
|
+
"""
|
|
122
|
+
return tuple(
|
|
123
|
+
Atom(name, ())
|
|
124
|
+
for name, decl in sorted(signature.predicates.items())
|
|
125
|
+
if decl.arity == 0
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _random_prop_formula(rng: random.Random, atoms: Sequence[Atom], budget: int) -> Node:
|
|
130
|
+
"""A random quantifier-free formula over ``atoms``, using only the AST
|
|
131
|
+
constructors ``And``/``Or``/``Not``/``Implies``/``Atom`` (per the roadmap
|
|
132
|
+
spec). ``budget`` bounds the recursion so a call always terminates and the
|
|
133
|
+
result stays exercise-sized; it is consumed by one per connective, and a
|
|
134
|
+
leaf (bare atom, or its negation) can also be chosen early at random.
|
|
135
|
+
"""
|
|
136
|
+
if budget <= 0 or rng.random() < 0.4:
|
|
137
|
+
atom = rng.choice(atoms)
|
|
138
|
+
return Not(atom) if rng.random() < 0.3 else atom
|
|
139
|
+
op = rng.choice(("and", "or", "implies", "not"))
|
|
140
|
+
if op == "not":
|
|
141
|
+
return Not(_random_prop_formula(rng, atoms, budget - 1))
|
|
142
|
+
left = _random_prop_formula(rng, atoms, budget - 1)
|
|
143
|
+
right = _random_prop_formula(rng, atoms, budget - 1)
|
|
144
|
+
cls = {"and": And, "or": Or, "implies": Implies}[op]
|
|
145
|
+
return cls(left, right)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _first_countermodel(table: TruthTable) -> Dict[str, bool]:
|
|
149
|
+
"""The first (in row order) non-designated valuation of ``table`` as an
|
|
150
|
+
``{atom_surface_form: bool}`` mapping. Requires ``table`` to actually have
|
|
151
|
+
one (i.e. not be a tautology) — callers check that first.
|
|
152
|
+
"""
|
|
153
|
+
for assignment, _value, designated in table.rows:
|
|
154
|
+
if not designated:
|
|
155
|
+
return {atom: bool(v) for atom, v in zip(table.atoms, assignment)}
|
|
156
|
+
raise AssertionError( # pragma: no cover — callers only reach this for a non-tautology
|
|
157
|
+
"internal: _first_countermodel called on a tautology (no falsifying row)."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _nullary_structure(valuation: Mapping[str, bool]) -> Structure:
|
|
162
|
+
"""A minimal :class:`Structure` encoding a propositional valuation.
|
|
163
|
+
|
|
164
|
+
One dummy individual as domain (no term in a nullary-atom formula ever
|
|
165
|
+
needs more) and each atom mapped to its truth value at ``(name, 0)`` —
|
|
166
|
+
exactly :class:`Structure`'s own documented nullary-predicate convention,
|
|
167
|
+
so :func:`~unicode_logic_kit.semantics.tarski.models` reads it correctly.
|
|
168
|
+
"""
|
|
169
|
+
return Structure(domain=("*",), predicates={(name, 0): bool(v) for name, v in valuation.items()})
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# 1. generate_valid_invalid_pair
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
@dataclass(frozen=True)
|
|
177
|
+
class ValidInvalidPair:
|
|
178
|
+
"""One valid (tautologous) and one invalid (non-tautologous) propositional
|
|
179
|
+
formula, sampled over the same atom pool.
|
|
180
|
+
|
|
181
|
+
``invalid_valuation`` is a genuine falsifying assignment for
|
|
182
|
+
``invalid_formula`` — a plain ``{atom_surface_form: bool}`` mapping, chosen
|
|
183
|
+
so a caller (or a test) can rebuild a
|
|
184
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure` from it independently
|
|
185
|
+
of how this module built its own (see
|
|
186
|
+
:func:`~unicode_logic_kit.semantics.tarski.models`). ``atoms`` is the pool
|
|
187
|
+
this pair was sampled from — not necessarily every atom in it appears in
|
|
188
|
+
either formula.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
valid_formula: Node
|
|
192
|
+
invalid_formula: Node
|
|
193
|
+
invalid_valuation: Mapping[str, bool]
|
|
194
|
+
atoms: Tuple[Atom, ...]
|
|
195
|
+
seed: Optional[int]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def generate_valid_invalid_pair(signature: Signature, max_atoms: int = 3,
|
|
199
|
+
seed: Optional[int] = None) -> ValidInvalidPair:
|
|
200
|
+
"""Sample a valid/invalid propositional exercise pair over ``signature``.
|
|
201
|
+
|
|
202
|
+
Draws up to ``max_atoms`` distinct 0-ary predicates from ``signature`` as
|
|
203
|
+
the propositional atom pool (fewer if the signature declares fewer;
|
|
204
|
+
refuses if it declares none), samples quantifier-free formulas from a
|
|
205
|
+
small seeded grammar (:func:`And`/:func:`Or`/:func:`Not`/:func:`Implies`
|
|
206
|
+
over those atoms), and classifies each with
|
|
207
|
+
:func:`~unicode_logic_kit.semantics.truthtable.truth_table` — a complete
|
|
208
|
+
decision procedure for this fragment, so the "valid" label is never a
|
|
209
|
+
guess. Retries up to :data:`_RETRY_BUDGET` times per formula to find a
|
|
210
|
+
naturally-shaped tautology / non-tautology; if that budget is exhausted
|
|
211
|
+
(rare — most small random formulas are quickly classified either way) it
|
|
212
|
+
falls back to a construction that is tautologous (resp. non-tautologous)
|
|
213
|
+
BY CONSTRUCTION — ``Implies(phi, phi)`` is a tautology for any ``phi``;
|
|
214
|
+
``And(atom, Not(atom))`` is a contradiction, hence never a tautology —
|
|
215
|
+
so every call terminates. Either way, both formulas are re-classified
|
|
216
|
+
(and the countermodel independently re-evaluated against a fresh
|
|
217
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure` via
|
|
218
|
+
:func:`~unicode_logic_kit.semantics.tarski.models`) before returning, so a
|
|
219
|
+
bug in the sampler can only make this function raise, never ship a
|
|
220
|
+
mislabelled pair.
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
ValueError: ``max_atoms < 1``, or ``signature`` declares no 0-ary
|
|
224
|
+
predicate (this generator is propositional-only — see the module
|
|
225
|
+
docstring for why the first-order case is out of scope here).
|
|
226
|
+
"""
|
|
227
|
+
if max_atoms < 1:
|
|
228
|
+
raise ValueError("generate_valid_invalid_pair: max_atoms must be >= 1.")
|
|
229
|
+
pool = _nullary_atoms(signature)
|
|
230
|
+
if not pool:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"generate_valid_invalid_pair: signature declares no nullary (arity-0) "
|
|
233
|
+
"predicates; this generator samples over the propositional fragment "
|
|
234
|
+
"only, where each distinct atom is a bare propositional variable -- "
|
|
235
|
+
"add at least one 0-ary predicate to the signature."
|
|
236
|
+
)
|
|
237
|
+
rng = random.Random(seed)
|
|
238
|
+
n = min(max_atoms, len(pool))
|
|
239
|
+
atoms = tuple(rng.sample(pool, n))
|
|
240
|
+
budget = n + 2
|
|
241
|
+
|
|
242
|
+
valid_formula: Optional[Node] = None
|
|
243
|
+
for _ in range(_RETRY_BUDGET):
|
|
244
|
+
candidate = _random_prop_formula(rng, atoms, budget)
|
|
245
|
+
if truth_table(candidate).is_tautology:
|
|
246
|
+
valid_formula = candidate
|
|
247
|
+
break
|
|
248
|
+
if valid_formula is None:
|
|
249
|
+
phi = _random_prop_formula(rng, atoms, budget)
|
|
250
|
+
valid_formula = Implies(phi, phi)
|
|
251
|
+
|
|
252
|
+
invalid_formula: Optional[Node] = None
|
|
253
|
+
invalid_valuation: Optional[Dict[str, bool]] = None
|
|
254
|
+
for _ in range(_RETRY_BUDGET):
|
|
255
|
+
candidate = _random_prop_formula(rng, atoms, budget)
|
|
256
|
+
table = truth_table(candidate)
|
|
257
|
+
if not table.is_tautology:
|
|
258
|
+
invalid_formula = candidate
|
|
259
|
+
invalid_valuation = _first_countermodel(table)
|
|
260
|
+
break
|
|
261
|
+
if invalid_formula is None:
|
|
262
|
+
atom0 = atoms[0]
|
|
263
|
+
invalid_formula = And(atom0, Not(atom0))
|
|
264
|
+
invalid_valuation = {atom0.predicate: False}
|
|
265
|
+
|
|
266
|
+
# Self-check (defence in depth, same route as construction). The
|
|
267
|
+
# INDEPENDENT check -- a fresh 2^n enumeration and a fresh Structure, not
|
|
268
|
+
# reusing truth_table/models at all -- lives in tests/test_exercise_gen.py.
|
|
269
|
+
if not truth_table(valid_formula).is_tautology:
|
|
270
|
+
raise AssertionError("internal: constructed 'valid' formula is not a tautology.")
|
|
271
|
+
if truth_table(invalid_formula).is_tautology:
|
|
272
|
+
raise AssertionError("internal: constructed 'invalid' formula is a tautology.")
|
|
273
|
+
if models(invalid_formula, _nullary_structure(invalid_valuation)):
|
|
274
|
+
raise AssertionError("internal: invalid_valuation does not falsify invalid_formula.")
|
|
275
|
+
|
|
276
|
+
return ValidInvalidPair(valid_formula, invalid_formula, invalid_valuation, atoms, seed)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# 2. generate_entailment_with_proof
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
@dataclass(frozen=True)
|
|
284
|
+
class EntailmentExercise:
|
|
285
|
+
"""A constructively-built Fitch proof exercise.
|
|
286
|
+
|
|
287
|
+
``depth`` is a STATIC count of nested :class:`~unicode_logic_kit.atp.fitch.Subproof`
|
|
288
|
+
levels in ``proof.steps`` — the shipped worked solution's depth, not a
|
|
289
|
+
claim that no shallower proof of ``conclusion`` exists (see the module
|
|
290
|
+
docstring). ``premises``/``conclusion`` are read off the checked proof
|
|
291
|
+
itself (:func:`~unicode_logic_kit.atp.fitch.verify_proof`'s own certified
|
|
292
|
+
sequent), not re-derived separately.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
proof: Proof
|
|
296
|
+
premises: Tuple[Node, ...]
|
|
297
|
+
conclusion: Node
|
|
298
|
+
depth: int
|
|
299
|
+
seed: Optional[int]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _build_depth_chain(atoms: Sequence[Atom], start: int
|
|
303
|
+
) -> Tuple[Subproof, Line, int]:
|
|
304
|
+
"""Build one ``→I`` box for ``atoms[0]``, recursively nesting one more box
|
|
305
|
+
per remaining atom, numbering lines from ``start``.
|
|
306
|
+
|
|
307
|
+
The innermost box (a single remaining atom) just reiterates its own
|
|
308
|
+
assumption; each enclosing box discharges the one it wraps with ``→I``,
|
|
309
|
+
so the fully assembled chain proves
|
|
310
|
+
``atoms[0] → (atoms[1] → ( … → (atoms[-1] → atoms[-1]) … ))`` with
|
|
311
|
+
exactly ``len(atoms)`` nested subproof levels — one per atom. Returns
|
|
312
|
+
``(subproof, discharge_line, next_free_line_number)``: ``subproof`` is
|
|
313
|
+
the box for ``atoms[0]`` and ``discharge_line`` is the ``→I`` line that
|
|
314
|
+
closes it (placed by the CALLER, in the enclosing scope — a box never
|
|
315
|
+
contains the line that closes it).
|
|
316
|
+
"""
|
|
317
|
+
a0 = atoms[0]
|
|
318
|
+
assume_line = assume(start, a0)
|
|
319
|
+
if len(atoms) == 1:
|
|
320
|
+
reit_line = line(start + 1, a0, "Reit", start)
|
|
321
|
+
body: Tuple[Union[Line, Subproof], ...] = (reit_line,)
|
|
322
|
+
body_formula = a0
|
|
323
|
+
next_num = start + 2
|
|
324
|
+
else:
|
|
325
|
+
inner_subproof, inner_discharge, next_num = _build_depth_chain(atoms[1:], start + 1)
|
|
326
|
+
body = (inner_subproof, inner_discharge)
|
|
327
|
+
body_formula = inner_discharge.formula
|
|
328
|
+
subproof = Subproof(assumption=assume_line, body=body)
|
|
329
|
+
discharge = line(next_num, Implies(a0, body_formula), "→I", (start, next_num - 1))
|
|
330
|
+
return subproof, discharge, next_num + 1
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _nesting_depth(steps: Sequence[Union[Line, Subproof]]) -> int:
|
|
334
|
+
"""The maximum number of NESTED :class:`Subproof` levels among ``steps``
|
|
335
|
+
(0 if none; two sibling subproofs at the same level both count as 1, not
|
|
336
|
+
2 — see the module's De Morgan / LEM hand-counted examples in the tests).
|
|
337
|
+
"""
|
|
338
|
+
depth = 0
|
|
339
|
+
for step in steps:
|
|
340
|
+
if isinstance(step, Subproof):
|
|
341
|
+
depth = max(depth, 1 + _nesting_depth(step.body))
|
|
342
|
+
return depth
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def generate_entailment_with_proof(signature: Signature, target_depth: int,
|
|
346
|
+
seed: Optional[int] = None) -> EntailmentExercise:
|
|
347
|
+
"""Constructively build a Fitch proof with exactly ``target_depth`` nested
|
|
348
|
+
subproof levels, over ``target_depth`` distinct 0-ary predicates drawn
|
|
349
|
+
from ``signature``.
|
|
350
|
+
|
|
351
|
+
The derivation is a chain of nested ``→I`` introductions (see
|
|
352
|
+
:func:`_build_depth_chain`): no search is performed, so the depth is
|
|
353
|
+
exact by construction, not merely a search bound. The assembled proof is
|
|
354
|
+
re-checked with :func:`~unicode_logic_kit.atp.fitch.verify_proof` before
|
|
355
|
+
being returned — the module's own "soundness is free" independent
|
|
356
|
+
checker (see the module docstring for why a depth-bounded SEARCH could
|
|
357
|
+
never certify this the same way).
|
|
358
|
+
|
|
359
|
+
Raises:
|
|
360
|
+
ValueError: ``target_depth < 1``, or ``signature`` declares fewer
|
|
361
|
+
than ``target_depth`` distinct 0-ary predicates.
|
|
362
|
+
"""
|
|
363
|
+
if target_depth < 1:
|
|
364
|
+
raise ValueError("generate_entailment_with_proof: target_depth must be >= 1.")
|
|
365
|
+
pool = _nullary_atoms(signature)
|
|
366
|
+
if len(pool) < target_depth:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"generate_entailment_with_proof: signature declares only "
|
|
369
|
+
f"{len(pool)} nullary (arity-0) predicate(s), but target_depth="
|
|
370
|
+
f"{target_depth} distinct ones are needed -- this generator builds "
|
|
371
|
+
"one nested ->I box per level over distinct propositional atoms."
|
|
372
|
+
)
|
|
373
|
+
rng = random.Random(seed)
|
|
374
|
+
atoms = rng.sample(pool, target_depth)
|
|
375
|
+
|
|
376
|
+
subproof, discharge, _next = _build_depth_chain(atoms, 1)
|
|
377
|
+
proof = Proof(premises=(), steps=(subproof, discharge), logic="fol")
|
|
378
|
+
|
|
379
|
+
result = verify_proof(proof)
|
|
380
|
+
if not result.ok:
|
|
381
|
+
raise AssertionError( # pragma: no cover -- construction is formally sound
|
|
382
|
+
f"internal: constructively-built proof failed to verify at line "
|
|
383
|
+
f"{result.error_line}: {result.error}"
|
|
384
|
+
)
|
|
385
|
+
depth = _nesting_depth(proof.steps)
|
|
386
|
+
if depth != target_depth:
|
|
387
|
+
raise AssertionError( # pragma: no cover -- one box per atom, by construction
|
|
388
|
+
f"internal: assembled proof has nesting depth {depth}, expected {target_depth}."
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return EntailmentExercise(proof=proof, premises=result.premises,
|
|
392
|
+
conclusion=result.conclusion, depth=depth, seed=seed)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ---------------------------------------------------------------------------
|
|
396
|
+
# 3. generate_theory_with_model_size
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
@dataclass(frozen=True)
|
|
400
|
+
class ModelSizeExercise:
|
|
401
|
+
"""A small finite theory whose minimal model size is exactly ``target_size``.
|
|
402
|
+
|
|
403
|
+
``theory`` is a strict total order (irreflexive, transitive, total) over
|
|
404
|
+
the binary predicate ``relation`` (drawn from ``signature``), conjoined
|
|
405
|
+
with an existential chain forcing at least ``target_size`` pairwise
|
|
406
|
+
distinct, linearly ``relation``-ordered elements. ``witness`` is a
|
|
407
|
+
concrete model of exactly that size, found by
|
|
408
|
+
:func:`~unicode_logic_kit.semantics.modelfinder.find_model`.
|
|
409
|
+
|
|
410
|
+
Equality caveat: ``witness`` is a
|
|
411
|
+
:class:`~unicode_logic_kit.semantics.tarski.Structure`, which has no
|
|
412
|
+
``__eq__`` and so compares by object identity — two
|
|
413
|
+
``ModelSizeExercise`` values with identical content (e.g. from the same
|
|
414
|
+
``seed``) are never ``==`` to each other. Compare ``witness.domain``,
|
|
415
|
+
``witness.constants``, ``witness.predicates`` and ``witness.sorts``
|
|
416
|
+
directly instead of the whole dataclass or the whole ``witness``.
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
theory: Tuple[Node, ...]
|
|
420
|
+
target_size: int
|
|
421
|
+
relation: str
|
|
422
|
+
witness: Structure
|
|
423
|
+
seed: Optional[int]
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def generate_theory_with_model_size(
|
|
427
|
+
signature: Signature, target_size: int, seed: Optional[int] = None,
|
|
428
|
+
max_candidates: int = modelfinder.MAX_CANDIDATES,
|
|
429
|
+
) -> ModelSizeExercise:
|
|
430
|
+
"""Build a theory (a strict total order forced to contain a chain of
|
|
431
|
+
length ``target_size``) whose minimal finite model size is EXACTLY
|
|
432
|
+
``target_size``, over one binary predicate drawn from ``signature``.
|
|
433
|
+
|
|
434
|
+
Why this family: a strict order (irreflexive + transitive + total) that
|
|
435
|
+
additionally asserts a chain ``x1 R x2 R … R xN`` forces at least ``N``
|
|
436
|
+
pairwise distinct elements (irreflexivity + transitivity rule out any
|
|
437
|
+
``xi = xj``), and a genuine ``N``-element total order trivially satisfies
|
|
438
|
+
everything, so the minimal model size is exactly ``N`` — the textbook
|
|
439
|
+
fact this roadmap item's own test oracle names ("an irreflexive total
|
|
440
|
+
order needs domain size >= 2") generalised to an arbitrary chain length.
|
|
441
|
+
|
|
442
|
+
Both halves of the claim are confirmed before returning, not assumed:
|
|
443
|
+
|
|
444
|
+
- :func:`~unicode_logic_kit.semantics.modelfinder.is_size_exhaustive` is
|
|
445
|
+
checked for every size ``1..target_size`` FIRST. ``find_model`` itself
|
|
446
|
+
conflates "this size was searched and refuted" with "this size was
|
|
447
|
+
skipped because its interpretation space exceeded ``max_candidates``"
|
|
448
|
+
(see that function's docstring) — a claim like "no smaller model
|
|
449
|
+
exists" would be unsound if it rested on a skipped size, so this
|
|
450
|
+
generator refuses outright (``ValueError``, not a wrong "minimal size"
|
|
451
|
+
claim) rather than risk that.
|
|
452
|
+
- Only once every relevant size is confirmed exhaustive does it call
|
|
453
|
+
``find_model`` for the witness (at ``target_size``) and for minimality
|
|
454
|
+
(at ``target_size - 1``, expecting ``None``).
|
|
455
|
+
|
|
456
|
+
Raises:
|
|
457
|
+
ValueError: ``target_size < 1``; ``signature`` declares no binary
|
|
458
|
+
(arity-2) predicate; or some size ``<= target_size`` would be
|
|
459
|
+
skipped (not exhaustively searched) under ``max_candidates`` —
|
|
460
|
+
raise ``max_candidates`` or lower ``target_size`` to proceed. A
|
|
461
|
+
binary relation's interpretation count grows as ``2**(k**2)``, so
|
|
462
|
+
this budget is reached well before ``target_size`` gets large —
|
|
463
|
+
by design: refusing loudly beats silently narrowing the claim.
|
|
464
|
+
"""
|
|
465
|
+
if target_size < 1:
|
|
466
|
+
raise ValueError("generate_theory_with_model_size: target_size must be >= 1.")
|
|
467
|
+
binaries = sorted(
|
|
468
|
+
name for name, decl in signature.predicates.items() if decl.arity == 2
|
|
469
|
+
)
|
|
470
|
+
if not binaries:
|
|
471
|
+
raise ValueError(
|
|
472
|
+
"generate_theory_with_model_size: signature declares no binary "
|
|
473
|
+
"(arity-2) predicate; this generator builds a strict-total-order "
|
|
474
|
+
"theory (irreflexive, transitive, total) over one binary relation, "
|
|
475
|
+
"forced to a chain of the target length -- add a 2-ary predicate."
|
|
476
|
+
)
|
|
477
|
+
rng = random.Random(seed)
|
|
478
|
+
relation = rng.choice(binaries)
|
|
479
|
+
|
|
480
|
+
def R(a: Node, b: Node) -> Atom:
|
|
481
|
+
return Atom(relation, (a, b))
|
|
482
|
+
|
|
483
|
+
x, y, z = Variable("x"), Variable("y"), Variable("z")
|
|
484
|
+
irreflexive = Quantifier("∀", x, Not(R(x, x)))
|
|
485
|
+
transitive = Quantifier(
|
|
486
|
+
"∀", x, Quantifier(
|
|
487
|
+
"∀", y, Quantifier(
|
|
488
|
+
"∀", z, Implies(And(R(x, y), R(y, z)), R(x, z)))))
|
|
489
|
+
total = Quantifier(
|
|
490
|
+
"∀", x, Quantifier(
|
|
491
|
+
"∀", y, Or(R(x, y), Or(Atom("=", (x, y)), R(y, x)))))
|
|
492
|
+
theory: List[Node] = [irreflexive, transitive, total]
|
|
493
|
+
|
|
494
|
+
if target_size >= 2:
|
|
495
|
+
chain_vars = [Variable(f"e{i}") for i in range(target_size)]
|
|
496
|
+
links = [R(chain_vars[i], chain_vars[i + 1]) for i in range(target_size - 1)]
|
|
497
|
+
chain_body: Node = reduce(And, links)
|
|
498
|
+
chain_formula = chain_body
|
|
499
|
+
for v in reversed(chain_vars):
|
|
500
|
+
chain_formula = Quantifier("∃", v, chain_formula)
|
|
501
|
+
theory.append(chain_formula)
|
|
502
|
+
theory_t = tuple(theory)
|
|
503
|
+
|
|
504
|
+
for k in range(1, target_size + 1):
|
|
505
|
+
if not modelfinder.is_size_exhaustive(theory_t, k, max_candidates=max_candidates):
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"generate_theory_with_model_size: cannot certify a minimal "
|
|
508
|
+
f"model size of {target_size} for relation {relation!r}: "
|
|
509
|
+
f"domain size {k}'s interpretation space exceeds "
|
|
510
|
+
f"max_candidates={max_candidates} and would be SKIPPED, not "
|
|
511
|
+
"exhaustively searched or refuted, by find_model -- raise "
|
|
512
|
+
"max_candidates or lower target_size."
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
witness = modelfinder.find_model(theory_t, max_size=target_size,
|
|
516
|
+
max_candidates=max_candidates)
|
|
517
|
+
if witness is None or len(witness.domain) != target_size:
|
|
518
|
+
raise AssertionError( # pragma: no cover -- formally guaranteed by construction
|
|
519
|
+
f"internal: the strict-order+chain theory for relation={relation!r} "
|
|
520
|
+
f"has no model of exactly size {target_size}, despite every size "
|
|
521
|
+
f"1..{target_size} being confirmed exhaustively searched."
|
|
522
|
+
)
|
|
523
|
+
if target_size > 1:
|
|
524
|
+
smaller = modelfinder.find_model(theory_t, max_size=target_size - 1,
|
|
525
|
+
max_candidates=max_candidates)
|
|
526
|
+
if smaller is not None:
|
|
527
|
+
raise AssertionError( # pragma: no cover -- formally guaranteed minimal
|
|
528
|
+
f"internal: a smaller model of size {len(smaller.domain)} exists "
|
|
529
|
+
f"for relation={relation!r}, despite target_size={target_size}."
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
return ModelSizeExercise(theory=theory_t, target_size=target_size,
|
|
533
|
+
relation=relation, witness=witness, seed=seed)
|