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,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