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,657 @@
1
+ """Second-order finite-model semantics: Tarskian satisfaction with ∀P / ∃P.
2
+
3
+ This evaluator extends classical Tarskian satisfaction (see
4
+ :mod:`semantics.tarski`) with second-order quantification over PREDICATE
5
+ variables: ``∀P φ`` and ``∃P φ``, where ``P`` ranges over every relation of its
6
+ arity on a FINITE domain. It reuses :class:`semantics.tarski.Structure` and
7
+ :func:`semantics.tarski.term_value` unchanged; only satisfaction is re-defined,
8
+ so that a bound predicate variable can be interpreted by a transient
9
+ ``pred_binding`` rather than by the structure's fixed predicate tables.
10
+
11
+ A ``pred_binding`` maps a bound predicate-variable name → its current
12
+ interpretation: a set of argument tuples (a relation on the domain). It is the
13
+ predicate-level analogue of the first-order variable ``assignment``. Both are
14
+ threaded immutably — extended into a fresh copy for each bound symbol, never
15
+ mutated in place.
16
+
17
+ Second-order quantification is interpreted by brute-force enumeration over the
18
+ powerset of ``domain ** arity`` (every relation of the right arity). This is
19
+ ``2 ** (n ** k)`` relations for a domain of size ``n`` and arity ``k`` — doubly
20
+ exponential, and intended ONLY for very small finite models (a handful of
21
+ elements, arity ≤ 2). Arity 0 is the propositional/Boolean case: the two
22
+ "relations" are ``frozenset()`` (false) and ``frozenset({()})`` (true), so
23
+ ``∀P``/``∃P`` over an arity-0 ``P`` quantifies over ``P``'s truth value.
24
+
25
+ Scope of the second order here: this is second-order PREDICATE (relation)
26
+ quantification only. Quantification over functions, over relations-of-relations
27
+ (third order and up), and a full higher-order type system are OUT OF SCOPE; the
28
+ lambda layer (:class:`fol.nodes.Lambda` / ``Application``) already supplies
29
+ higher-order TERMS and is intentionally rejected by this evaluator — beta-reduce
30
+ and lambda-eliminate first. Łukasiewicz (fuzzy) and modal nodes are likewise
31
+ rejected: use the fuzzy / Kripke evaluators.
32
+
33
+ A free OBJECT variable of a formula handed to a search function (:func:`so_find_model`,
34
+ :func:`so_find_countermodel`, :func:`so_is_satisfiable_finite`, :func:`so_is_valid_finite`)
35
+ is a PARAMETER: one unknown element, a constant of its own name that every candidate
36
+ structure interprets (see :func:`_so_sentence`). It is never closed universally, so
37
+ ``P(x) ∧ ¬P(y)`` is satisfiable, and the structure a search returns reports the element as
38
+ ``constants['x']``. The evaluators :func:`satisfies_so` and :func:`holds` take the
39
+ assignment from the caller and close nothing.
40
+
41
+ The four search functions answer for every domain size ``1 .. max_size`` or they raise. A
42
+ size whose number of candidate interpretations of the free symbols exceeds ``max_candidates``
43
+ is NOT skipped: a search that has found no structure at the smaller sizes raises
44
+ :class:`CandidateBoundExceeded` on reaching it (the size, the number, and the two ways out:
45
+ raise ``max_candidates`` or lower ``max_size``), because "valid" or "no model" for
46
+ ``max_size`` would otherwise be said about sizes nobody looked at. A structure found at a
47
+ smaller size is returned as before. A many-sorted formula is searched by the one-universe
48
+ reading of :mod:`~unicode_logic_kit.semantics.modelfinder`: one domain, each sort a non-empty
49
+ subset of it, ``c:S`` an element of ``S``, a sort and the unary predicate of its name one
50
+ symbol; the second-order quantifiers range over every relation on the whole domain. A
51
+ predicate name is a free symbol of the structure wherever no second-order quantifier of that
52
+ name encloses it, so ``¬P(a) ∧ ∃P P(a)`` is about the structure's own ``P`` in its first
53
+ conjunct and a bound one in its second.
54
+ """
55
+
56
+ from itertools import product
57
+ from typing import Any, FrozenSet, Iterable, Mapping, Optional, Tuple
58
+
59
+ from ..fol._free_parameters import parameterize
60
+ from ..fol.nodes import (
61
+ Node,
62
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
63
+ SortedQuantifier,
64
+ SecondOrderQuantifier,
65
+ )
66
+ from .tarski import (
67
+ Structure, term_value, _atom_value, _extend,
68
+ _FORALL, _EXISTS, _FUZZY_TYPES, _LAMBDA_TYPES, _refuse_cardinality_as_individual,
69
+ )
70
+
71
+ # A relation interpretation for a bound predicate variable: a (frozen)set of
72
+ # argument tuples of domain individuals. Mapping name -> relation.
73
+ Relation = FrozenSet[Tuple[Any, ...]]
74
+ PredBinding = Mapping[str, Relation]
75
+
76
+ # Safety cap for second-order enumeration. A ∀P/∃P over an arity-k predicate on
77
+ # an n-element domain ranges over 2 ** (n ** k) relations. Past this many the
78
+ # enumeration cannot finish in practice, so it raises a clear error instead of
79
+ # hanging. Raise this module attribute if you really mean to enumerate more.
80
+ MAX_RELATIONS = 1 << 22 # ~4.2 million
81
+
82
+
83
+ def satisfies_so(
84
+ formula: Node,
85
+ structure: Structure,
86
+ assignment: Optional[Mapping[str, Any]] = None,
87
+ pred_binding: Optional[PredBinding] = None,
88
+ ) -> bool:
89
+ """Return whether ``structure`` satisfies ``formula`` (second-order Tarski).
90
+
91
+ ``assignment`` maps object-variable names to individuals (as in
92
+ :func:`semantics.tarski.satisfies`); ``pred_binding`` maps a *bound*
93
+ predicate-variable name to its current relation (a set of argument tuples).
94
+ Both default to empty and are threaded immutably.
95
+
96
+ Recursion:
97
+
98
+ - **Atom** ``A(t1..tk)``: if ``A`` is currently bound (``A in pred_binding``),
99
+ it is true iff the tuple of evaluated term values is in ``pred_binding[A]``
100
+ (an arity-0 bound ``A`` is true iff ``() in pred_binding[A]``). Otherwise
101
+ satisfaction falls back to the structure exactly as
102
+ :func:`semantics.tarski.satisfies` does — including ``=`` as identity and
103
+ ``≠`` as non-identity.
104
+ - **Not/And/Or/Xor/Implies/Iff**: the classical truth tables.
105
+ - **Quantifier / SortedQuantifier** (object-level): range over the domain
106
+ (or the named sort), threading the same ``pred_binding``.
107
+ - **SecondOrderQuantifier** ``∀P/k φ`` / ``∃P/k φ``: range ``P`` over every
108
+ relation ``R ⊆ domain ** k`` (the powerset of all ``k``-tuples). ``∀``
109
+ holds iff ``φ`` holds for all such ``R``; ``∃`` iff for some. See the
110
+ module docstring for the ``2 ** (n ** k)`` complexity.
111
+
112
+ Raises:
113
+ ValueError: on a Łukasiewicz node (use the fuzzy evaluator), an unknown
114
+ quantifier type / node type, or when a ``∀P`` / ``∃P`` would enumerate
115
+ more than :data:`MAX_RELATIONS` relations (a clear error instead of a
116
+ hang — see the module docstring for the ``2 ** (n ** k)`` cost).
117
+ NotImplementedError: on a lambda or modal node — these are out of scope
118
+ for second-order predicate semantics (lambda: beta-reduce and
119
+ lambda-eliminate first).
120
+ """
121
+ if assignment is None:
122
+ assignment = {}
123
+ if pred_binding is None:
124
+ pred_binding = {}
125
+
126
+ if isinstance(formula, Atom):
127
+ return _so_atom_value(formula, structure, assignment, pred_binding)
128
+
129
+ if isinstance(formula, Not):
130
+ return not satisfies_so(formula.formula, structure, assignment, pred_binding)
131
+
132
+ if isinstance(formula, And):
133
+ return (satisfies_so(formula.left, structure, assignment, pred_binding)
134
+ and satisfies_so(formula.right, structure, assignment, pred_binding))
135
+
136
+ if isinstance(formula, Or):
137
+ return (satisfies_so(formula.left, structure, assignment, pred_binding)
138
+ or satisfies_so(formula.right, structure, assignment, pred_binding))
139
+
140
+ if isinstance(formula, Xor):
141
+ return (satisfies_so(formula.left, structure, assignment, pred_binding)
142
+ != satisfies_so(formula.right, structure, assignment, pred_binding))
143
+
144
+ if isinstance(formula, Implies):
145
+ return ((not satisfies_so(formula.left, structure, assignment, pred_binding))
146
+ or satisfies_so(formula.right, structure, assignment, pred_binding))
147
+
148
+ if isinstance(formula, Iff):
149
+ return (satisfies_so(formula.left, structure, assignment, pred_binding)
150
+ == satisfies_so(formula.right, structure, assignment, pred_binding))
151
+
152
+ if isinstance(formula, Quantifier):
153
+ return _eval_object_quantifier(
154
+ formula.type, formula.variable.name, structure.domain,
155
+ formula.formula, structure, assignment, pred_binding,
156
+ )
157
+
158
+ if isinstance(formula, SortedQuantifier):
159
+ universe = structure.sort_universe(formula.sort)
160
+ return _eval_object_quantifier(
161
+ formula.type, formula.variable.name, universe,
162
+ formula.formula, structure, assignment, pred_binding,
163
+ )
164
+
165
+ if isinstance(formula, SecondOrderQuantifier):
166
+ return _eval_second_order_quantifier(
167
+ formula, structure, assignment, pred_binding,
168
+ )
169
+
170
+ if isinstance(formula, _FUZZY_TYPES):
171
+ raise ValueError(
172
+ f"Cannot evaluate Łukasiewicz node {type(formula).__name__} with the "
173
+ "two-valued second-order evaluator; use the fuzzy evaluator instead."
174
+ )
175
+
176
+ if isinstance(formula, _LAMBDA_TYPES):
177
+ raise NotImplementedError(
178
+ f"Lambda node {type(formula).__name__} is out of scope for the "
179
+ "second-order evaluator (it handles second-order PREDICATE "
180
+ "quantification, not higher-order terms); beta-reduce and "
181
+ "lambda-eliminate the formula first."
182
+ )
183
+
184
+ # Modal nodes (Box / Diamond) live in the modal AST and have no class here;
185
+ # they fall through to this generic rejection alongside any other unknown
186
+ # node type. Modal formulas need the Kripke evaluator.
187
+ if type(formula).__name__ in ("Box", "Diamond"):
188
+ raise NotImplementedError(
189
+ f"Modal node {type(formula).__name__} is out of scope for the "
190
+ "second-order evaluator; use the Kripke (modal) evaluator instead."
191
+ )
192
+
193
+ raise ValueError(
194
+ f"satisfies_so: unsupported node type {type(formula).__name__}."
195
+ )
196
+
197
+
198
+ def _so_atom_value(
199
+ atom: Atom,
200
+ structure: Structure,
201
+ assignment: Mapping[str, Any],
202
+ pred_binding: PredBinding,
203
+ ) -> bool:
204
+ """Truth value of an atom, consulting ``pred_binding`` for bound predicates.
205
+
206
+ If the atom's predicate name is currently bound to a relation, the atom is
207
+ true iff the tuple of evaluated argument values is in that relation. (For an
208
+ arity-0 bound predicate the relevant tuple is the empty tuple ``()``.)
209
+ Otherwise satisfaction is delegated to the first-order
210
+ :func:`semantics.tarski._atom_value`, which handles ``=`` / ``≠`` and the
211
+ structure's predicate tables. The ``=`` / ``≠`` builtins are never treated
212
+ as bindable predicate variables.
213
+ """
214
+ if atom.predicate in pred_binding and atom.predicate not in ("=", "≠"):
215
+ relation = pred_binding[atom.predicate]
216
+ values = tuple(
217
+ term_value(a, structure, assignment) for a in atom.args
218
+ )
219
+ return values in relation
220
+ return _atom_value(atom, structure, assignment)
221
+
222
+
223
+ def _eval_object_quantifier(
224
+ qtype: str,
225
+ var_name: str,
226
+ universe: Iterable[Any],
227
+ body: Node,
228
+ structure: Structure,
229
+ assignment: Mapping[str, Any],
230
+ pred_binding: PredBinding,
231
+ ) -> bool:
232
+ """Evaluate an object-level ∀/∃ over a universe, threading ``pred_binding``.
233
+
234
+ Mirrors :func:`semantics.tarski._eval_quantifier` but recurses through
235
+ :func:`satisfies_so` so the predicate binding survives object quantifiers.
236
+ """
237
+ if qtype in _FORALL:
238
+ return all(
239
+ satisfies_so(body, structure, _extend(assignment, var_name, d), pred_binding)
240
+ for d in universe
241
+ )
242
+ if qtype in _EXISTS:
243
+ return any(
244
+ satisfies_so(body, structure, _extend(assignment, var_name, d), pred_binding)
245
+ for d in universe
246
+ )
247
+ raise ValueError(f"Unknown quantifier type: {qtype!r}")
248
+
249
+
250
+ def _all_relations(domain: Tuple[Any, ...], arity: int) -> Iterable[Relation]:
251
+ """Yield every relation R ⊆ domain ** arity (the powerset of all arity-tuples).
252
+
253
+ The base set is all ``arity``-tuples of domain elements (``len ==
254
+ n ** arity``); a relation is any subset of it, so there are ``2 ** (n **
255
+ arity)`` of them. For ``arity == 0`` the base set is the single empty tuple
256
+ ``{()}``, giving exactly two relations — ``frozenset()`` (Boolean false) and
257
+ ``frozenset({()})`` (Boolean true).
258
+
259
+ Each subset is yielded as a ``frozenset`` so it is hashable and immutable.
260
+ """
261
+ base = list(product(domain, repeat=arity))
262
+ # Enumerate subsets via the bitmask 0 .. 2**len(base) - 1.
263
+ for mask in range(1 << len(base)):
264
+ subset = frozenset(
265
+ base[i] for i in range(len(base)) if (mask >> i) & 1
266
+ )
267
+ yield subset
268
+
269
+
270
+ def _eval_second_order_quantifier(
271
+ node: SecondOrderQuantifier,
272
+ structure: Structure,
273
+ assignment: Mapping[str, Any],
274
+ pred_binding: PredBinding,
275
+ ) -> bool:
276
+ """Evaluate ∀P/k or ∃P/k by enumerating every relation R ⊆ domain ** k.
277
+
278
+ The bound predicate name is interpreted, in turn, by each candidate relation
279
+ added to a *copy* of ``pred_binding`` (shadowing any outer binding of the
280
+ same name). ``∀`` holds iff the body holds under every candidate; ``∃`` iff
281
+ under some. See the module docstring for the doubly-exponential cost.
282
+
283
+ Raises ValueError if the 2 ** (n ** k) relation count exceeds
284
+ :data:`MAX_RELATIONS`, rather than enumerating a hopelessly large space.
285
+ """
286
+ num_tuples = len(structure.domain) ** node.arity
287
+ num_relations = 1 << num_tuples # 2 ** (n ** k)
288
+ if num_relations > MAX_RELATIONS:
289
+ raise ValueError(
290
+ f"Second-order quantifier {node.type}{node.predicate}/{node.arity} "
291
+ f"over a {len(structure.domain)}-element domain would enumerate "
292
+ f"2 ** ({len(structure.domain)} ** {node.arity}) = 2 ** {num_tuples} "
293
+ f"relations, above MAX_RELATIONS = {MAX_RELATIONS}. Shrink the domain "
294
+ "or the arity (or raise secondorder.MAX_RELATIONS)."
295
+ )
296
+ relations = _all_relations(structure.domain, node.arity)
297
+ if node.type in _FORALL:
298
+ return all(
299
+ satisfies_so(
300
+ node.formula, structure, assignment,
301
+ _extend(pred_binding, node.predicate, relation),
302
+ )
303
+ for relation in relations
304
+ )
305
+ if node.type in _EXISTS:
306
+ return any(
307
+ satisfies_so(
308
+ node.formula, structure, assignment,
309
+ _extend(pred_binding, node.predicate, relation),
310
+ )
311
+ for relation in relations
312
+ )
313
+ raise ValueError(
314
+ f"Unknown second-order quantifier type: {node.type!r}"
315
+ )
316
+
317
+
318
+ def holds(formula: Node, structure: Structure, fast: bool = False) -> bool:
319
+ """Convenience: ``satisfies_so(formula, structure, {}, {})`` for a sentence.
320
+
321
+ Reads as "structure satisfies the (closed) second-order formula": the empty
322
+ object assignment and empty predicate binding are appropriate when the
323
+ formula has no free object or predicate variables.
324
+
325
+ Args:
326
+ fast: opt-in (default ``False``, which keeps every existing call's
327
+ behaviour byte-identical); when ``True``, checks via
328
+ :func:`~unicode_logic_kit.semantics.asp_models.asp_holds_so`
329
+ instead — ASP-grounded (clingo propagation prunes the search
330
+ instead of this module's brute-force ``2 ** (n ** k)``
331
+ enumeration; see that function's docstring) but restricted to
332
+ second-order sentences whose ``SecondOrderQuantifier``
333
+ occurrences form a single, same-polarity block (roadmap C24). A
334
+ ``formula`` outside that fragment raises ``ValueError`` naming
335
+ the offending construct (never a silent fallback to the
336
+ brute-force reading above); a missing ``clingo`` install (the
337
+ optional ``asp`` extra) raises too.
338
+ """
339
+ if fast:
340
+ from .asp_models import asp_holds_so
341
+ return asp_holds_so(formula, structure)
342
+ return satisfies_so(formula, structure, {}, {})
343
+
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # Bounded second-order validity / (counter)model search
347
+ # ---------------------------------------------------------------------------
348
+ #
349
+ # Second-order logic has no complete proof system and SO validity is not even
350
+ # semi-decidable, so this is a *bounded finite-model* search (the SO analogue of
351
+ # semantics.modelfinder): it enumerates finite structures interpreting the FREE
352
+ # symbols — the SO-quantified predicates are NOT interpreted by the structure, the
353
+ # satisfies_so evaluator ranges them over every relation — and evaluates the SO
354
+ # sentence in each. A found model/counter-model is genuine; "none up to size N" is
355
+ # bounded evidence, not a proof. The free-symbol enumeration (_so_structures below)
356
+ # uses modelfinder's LNH symmetry-breaking generator (roadmap C23), so a
357
+ # constant-heavy free signature is searched without the k! relabeling redundancy —
358
+ # see _so_structures's docstring.
359
+
360
+
361
+ def _so_bound_predicate_names(formula: Node) -> set:
362
+ """Names bound by a second-order quantifier anywhere in ``formula``."""
363
+ return {n.predicate for n in formula.walk()
364
+ if isinstance(n, SecondOrderQuantifier)}
365
+
366
+
367
+ def _so_free_predicates(node: Node, bound: FrozenSet[str] = frozenset()) -> set:
368
+ """The ``(name, arity)`` of every atom of ``node`` whose predicate no enclosing
369
+ second-order quantifier binds: the predicates a structure has to interpret.
370
+
371
+ A name that a quantifier binds is a bound variable only inside that quantifier's scope,
372
+ so ``P(a) ∧ ∃P ¬P(a)`` has the free predicate ``P`` (its first conjunct) and a bound one.
373
+ """
374
+ if isinstance(node, SecondOrderQuantifier):
375
+ return _so_free_predicates(node.formula, bound | {node.predicate})
376
+ found: set = set()
377
+ if isinstance(node, Atom) and node.predicate not in bound:
378
+ found.add((node.predicate, len(node.args)))
379
+ for child in node._child_nodes():
380
+ found |= _so_free_predicates(child, bound)
381
+ return found
382
+
383
+
384
+ def _so_signature(sentence: Node):
385
+ """The structure signature of ``sentence`` minus its SO-bound predicates.
386
+
387
+ A predicate is left out only where a second-order quantifier binds it: an occurrence of the
388
+ same name outside every such quantifier is a free predicate, which a structure interprets.
389
+
390
+ Raises:
391
+ NotImplementedError: a bound predicate variable has the name of a sort of ``sentence``
392
+ (a sort and the unary predicate of its name are one symbol, and a quantifier cannot
393
+ rebind half of it).
394
+ """
395
+ from .modelfinder import _Signature
396
+ bound = _so_bound_predicate_names(sentence)
397
+ sig = _Signature()
398
+ sig.scan(sentence)
399
+ clash = sorted(bound & sig.sorts)
400
+ if clash:
401
+ raise NotImplementedError(
402
+ f"semantics.secondorder: the predicate variable {clash[0]!r} that a second-order "
403
+ f"quantifier binds has the name of a sort of the formula. A sort and the unary "
404
+ f"predicate of its name are ONE symbol, so a quantifier over it would rebind "
405
+ f"the predicate but not the sort; rename the bound predicate variable.")
406
+ free = _so_free_predicates(sentence)
407
+ sig.predicates = {(name, ar) for (name, ar) in sig.predicates if (name, ar) in free}
408
+ return sig
409
+
410
+
411
+ def _describe_symbols(sig) -> str:
412
+ """The symbols of ``sig`` as the user wrote them (``c``, ``f/1``, ``T/3``, sort ``S``)."""
413
+ parts = sorted(sig.constants)
414
+ parts += [f"{name}/{arity}" for name, arity in sorted(sig.functions)]
415
+ parts += [f"{name}/{arity}" for name, arity in sorted(sig.predicates)]
416
+ parts += [f"sort {name}" for name in sorted(sig.sorts)]
417
+ return ", ".join(parts) if parts else "none"
418
+
419
+
420
+ class CandidateBoundExceeded(ValueError):
421
+ """A bounded second-order search reached a domain size it does not enumerate.
422
+
423
+ :func:`so_find_model`, :func:`so_find_countermodel`, :func:`so_is_satisfiable_finite` and
424
+ :func:`so_is_valid_finite` answer "no model" (or "valid") for the domain sizes
425
+ ``1 .. max_size``, which is only true if every one of them was searched. A size whose
426
+ number of candidate interpretations of the free symbols exceeds ``max_candidates`` is not
427
+ searched, and an answer that leaves it out would be an answer about structures the search
428
+ never looked at; so the search raises this error when it reaches such a size without
429
+ having found a structure. A structure found at a smaller size is returned as before, since
430
+ it is a witness whatever the larger sizes hold.
431
+
432
+ The instance carries ``size`` (the first size that was not searched), ``candidates`` (how
433
+ many interpretations of the free symbols it has: an upper bound for a many-sorted formula)
434
+ and ``max_candidates`` (the bound that was exceeded). The message names the two ways out:
435
+ raise ``max_candidates``, or lower ``max_size`` below ``size``.
436
+ """
437
+
438
+ size: int = 0
439
+ candidates: int = 0
440
+ max_candidates: int = 0
441
+
442
+
443
+ def _bound_exceeded(where: str, sig, size: int, candidates: int,
444
+ max_candidates: int) -> CandidateBoundExceeded:
445
+ """The :class:`CandidateBoundExceeded` that the search ``where`` raises at ``size``."""
446
+ way_out = (f"lower max_size to {size - 1}, which asks only about the sizes that were searched"
447
+ if size > 1 else "there is no smaller size to ask about")
448
+ error = CandidateBoundExceeded(
449
+ f"{where}: domain size {size} has {'at most ' if sig.sorts else ''}{candidates} "
450
+ f"candidate interpretations of the free symbols ({_describe_symbols(sig)}), more than "
451
+ f"max_candidates = {max_candidates}, so that size is not searched and the search gives "
452
+ f"no answer for it (leaving it out would report 'no model' or 'valid' for structures "
453
+ f"never looked at). Either raise max_candidates to at least {candidates}, or {way_out}.")
454
+ error.size = size
455
+ error.candidates = candidates
456
+ error.max_candidates = max_candidates
457
+ return error
458
+
459
+
460
+ def _so_structures(sentence: Node, max_size: int, max_candidates: int,
461
+ where: str = "semantics.secondorder"):
462
+ """Yield every candidate :class:`Structure` over domains ``1 .. max_size``.
463
+
464
+ A size is searched completely or the generator raises :class:`CandidateBoundExceeded` on
465
+ reaching it: it never skips a size and goes on to the next one. What it has yielded before
466
+ the raise is complete for the smaller sizes.
467
+
468
+ Enumerates the FREE-symbol part with :func:`modelfinder._canonical_interpretations`
469
+ (roadmap C23) instead of the plain :func:`modelfinder._interpretations`, so
470
+ ``so_find_model``/``so_find_countermodel`` inherit the LNH symmetry-breaking
471
+ reduction on constant assignments for free — no separate implementation needed
472
+ here (functions and predicates, including the SO-quantified ones
473
+ ``satisfies_so`` ranges over, stay exhaustive exactly as before; see that
474
+ generator's docstring for the full soundness argument and why functions are
475
+ NOT LNH-reduced). The number of candidates of a size is the EXACT count the canonical
476
+ generator yields (:func:`modelfinder._canonical_candidate_count`), so a size is refused
477
+ only when it really has more than ``max_candidates`` structures.
478
+
479
+ A formula with sorts is enumerated with :func:`modelfinder._sorted_interpretations`, which
480
+ is the one-universe reading of the model finder: ONE domain, each sort a non-empty subset of
481
+ it (overlapping freely), a sorted constant ``c:S`` an element of ``S``, a sort and the unary
482
+ predicate of its name one symbol, every other symbol over the whole domain. That search has
483
+ no symmetry breaking, and its candidate count is the upper bound
484
+ :func:`modelfinder._candidate_count`.
485
+
486
+ ``where`` names the public function the caller is, for the message of the raise.
487
+ """
488
+ from .modelfinder import (
489
+ _canonical_candidate_count, _canonical_interpretations, _candidate_count,
490
+ _sorted_interpretations,
491
+ )
492
+ sig = _so_signature(sentence)
493
+ for k in range(1, max_size + 1):
494
+ candidates = _candidate_count(sig, k) if sig.sorts else _canonical_candidate_count(sig, k)
495
+ if candidates > max_candidates:
496
+ raise _bound_exceeded(where, sig, k, candidates, max_candidates)
497
+ domain = tuple(range(k))
498
+ if sig.sorts:
499
+ for constants, functions, predicates, sorts in _sorted_interpretations(sig, domain):
500
+ yield Structure(domain, constants=constants, functions=functions,
501
+ predicates=predicates, sorts=sorts)
502
+ else:
503
+ for constants, functions, predicates in _canonical_interpretations(sig, domain):
504
+ yield Structure(domain, constants=constants,
505
+ functions=functions, predicates=predicates)
506
+
507
+
508
+ def _so_sentence(formula: Node) -> Node:
509
+ """``formula`` with every free object variable read as a PARAMETER.
510
+
511
+ A free variable is one unknown element (the assignment-wise reading of the other
512
+ finite-model routes: a structure AND an assignment satisfy ``φ(x)`` iff the
513
+ structure satisfies ``φ(c)`` with ``x`` ↦ ``c``): it is replaced by a constant of its
514
+ own name (:func:`~unicode_logic_kit.fol._free_parameters.parameterize`), which every
515
+ structure of the search interprets and which a returned structure reports as
516
+ ``constants['x']``. The formula is never closed universally, so ``P(x) ∧ ¬P(y)`` has a
517
+ model (``x`` and ``y`` are two elements), and validity is still validity under every
518
+ assignment.
519
+
520
+ Raises:
521
+ NotImplementedError: a free variable has the spelling of a constant of ``formula``
522
+ (a structure holds one entry per name), or a cardinality term ``|{v : φ}|`` is not
523
+ an operand of a comparison with a number (a natural number is no element of the
524
+ domain; see :func:`~unicode_logic_kit.semantics.tarski.satisfies`).
525
+ """
526
+ sentence = parameterize([formula], after_variables=True)[0][0]
527
+ _refuse_cardinality_as_individual([sentence], "semantics.secondorder")
528
+ return sentence
529
+
530
+
531
+ def _first_structure(formula: Node, max_size: int, max_candidates: int, fast: bool,
532
+ satisfying: bool, where: str) -> Optional[Structure]:
533
+ """The first candidate structure, smallest domain first, in which ``formula`` is true
534
+ (``satisfying=True``) or false (``satisfying=False``); ``None`` when every size up to
535
+ ``max_size`` was searched and none is such a structure.
536
+
537
+ Raises:
538
+ NotImplementedError: see :func:`_so_sentence`, and :func:`_so_signature`.
539
+ ~unicode_logic_kit.semantics.secondorder.CandidateBoundExceeded:
540
+ a size up to ``max_size`` has more candidates than ``max_candidates`` and no
541
+ structure was found at a smaller size.
542
+ """
543
+ sentence = _so_sentence(formula)
544
+ for structure in _so_structures(sentence, max_size, max_candidates, where):
545
+ if bool(holds(sentence, structure, fast=fast)) == satisfying:
546
+ return structure
547
+ return None
548
+
549
+
550
+ def so_find_model(formula: Node, max_size: int = 3,
551
+ max_candidates: int = MAX_RELATIONS, fast: bool = False) -> Optional[Structure]:
552
+ """Return a finite structure in which the SO ``formula`` holds, or None (bounded).
553
+
554
+ ``None`` means that every domain size ``1 .. max_size`` was searched and none has a model.
555
+ A size with more than ``max_candidates`` candidate interpretations of the free symbols is
556
+ not skipped: the search raises instead (see below). A free object variable is a PARAMETER
557
+ (:func:`_so_sentence`): the structure holds when some assignment satisfies ``formula``, and
558
+ it reports that assignment as the constant of the variable's own name. A formula with
559
+ sorts is read by the one-universe reading of
560
+ :mod:`~unicode_logic_kit.semantics.modelfinder`: the returned structure carries ``sorts``,
561
+ each a non-empty subset of the one domain, and ``∀x:S`` / ``∃x:S`` range over it.
562
+
563
+ ``fast`` is passed straight through to :func:`holds` for each candidate
564
+ structure — see that function's ``fast`` parameter (opt-in, default
565
+ ``False`` keeps this byte-identical to before). With ``fast=True`` a sorted quantifier or
566
+ sorted constant INSIDE the scope of a second-order quantifier is refused by a
567
+ ``ValueError`` that names it (the ASP encoding reads unsorted formulas only); outside
568
+ every second-order quantifier a sort is evaluated as with ``fast=False``.
569
+
570
+ Raises:
571
+ NotImplementedError: a free variable has the spelling of a constant of ``formula``, a
572
+ cardinality term is used as an individual (see :func:`_so_sentence`), or a bound
573
+ predicate variable has the name of one of the sorts of ``formula``.
574
+ ~unicode_logic_kit.semantics.secondorder.CandidateBoundExceeded:
575
+ a domain size up to ``max_size`` has more than ``max_candidates`` candidate
576
+ interpretations and no model was found at a smaller size. Raise ``max_candidates`` or
577
+ lower ``max_size``; the message gives the size and the number.
578
+ """
579
+ return _first_structure(formula, max_size, max_candidates, fast, True, "so_find_model")
580
+
581
+
582
+ def so_find_countermodel(formula: Node, max_size: int = 3,
583
+ max_candidates: int = MAX_RELATIONS, fast: bool = False) -> Optional[Structure]:
584
+ """Return a finite structure in which the SO ``formula`` FAILS, or None (bounded).
585
+
586
+ A returned structure witnesses that ``formula`` is not second-order valid. A free
587
+ object variable is a PARAMETER (:func:`_so_sentence`): the structure reports the
588
+ assignment that falsifies ``formula`` as the constant of the variable's own name.
589
+ For a formula on its own this is the same verdict as the universal closure gives
590
+ (``φ`` holds under every assignment iff ``∀x φ`` holds); it is the reading of the
591
+ satisfiability routes, so a returned structure is a model of ``¬formula`` there too.
592
+
593
+ ``None`` means that every domain size ``1 .. max_size`` was searched and none has a
594
+ countermodel: a size with more than ``max_candidates`` candidate interpretations of the
595
+ free symbols is not skipped, the search raises instead. A formula with sorts is read by the
596
+ one-universe reading of :mod:`~unicode_logic_kit.semantics.modelfinder` (see
597
+ :func:`so_find_model`).
598
+
599
+ ``fast`` is passed straight through to :func:`holds` for each candidate
600
+ structure — see that function's ``fast`` parameter (opt-in, default
601
+ ``False`` keeps this byte-identical to before).
602
+
603
+ Raises:
604
+ NotImplementedError: see :func:`so_find_model`.
605
+ ~unicode_logic_kit.semantics.secondorder.CandidateBoundExceeded:
606
+ a domain size up to ``max_size`` has more than ``max_candidates`` candidate
607
+ interpretations and no countermodel was found at a smaller size.
608
+ """
609
+ return _first_structure(formula, max_size, max_candidates, fast, False, "so_find_countermodel")
610
+
611
+
612
+ def so_is_satisfiable_finite(formula: Node, max_size: int = 3,
613
+ max_candidates: int = MAX_RELATIONS, fast: bool = False) -> bool:
614
+ """True iff the SO ``formula`` has a finite model of size ≤ ``max_size`` (bounded).
615
+
616
+ ``False`` means that every size ``1 .. max_size`` was searched and has no model; a size
617
+ that cannot be searched within ``max_candidates`` raises (see :func:`so_find_model`).
618
+
619
+ ``fast``: see :func:`holds`'s parameter of the same name (opt-in, default
620
+ ``False`` keeps this byte-identical to before).
621
+
622
+ Raises:
623
+ NotImplementedError: see :func:`so_find_model`.
624
+ ~unicode_logic_kit.semantics.secondorder.CandidateBoundExceeded:
625
+ a domain size up to ``max_size`` has more than ``max_candidates`` candidate
626
+ interpretations and no model was found at a smaller size.
627
+ """
628
+ return _first_structure(formula, max_size, max_candidates, fast, True,
629
+ "so_is_satisfiable_finite") is not None
630
+
631
+
632
+ def so_is_valid_finite(formula: Node, max_size: int = 3,
633
+ max_candidates: int = MAX_RELATIONS, fast: bool = False) -> bool:
634
+ """True iff no finite counter-model of the SO ``formula`` is found up to ``max_size``.
635
+
636
+ Bounded and one-sided: ``True`` is strong evidence of second-order validity (not a
637
+ proof — SO validity is not semi-decidable) and means that EVERY domain size
638
+ ``1 .. max_size`` was searched; ``False`` is a genuine refutation, with
639
+ the witness available from :func:`so_find_countermodel`. A size with more than
640
+ ``max_candidates`` candidate interpretations of the free symbols is never left out to
641
+ answer ``True`` for the rest: the call raises instead.
642
+
643
+ ``fast``: see :func:`holds`'s parameter of the same name (opt-in, default
644
+ ``False`` keeps this byte-identical to before) — each candidate structure
645
+ is then checked via
646
+ :func:`~unicode_logic_kit.semantics.asp_models.asp_holds_so` instead of the
647
+ brute-force :func:`satisfies_so`, restricted to the single-block SO
648
+ fragment that function accepts.
649
+
650
+ Raises:
651
+ NotImplementedError: see :func:`so_find_model`.
652
+ ~unicode_logic_kit.semantics.secondorder.CandidateBoundExceeded:
653
+ a domain size up to ``max_size`` has more than ``max_candidates`` candidate
654
+ interpretations and no countermodel was found at a smaller size.
655
+ """
656
+ return _first_structure(formula, max_size, max_candidates, fast, False,
657
+ "so_is_valid_finite") is None