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.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. 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)