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,1200 @@
1
+ """Minimal models via ASP — clingo enumerates, the kit's own filter decides.
2
+
3
+ :func:`nonmonotonic.minimal_models <unicode_logic_kit.semantics.nonmonotonic.minimal_models>`
4
+ answers "which finite models are ≤-minimal" by brute force: enumerate every
5
+ interpretation of the signature over a domain, keep the ones that satisfy the
6
+ premises, then compare every survivor against every other survivor in its
7
+ fixed-part group. That comparison step is correct but the enumeration step is
8
+ not what an ASP solver is for — clingo already ships a search procedure that
9
+ prunes as it grounds instead of generating every candidate and checking it
10
+ afterward. This module keeps the (already correct, unmodified) comparison and
11
+ replaces only the enumeration: :func:`asp_minimal_models` grounds ``premises``
12
+ into an ASP program whose answer sets are *exactly* the models of a given
13
+ domain size, lets clingo enumerate those, and then hands the result to
14
+ nonmonotonic's own ``_circ_profile`` / ``_strictly_below`` / ``_fixed_key`` —
15
+ imported, not reimplemented — to pick out the minimal ones.
16
+
17
+ Why reuse rather than reimplement the filter
18
+ ---------------------------------------------
19
+ A second, independently-written "is this model minimal" check would need to
20
+ agree with the first one for this module to be trustworthy, and disagreement
21
+ between two independent implementations of the same non-trivial comparison
22
+ (componentwise subset-or-equal with at least one strict subset, grouped by a
23
+ shared fixed part) is exactly the kind of bug this kit's differential-testing
24
+ discipline exists to catch — better to make it structurally impossible by
25
+ sharing the code. Importing the private ``_circ_profile`` / ``_strictly_below``
26
+ / ``_fixed_key`` from :mod:`~unicode_logic_kit.semantics.nonmonotonic` (that
27
+ module is not modified here) means the two routes can only ever disagree about
28
+ *which models exist* (an enumeration bug), never about *which of them are
29
+ minimal* (a comparison bug) — the second class of disagreement is ruled out
30
+ by construction, not merely tested for.
31
+
32
+ Why not ``#minimize`` over the circumscribed predicates
33
+ ----------------------------------------------------------
34
+ clingo's own optimisation directive would find the models with the fewest
35
+ TRUE atoms among the circumscribed predicates — CARDINALITY-minimal models.
36
+ Circumscription wants SUBSET-minimal models: ``{a}`` is more minimal than
37
+ ``{a, b}`` even though both have "few" elements, and a set with 3 elements
38
+ distributed one way is not comparable by cardinality alone to a different
39
+ 3-element set. The two orders coincide only when every minimal set has the
40
+ same size, which is not true in general (see the disjunctive-fact example in
41
+ this module's own verification below: ``P(a) ∨ P(b)`` has minimal models
42
+ ``{a}`` and ``{b}``, both size 1, but a formula like ``P(a) ∨ (P(b) ∧ P(c))``
43
+ already breaks the coincidence). Substituting cardinality-minimality for
44
+ subset-minimality would be exactly the silent semantic swap this kit's other
45
+ non-monotonic-reasoning code refuses to make elsewhere, so it is refused here
46
+ too: this module enumerates ALL models and filters in Python with the real
47
+ predicate, never ``#minimize``.
48
+
49
+ ``size`` is a single domain size, not a bound
50
+ ------------------------------------------------
51
+ :func:`nonmonotonic.minimal_models <unicode_logic_kit.semantics.nonmonotonic.minimal_models>`
52
+ takes ``max_size`` and unions the minimal models found at every size from 1 up
53
+ to that bound (models at different sizes are never compared against each
54
+ other — see that function's own docstring). :func:`asp_minimal_models` here
55
+ takes a single ``size`` and grounds exactly once, at that size, because
56
+ grounding is the expensive step an ASP solver does per call and silently
57
+ regrounding ``size`` times to imitate ``max_size`` would hide that cost from
58
+ the caller. A caller who wants the union-over-sizes behaviour gets it by
59
+ calling this function in a loop and concatenating the results — one line at
60
+ the call site, versus a hidden cost inside every call otherwise. This is a
61
+ genuine, deliberate difference from ``minimal_models``'s parameter of a
62
+ similar name; the verification below accounts for it by comparing against
63
+ ``minimal_models(..., max_size=size)`` FILTERED to the structures whose own
64
+ domain has exactly ``size`` elements, which — because sizes are never compared
65
+ across each other in that function either — is provably the same set a
66
+ hypothetical single-size ``minimal_models`` would have returned.
67
+
68
+ The ASP encoding
69
+ -------------------
70
+ For a fixed domain size ``n``, ``dom(0..n-1).`` names the individuals; every
71
+ declared predicate, function, and constant gets a free ASP choice (a function
72
+ of arity ``k`` as the standard *total relation*: ``1 { f(x̄, v) : dom(v) } 1``
73
+ per input tuple ``x̄``, which enforces functionality — at most one result —
74
+ and totality — at least one — in the same rule the way
75
+ :mod:`~unicode_logic_kit.atp.finite_domain`'s design note also describes it).
76
+ Each (sub)formula then gets its own auxiliary ASP atom, defined by rules that
77
+ mirror its connective: ``And`` needs one rule (a conjunction of positive
78
+ literals is what a rule body already is), ``Or``/``Xor``/``Implies``/``Iff``
79
+ need two rules each (one for each way to satisfy them — plain multi-rule
80
+ disjunction, no aggregate), ``Not`` is default negation over its
81
+ already-fully-defined child (safe precisely because the child's rules are
82
+ grounded, and hence decided, for every combination of its free variables
83
+ before the parent ever refers to it — the formula tree is a DAG with no
84
+ cycles through negation, so this is ordinary stratified Datalog negation, not
85
+ anything exotic). ``∃x φ`` is a projection: a rule whose body mentions ``x``
86
+ but whose head does not, so grounding produces "true for some x" for free —
87
+ no aggregate needed. ``∀x φ`` is rewritten to ``¬∃x ¬φ`` before encoding
88
+ rather than given its own bespoke aggregate rule, so its correctness rides on
89
+ the already-tested ``Not``/``∃`` encodings instead of a third, separately-
90
+ fallible implementation of the same quantifier alternation. ``Count`` (the
91
+ one place real counting is needed) is the one aggregate this module emits:
92
+ ``#count{x : dom(x), φ(x)} <op> n``. Symbol names in the generated program
93
+ are never the FOL names themselves (``p0``, ``f0``, ``k0``, …, assigned by a
94
+ signature scan) — sidestepping both ASP's stricter identifier syntax
95
+ (FOL names may contain non-ASCII letters this kit deliberately allows
96
+ elsewhere) and any namespace collision between a predicate and a same-spelled
97
+ constant, which a flat ASP atom namespace would otherwise conflate.
98
+
99
+ Fragment supported here (deliberately narrower than
100
+ :func:`~unicode_logic_kit.atp.finite_domain.fragment_check`'s)
101
+ ------------------------------------------------------------------
102
+ ``Atom``, ``Not``, ``And``, ``Or``, ``Xor``, ``Implies``, ``Iff``,
103
+ ``Quantifier`` (unsorted ``∀``/``∃`` only), ``Count``, over
104
+ ``Variable``/``Constant``/``Number``/``Function`` terms — exactly the
105
+ fragment :mod:`~unicode_logic_kit.semantics.tarski` (via
106
+ :mod:`~unicode_logic_kit.semantics.modelfinder`) evaluates, because that
107
+ evaluator is this module's own verification oracle below: supporting a
108
+ construct here that the oracle cannot check would mean shipping an
109
+ un-cross-checked code path, which is what this whole module exists to avoid.
110
+ Two constructs :func:`~unicode_logic_kit.atp.finite_domain.fragment_check` DOES
111
+ admit are refused here for exactly that reason: ``Cardinality`` (the oracle's
112
+ :func:`~unicode_logic_kit.semantics.tarski.term_value` can evaluate it as a
113
+ term, but only inside an equality/order comparison whose VALUE is an
114
+ arithmetic integer, not a domain individual — plumbing that through this
115
+ module's term encoding, which only ever produces domain-individual variables,
116
+ is a distinct unit of work with its own risk of a subtly wrong aggregate,
117
+ and it is not needed for circumscription premises, which reason about
118
+ predicate extensions, not counts) and ``Contrast`` (the oracle does not
119
+ evaluate it at all — :func:`~unicode_logic_kit.semantics.tarski.satisfies` has
120
+ no case for it — so there is no oracle to verify against even if this module
121
+ encoded it). A sentence outside this fragment raises ``ValueError`` naming
122
+ the offending node type rather than silently mis-encoding it.
123
+
124
+ Public API: :func:`asp_minimal_models`, :func:`asp_find_model`.
125
+ """
126
+
127
+ from itertools import product
128
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
129
+
130
+ from ..fol.nodes import (
131
+ Node, Variable, Constant, Number, Function,
132
+ Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Count,
133
+ SecondOrderQuantifier,
134
+ )
135
+ from ..fol._fol_nodes import numeral_key
136
+ from ..fol._truth_constants import truth_value as _truth_value
137
+ from .tarski import Structure, _FORALL, _EXISTS, _ORDER_COMPARISONS, _ORDER_OPS, _is_number
138
+ from ..fol._free_parameters import parameterize
139
+ from .modelfinder import _Signature
140
+ from .nonmonotonic import _circ_profile, _strictly_below, _fixed_key
141
+
142
+ __all__ = ["asp_minimal_models", "asp_find_model", "asp_holds_so"]
143
+
144
+
145
+ # =============================================================================
146
+ # Fragment gate — see the module docstring's "Fragment supported here" section
147
+ # for what is admitted and, for the two admitted-elsewhere-but-not-here node
148
+ # types, exactly why.
149
+ # =============================================================================
150
+
151
+ _ALLOWED_FORMULA_TYPES = (Atom, Not, And, Or, Xor, Implies, Iff, Quantifier, Count)
152
+ _ALLOWED_TERM_TYPES = (Variable, Constant, Number, Function)
153
+
154
+
155
+ def _silent(code, message):
156
+ """clingo logger that drops its info/warning chatter about the GENERATED program
157
+ (e.g. "atom does not occur in any rule head" for an empty extension), the same
158
+ choice atp.clingo_backend makes; real errors still raise from clingo itself."""
159
+
160
+
161
+ def _check_fragment(sentences: Iterable[Node]) -> None:
162
+ """Raise ``ValueError`` at the first node outside this module's fragment.
163
+
164
+ Walks every sentence pre-order (so a disallowed node nested inside an
165
+ allowed one is still caught) and additionally rejects a ``Quantifier``
166
+ whose ``type`` is neither an unsorted ``∀``/``forall`` nor an unsorted
167
+ ``∃``/``exists`` spelling — the only two this module's encoder
168
+ recognises (the same spellings :mod:`~unicode_logic_kit.semantics.tarski`
169
+ recognises, imported from there rather than re-listed here so the two
170
+ can never drift apart).
171
+
172
+ Raises:
173
+ ValueError: naming the first offending node type (or quantifier
174
+ spelling) encountered, and — for a well-known excluded
175
+ construct (``Cardinality``, ``Contrast``, the sorted family,
176
+ …) — why it is excluded (see the module docstring).
177
+ """
178
+ for sentence in sentences:
179
+ for node in sentence.walk():
180
+ if isinstance(node, _ALLOWED_FORMULA_TYPES) or isinstance(node, _ALLOWED_TERM_TYPES):
181
+ if isinstance(node, Quantifier) and node.type not in _FORALL and node.type not in _EXISTS:
182
+ raise ValueError(
183
+ f"asp_models: unknown quantifier spelling {node.type!r} — "
184
+ f"only unsorted {_FORALL + _EXISTS} are encodable here."
185
+ )
186
+ continue
187
+ raise ValueError(
188
+ f"asp_models: {type(node).__name__} is not in the fragment this "
189
+ "module encodes (unsorted classical FOL — Atom/Not/And/Or/Xor/"
190
+ "Implies/Iff/Quantifier/Count over Variable/Constant/Number/"
191
+ "Function terms; see the module docstring's 'Fragment supported "
192
+ "here' section for why Cardinality/Contrast/the sorted and modal "
193
+ "families are excluded)."
194
+ )
195
+
196
+
197
+ # =============================================================================
198
+ # The ASP encoder
199
+ # =============================================================================
200
+
201
+ class _AspEncoder:
202
+ """Translates closed FOL sentences into a clingo program, one call at a time.
203
+
204
+ Every (sub)formula gets its own fresh auxiliary ASP atom (a Tseitin-style
205
+ encoding — see the module docstring's "The ASP encoding" section for the
206
+ per-connective rule shapes and why each is sound). State kept here is
207
+ purely bookkeeping for that translation: the accumulated rule TEXT, fresh
208
+ name counters, and the FOL-symbol-name -> ASP-identifier maps (so a FOL
209
+ name containing non-ASCII characters, or one that collides with a FOL
210
+ name in a *different* symbol class, never has to become a raw ASP
211
+ identifier — see the module docstring).
212
+ """
213
+
214
+ def __init__(self, size: int):
215
+ self.size = size
216
+ self.rules: List[str] = []
217
+ self._var_ctr = 0
218
+ self._head_ctr = 0
219
+ self.pred_asp: Dict[Tuple[str, int], str] = {}
220
+ self.func_asp: Dict[Tuple[str, int], str] = {}
221
+ self.const_asp: Dict[str, str] = {}
222
+
223
+ def _fresh_var(self) -> str:
224
+ self._var_ctr += 1
225
+ return f"V{self._var_ctr}"
226
+
227
+ def _fresh_head(self) -> str:
228
+ self._head_ctr += 1
229
+ return f"h{self._head_ctr}"
230
+
231
+ # -- setup --------------------------------------------------------------
232
+
233
+ def declare_signature(self, sig: "_Signature") -> None:
234
+ """Assign a fresh, ASCII, collision-free ASP identifier to every symbol.
235
+
236
+ Enumeration order is ``sorted(...)`` of each namespace, so this is
237
+ deterministic given a signature — useful for reading generated
238
+ programs back while debugging, though nothing downstream depends on
239
+ the specific numbering.
240
+ """
241
+ for i, key in enumerate(sorted(sig.predicates)):
242
+ self.pred_asp[key] = f"p{i}"
243
+ for i, key in enumerate(sorted(sig.functions)):
244
+ self.func_asp[key] = f"f{i}"
245
+ for i, name in enumerate(sorted(sig.constants)):
246
+ self.const_asp[name] = f"k{i}"
247
+
248
+ def emit_base_facts(self) -> None:
249
+ """Emit ``dom/1`` and the free choice for every declared symbol.
250
+
251
+ A predicate of arity ``k`` gets an unconstrained choice over its
252
+ ``domain**k`` possible tuples (arity 0 is a single choice atom, no
253
+ domain conditions needed). A function or constant gets the "total
254
+ relation" choice: for every input tuple (none, for a constant),
255
+ ``1 { … } 1`` picks exactly one result — enforcing functionality
256
+ (at most one) and totality (at least one) in the same rule, the same
257
+ reading :mod:`~unicode_logic_kit.atp.finite_domain`'s design note
258
+ describes for the sibling refutation backends.
259
+ """
260
+ self.rules.append(f"dom(0..{self.size - 1}).")
261
+ for (name, arity), asp in self.pred_asp.items():
262
+ if arity == 0:
263
+ self.rules.append(f"{{{asp}}}.")
264
+ else:
265
+ xs = [f"X{i}" for i in range(arity)]
266
+ conds = ", ".join(f"dom({x})" for x in xs)
267
+ self.rules.append(f"{{{asp}({','.join(xs)}) : {conds}}}.")
268
+ for (name, arity), asp in self.func_asp.items():
269
+ v = "V"
270
+ if arity == 0:
271
+ self.rules.append(f"1 {{ {asp}({v}) : dom({v}) }} 1.")
272
+ else:
273
+ xs = [f"X{i}" for i in range(arity)]
274
+ choice = f"1 {{ {asp}({','.join(xs + [v])}) : dom({v}) }} 1"
275
+ body = ", ".join(f"dom({x})" for x in xs)
276
+ self.rules.append(f"{choice} :- {body}.")
277
+ for name, asp in self.const_asp.items():
278
+ self.rules.append(f"1 {{ {asp}(V) : dom(V) }} 1.")
279
+
280
+ def emit_fixed_facts(self, structure: Structure, index_of: Dict[Any, int],
281
+ free_predicates: Set[Tuple[str, int]]) -> None:
282
+ """Emit ``dom/1``, plus GROUND facts pinning ``structure``'s fixed part.
283
+
284
+ Sibling of :meth:`emit_base_facts`, used only by :func:`asp_holds_so`
285
+ (``emit_base_facts`` itself is untouched, so :func:`asp_find_model` /
286
+ :func:`asp_minimal_models` are unaffected). Every declared predicate
287
+ named in ``free_predicates`` — the SO-quantifier-bound ones — gets
288
+ exactly the same free choice rule ``emit_base_facts`` would give it
289
+ (the quantifier itself needs no other encoding: see
290
+ :func:`asp_holds_so`'s docstring). Every OTHER declared symbol
291
+ (predicate, function, constant) is pinned EXACTLY to its
292
+ interpretation in ``structure`` — ground facts, never a choice rule —
293
+ translating ``structure``'s own (arbitrary, hashable) domain
294
+ individuals to the ``0..size-1`` integers ``declare_signature``'s
295
+ numbering assumes elsewhere via ``index_of``.
296
+
297
+ One case is not "pin to the declared extension, or else empty": an
298
+ order comparison (``< > ≤ ≥``) that ``structure`` declares NO
299
+ extension for. :func:`~unicode_logic_kit.semantics.tarski._order_value`
300
+ does not read that as empty (always false) — its third, lowest-
301
+ priority reading is that the comparison still holds numerically
302
+ between two operands that themselves evaluate to numbers (see that
303
+ function's own docstring). Pinning such a predicate to the empty
304
+ relation here would silently disagree with the very evaluator
305
+ :func:`asp_holds_so` claims to match once the block's body compares
306
+ two numeric domain individuals with no declared ``<``/etc.
307
+ extension — see :meth:`_order_numeric_extension`, used below exactly
308
+ where ``emit_base_facts`` has no analogous case (a fully free choice
309
+ never needs this fallback).
310
+
311
+ Args:
312
+ structure: the structure whose fixed part is pinned.
313
+ index_of: ``{domain individual: 0-based index}`` for every
314
+ individual in ``structure.domain`` (``len(structure.domain)``
315
+ must equal ``self.size``).
316
+ free_predicates: the ``(name, arity)`` pairs to leave as a free
317
+ choice instead of pinning (the SO-quantifier-bound ones).
318
+
319
+ Raises:
320
+ ValueError: a constant has no interpretation in ``structure``
321
+ (and does not itself parse as a numeral, mirroring
322
+ :func:`~unicode_logic_kit.semantics.tarski.term_value`'s own
323
+ fallback), a function has no interpretation in ``structure``
324
+ or is not TOTAL over its domain (an argument tuple with no
325
+ value — this encoder's function encoding is the same
326
+ "total relation" reading :func:`emit_base_facts`
327
+ free-chooses, so a partial interpretation does not fit it),
328
+ or a pinned value (from a predicate's declared extension, a
329
+ function's arguments/result, or a constant) is not itself a
330
+ member of ``structure.domain``. A declared PREDICATE with no
331
+ interpretation in ``structure`` never raises here — it
332
+ silently falls through to the empty relation (or, for an
333
+ order comparison, to :meth:`_order_numeric_extension`'s
334
+ numeric reading), the same "missing extension is the empty
335
+ relation, hence false" fallback
336
+ :func:`~unicode_logic_kit.semantics.tarski._atom_value` /
337
+ :func:`~unicode_logic_kit.semantics.tarski._order_value`
338
+ themselves document, not a gap.
339
+ """
340
+ self.rules.append(f"dom(0..{self.size - 1}).")
341
+
342
+ for (name, arity), asp in self.pred_asp.items():
343
+ if (name, arity) in free_predicates:
344
+ if arity == 0:
345
+ self.rules.append(f"{{{asp}}}.")
346
+ else:
347
+ xs = [f"X{i}" for i in range(arity)]
348
+ conds = ", ".join(f"dom({x})" for x in xs)
349
+ self.rules.append(f"{{{asp}({','.join(xs)}) : {conds}}}.")
350
+ continue
351
+ if arity == 0:
352
+ if bool(structure.predicates.get((name, 0), False)):
353
+ self.rules.append(f"{asp}.")
354
+ continue
355
+ extension = structure.predicates.get((name, arity))
356
+ if extension is None:
357
+ extension = (self._order_numeric_extension(name, structure)
358
+ if arity == 2 and name in _ORDER_COMPARISONS else ())
359
+ for tup in extension:
360
+ idxs = [self._pin_value(v, index_of, "predicate", name) for v in tup]
361
+ self.rules.append(f"{asp}({','.join(str(i) for i in idxs)}).")
362
+
363
+ for (name, arity), asp in self.func_asp.items():
364
+ # Functions are never second-order-bound (SecondOrderQuantifier
365
+ # binds a PREDICATE name only — see the module docstring), so
366
+ # every function declared here is pinned.
367
+ key = (name, arity)
368
+ if key not in structure.functions:
369
+ raise ValueError(
370
+ f"asp_holds_so: function {name!r}/{arity} has no "
371
+ "interpretation in the given structure."
372
+ )
373
+ interp = structure.functions[key]
374
+ for args in product(structure.domain, repeat=arity):
375
+ if callable(interp):
376
+ value = interp(*args)
377
+ else:
378
+ if args not in interp:
379
+ raise ValueError(
380
+ f"asp_holds_so: function {name!r}/{arity} is not "
381
+ f"total over the given structure's domain — "
382
+ f"undefined for arguments {args!r} (this "
383
+ "encoder's function encoding requires exactly "
384
+ "one result per input tuple, the same 'total "
385
+ "relation' reading emit_base_facts free-chooses)."
386
+ )
387
+ value = interp[args]
388
+ idxs = [self._pin_value(a, index_of, "function", name) for a in args]
389
+ idxs.append(self._pin_value(value, index_of, "function", name))
390
+ self.rules.append(f"{asp}({','.join(str(i) for i in idxs)}).")
391
+
392
+ for name, asp in self.const_asp.items():
393
+ # Constants are never second-order-bound either.
394
+ if name in structure.constants:
395
+ value = structure.constants[name]
396
+ else:
397
+ # No override -- mirror tarski.term_value's own Number
398
+ # fallback (a Number is scanned as a constant named by its
399
+ # value, read as the literal itself unless overridden;
400
+ # see modelfinder._Signature.scan / this module's own _term).
401
+ try:
402
+ value = int(name)
403
+ except ValueError:
404
+ raise ValueError(
405
+ f"asp_holds_so: constant {name!r} has no "
406
+ "interpretation in the given structure."
407
+ )
408
+ idx = self._pin_value(value, index_of, "constant", name)
409
+ self.rules.append(f"{asp}({idx}).")
410
+
411
+ def _order_numeric_extension(self, name: str, structure: Structure) -> Set[Tuple[Any, Any]]:
412
+ """The numeric-fallback extension of an order comparison ``structure``
413
+ declares no extension for — mirrors
414
+ :func:`~unicode_logic_kit.semantics.tarski._order_value`'s rule (3).
415
+
416
+ Computed directly over ``structure.domain`` rather than over every
417
+ term this encoder's callers might build: every term
418
+ :meth:`emit_fixed_facts` ever pins (a constant, a function result, a
419
+ quantified variable) is already required to be a member of
420
+ ``structure.domain`` (:meth:`_pin_value` raises otherwise), so the
421
+ domain's own individuals are exactly the ``(left, right)`` value
422
+ pairs :func:`~unicode_logic_kit.semantics.tarski._order_value` would
423
+ ever see for this predicate once no declared extension applies. A
424
+ pair where either individual is not a number (:func:`~unicode_logic_kit.semantics.tarski._is_number`,
425
+ which excludes ``bool``) is simply absent from the result — the same
426
+ "anything else is false" fallback ``_order_value`` itself uses, not
427
+ an error: a non-numeric domain is a legitimate case, not a gap.
428
+
429
+ Returns:
430
+ The set of ``(x, y)`` pairs of RAW domain individuals (not yet
431
+ translated to ``dom/1`` indices — the caller does that via
432
+ :meth:`_pin_value`, same as for a declared extension) for which
433
+ ``name``'s numeric reading holds.
434
+ """
435
+ op = _ORDER_OPS[name]
436
+ return {
437
+ (x, y)
438
+ for x in structure.domain
439
+ for y in structure.domain
440
+ if _is_number(x) and _is_number(y) and op(x, y)
441
+ }
442
+
443
+ def _pin_value(self, value: Any, index_of: Dict[Any, int], kind: str, name: str) -> int:
444
+ """Translate one structure individual to its ``dom/1`` index, or raise."""
445
+ if value not in index_of:
446
+ raise ValueError(
447
+ f"asp_holds_so: the given structure's {kind} {name!r} "
448
+ f"produces the value {value!r}, which is not itself a member "
449
+ "of the structure's own domain."
450
+ )
451
+ return index_of[value]
452
+
453
+ # -- term evaluation ------------------------------------------------------
454
+
455
+ def _term(self, term: Node, var_of: Dict[str, str], body: List[str]) -> str:
456
+ """Return the ASP variable naming ``term``'s value, extending ``body``.
457
+
458
+ A term is not a single ASP value the way it is a single Python value
459
+ under :func:`~unicode_logic_kit.semantics.tarski.term_value` — every
460
+ constant/function application here is itself a RELATION (the total-
461
+ relation encoding), so evaluating a term means walking it and
462
+ emitting one join literal per constant/function occurrence, each
463
+ introducing a fresh ASP variable for its result. ``body`` is mutated
464
+ in place (appended to) rather than returned and concatenated by every
465
+ caller, since a term's evaluation is always exactly one ingredient of
466
+ a larger rule body being built up alongside it.
467
+ """
468
+ if isinstance(term, Variable):
469
+ if term.name not in var_of:
470
+ raise ValueError(
471
+ f"asp_models: variable {term.name!r} is not bound by any "
472
+ "enclosing quantifier — a free variable must have been "
473
+ "replaced by a parameter constant (see _closed_sentences) "
474
+ "before encoding."
475
+ )
476
+ return var_of[term.name]
477
+ if isinstance(term, Constant):
478
+ v = self._fresh_var()
479
+ body.append(f"{self.const_asp[term.name]}({v})")
480
+ return v
481
+ if isinstance(term, Number):
482
+ # Mirrors _Signature.scan exactly: a Number is registered as a
483
+ # constant named by its VALUE (numeral_key: 1 and 1.0 are '1'), NOT
484
+ # pinned to its literal value — see modelfinder._Signature.scan and
485
+ # tarski.term_value. Pinning it to the literal instead would look
486
+ # more natural but would silently diverge from what the oracle this
487
+ # module is verified against actually computes, breaking the one
488
+ # invariant this module exists to protect.
489
+ v = self._fresh_var()
490
+ body.append(f"{self.const_asp[numeral_key(term.value)]}({v})")
491
+ return v
492
+ if isinstance(term, Function):
493
+ arg_vars = [self._term(a, var_of, body) for a in term.args]
494
+ v = self._fresh_var()
495
+ key = (term.name, len(term.args))
496
+ body.append(f"{self.func_asp[key]}({','.join(arg_vars + [v])})")
497
+ return v
498
+ raise ValueError(
499
+ f"asp_models: {type(term).__name__} is not an encodable term "
500
+ "(only Variable/Constant/Number/Function are)."
501
+ )
502
+
503
+ # -- formula encoding -----------------------------------------------------
504
+
505
+ def encode(self, node: Node, var_of: Dict[str, str]) -> Tuple[str, List[str]]:
506
+ """Emit rules defining ``node``'s truth; return ``(reference, free_names)``.
507
+
508
+ ``reference`` is the ASP literal text a PARENT rule uses to mention
509
+ this node's truth value (a bare head name if ``node`` is closed, or
510
+ ``head(V1,…)`` over its free variables' current ASP names otherwise).
511
+ ``free_names`` is the sorted list of ``node``'s free FOL variable
512
+ names. ``var_of`` maps every FOL variable name currently in scope
513
+ (bound by an enclosing quantifier already processed) to the ASP
514
+ variable name standing for it — extended with a FRESH ASP name on
515
+ entering a ``Quantifier``/``Count``, so a shadowed inner binder using
516
+ the same FOL name as an outer one never captures the outer binding
517
+ (mirrors :func:`~unicode_logic_kit.semantics.tarski._extend`'s
518
+ dict-overwrite shadowing semantics, which is exactly as correct here
519
+ as it is there for the same reason: assignment lookup is always the
520
+ innermost binding).
521
+
522
+ A ``∀`` quantifier is rewritten to ``¬∃¬`` and re-dispatched through
523
+ this same method rather than given its own encoding — see the module
524
+ docstring's "The ASP encoding" section for why.
525
+ """
526
+ if isinstance(node, Quantifier) and node.type in _FORALL:
527
+ return self.encode(Not(Quantifier("∃", node.variable, Not(node.formula))), var_of)
528
+
529
+ free_names = sorted(_free_var_names_local(node))
530
+ head_args = [var_of[n] for n in free_names]
531
+ head = self._fresh_head()
532
+ head_ref = head if not head_args else f"{head}({','.join(head_args)})"
533
+ dom_lits = [f"dom({v})" for v in head_args]
534
+
535
+ if isinstance(node, Atom):
536
+ self._encode_atom(node, var_of, head_ref, dom_lits)
537
+ elif isinstance(node, Not):
538
+ inner_ref, _ = self.encode(node.formula, var_of)
539
+ self._rule(head_ref, dom_lits + [f"not {inner_ref}"])
540
+ elif isinstance(node, And):
541
+ l_ref, _ = self.encode(node.left, var_of)
542
+ r_ref, _ = self.encode(node.right, var_of)
543
+ self._rule(head_ref, dom_lits + [l_ref, r_ref])
544
+ elif isinstance(node, Or):
545
+ l_ref, _ = self.encode(node.left, var_of)
546
+ r_ref, _ = self.encode(node.right, var_of)
547
+ self._rule(head_ref, dom_lits + [l_ref])
548
+ self._rule(head_ref, dom_lits + [r_ref])
549
+ elif isinstance(node, Xor):
550
+ l_ref, _ = self.encode(node.left, var_of)
551
+ r_ref, _ = self.encode(node.right, var_of)
552
+ self._rule(head_ref, dom_lits + [l_ref, f"not {r_ref}"])
553
+ self._rule(head_ref, dom_lits + [r_ref, f"not {l_ref}"])
554
+ elif isinstance(node, Implies):
555
+ l_ref, _ = self.encode(node.left, var_of)
556
+ r_ref, _ = self.encode(node.right, var_of)
557
+ self._rule(head_ref, dom_lits + [f"not {l_ref}"])
558
+ self._rule(head_ref, dom_lits + [r_ref])
559
+ elif isinstance(node, Iff):
560
+ l_ref, _ = self.encode(node.left, var_of)
561
+ r_ref, _ = self.encode(node.right, var_of)
562
+ self._rule(head_ref, dom_lits + [l_ref, r_ref])
563
+ self._rule(head_ref, dom_lits + [f"not {l_ref}", f"not {r_ref}"])
564
+ elif isinstance(node, Quantifier): # node.type in _EXISTS, by elimination
565
+ inner_var = self._fresh_var()
566
+ var_of2 = dict(var_of)
567
+ var_of2[node.variable.name] = inner_var
568
+ inner_ref, _ = self.encode(node.formula, var_of2)
569
+ self._rule(head_ref, dom_lits + [inner_ref])
570
+ elif isinstance(node, Count):
571
+ inner_var = self._fresh_var()
572
+ var_of2 = dict(var_of)
573
+ var_of2[node.variable.name] = inner_var
574
+ inner_ref, _ = self.encode(node.formula, var_of2)
575
+ op = {"ge": ">=", "le": "<=", "eq": "="}[node.op]
576
+ agg = f"#count{{ {inner_var} : dom({inner_var}), {inner_ref} }} {op} {node.n.value}"
577
+ self._rule(head_ref, dom_lits + [agg])
578
+ else:
579
+ raise ValueError(
580
+ f"asp_models: {type(node).__name__} reached the encoder despite "
581
+ "_check_fragment — this is an internal invariant violation, not "
582
+ "a normal unsupported-fragment report."
583
+ )
584
+
585
+ return head_ref, free_names
586
+
587
+ def _rule(self, head_ref: str, body: List[str]) -> None:
588
+ """Append one ``head :- body`` rule (``body`` is always non-empty here)."""
589
+ self.rules.append(f"{head_ref} :- {', '.join(body)}.")
590
+
591
+ def _encode_atom(self, node: Atom, var_of: Dict[str, str], head_ref: str, dom_lits: List[str]) -> None:
592
+ """Emit the rule(s) defining an ``Atom``'s auxiliary head atom.
593
+
594
+ ``=``/``≠`` at arity 2 get the built-in identity reading (an ASP
595
+ variable comparison, never a free-choice predicate); every other
596
+ atom — including the order comparisons ``< > ≤ ≥``, which
597
+ :func:`~unicode_logic_kit.semantics.modelfinder._Signature.scan`
598
+ registers as ORDINARY predicates, not built-ins, exactly like
599
+ :func:`~unicode_logic_kit.semantics.tarski._order_value`'s own
600
+ "a declared extension wins" reading — is a free-choice predicate
601
+ lookup, so no special-casing is needed for them here beyond what
602
+ the general branch already does.
603
+ """
604
+ body: List[str] = list(dom_lits)
605
+ constant = _truth_value(node)
606
+ if constant is not None:
607
+ # `$true` holds wherever the head is defined at all, `$false` nowhere: no
608
+ # rule defines the head, so the solver reads it as false.
609
+ if constant:
610
+ if body:
611
+ self._rule(head_ref, body)
612
+ else:
613
+ self.rules.append(f"{head_ref}.")
614
+ return
615
+ if node.predicate in ("=", "≠") and len(node.args) == 2:
616
+ a = self._term(node.args[0], var_of, body)
617
+ b = self._term(node.args[1], var_of, body)
618
+ body.append(f"{a}{'=' if node.predicate == '=' else '!='}{b}")
619
+ self._rule(head_ref, body)
620
+ return
621
+ arg_vars = [self._term(a, var_of, body) for a in node.args]
622
+ key = (node.predicate, len(node.args))
623
+ if key not in self.pred_asp:
624
+ raise KeyError(
625
+ f"asp_models: predicate {node.predicate!r}/{len(node.args)} is "
626
+ "not in the scanned signature — internal invariant violation "
627
+ "(every predicate an encoded sentence uses must have been "
628
+ "scanned into the signature first)."
629
+ )
630
+ pred_lit = self.pred_asp[key] if not arg_vars else f"{self.pred_asp[key]}({','.join(arg_vars)})"
631
+ body.append(pred_lit)
632
+ self._rule(head_ref, body)
633
+
634
+
635
+ def _free_var_names_local(node: Node) -> Set[str]:
636
+ """``_free_var_names(node)`` from modelfinder, at the default (empty) bound.
637
+
638
+ A thin wrapper purely so every call site above reads as "the free
639
+ variables of this node" without repeating the ``frozenset()`` default
640
+ argument; kept local (not re-exported) since it is a one-line adapter,
641
+ not a new piece of logic.
642
+ """
643
+ from .modelfinder import _free_var_names
644
+ return _free_var_names(node)
645
+
646
+
647
+ # =============================================================================
648
+ # Model decoding — clingo's answer set back into modelfinder's own shape
649
+ # =============================================================================
650
+
651
+ def _decode_model(model, enc: "_AspEncoder", sig: "_Signature", size: int
652
+ ) -> Tuple[Dict[str, int], Dict[Tuple[str, int], dict], Dict[Tuple[str, int], set]]:
653
+ """Turn one clingo answer set into ``(constants, functions, predicates)``.
654
+
655
+ The exact shapes :func:`~unicode_logic_kit.semantics.modelfinder._interpretations`
656
+ produces (domain individuals are the plain ints ``0..size-1``, NOT the
657
+ strings :mod:`~unicode_logic_kit.semantics.structures.FiniteStructure` uses)
658
+ — required for byte-identical comparison against
659
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`'s own
660
+ output, and for feeding straight into ``_circ_profile``/``_fixed_key``.
661
+
662
+ Raises:
663
+ RuntimeError: a constant's choice rule produced a count of true
664
+ atoms other than 1 in this answer set — the ``1 { … } 1`` choice
665
+ rule :meth:`_AspEncoder.emit_base_facts` emits is supposed to
666
+ make this impossible, so seeing it would mean the generated
667
+ program itself is broken, not that the caller misused anything.
668
+ """
669
+ by_name: Dict[str, list] = {}
670
+ for sym in model.symbols(atoms=True):
671
+ by_name.setdefault(sym.name, []).append(sym)
672
+
673
+ constants: Dict[str, int] = {}
674
+ for name, asp in enc.const_asp.items():
675
+ matches = by_name.get(asp, [])
676
+ if len(matches) != 1:
677
+ raise RuntimeError(
678
+ f"asp_models: constant {name!r} ({asp}) has {len(matches)} "
679
+ "true atoms in an answer set, expected exactly 1 — the "
680
+ "'1 { … } 1' choice rule should make this impossible."
681
+ )
682
+ (arg,) = matches[0].arguments
683
+ constants[name] = arg.number
684
+
685
+ functions: Dict[Tuple[str, int], dict] = {}
686
+ for (name, arity), asp in enc.func_asp.items():
687
+ table: Dict[Tuple[int, ...], int] = {}
688
+ for sym in by_name.get(asp, []):
689
+ args = tuple(a.number for a in sym.arguments)
690
+ table[args[:-1]] = args[-1]
691
+ functions[(name, arity)] = table
692
+
693
+ predicates: Dict[Tuple[str, int], set] = {}
694
+ for (name, arity), asp in enc.pred_asp.items():
695
+ predicates[(name, arity)] = {
696
+ tuple(a.number for a in sym.arguments) for sym in by_name.get(asp, [])
697
+ }
698
+
699
+ return constants, functions, predicates
700
+
701
+
702
+ # =============================================================================
703
+ # Shared setup: signature scan + program assembly for both public functions
704
+ # =============================================================================
705
+
706
+ def _closed_sentences(premises: Iterable[Node]) -> List[Node]:
707
+ """``premises`` with every free variable read as a PARAMETER of the problem.
708
+
709
+ A free variable is one unknown element, the same in every premise: it is replaced,
710
+ in all the premises together, by a constant of its own name
711
+ (:func:`~unicode_logic_kit.fol._free_parameters.parameterize`), so ``P(x), ¬P(y)`` has a
712
+ model (``x`` and ``y`` are two elements) and ``P(x), ¬P(x)`` has none. A premise is
713
+ never closed universally: ``∀x P(x)`` is another premise than ``P(x)``. The structure
714
+ a call returns interprets the parameter like any constant, ``constants['x']``.
715
+
716
+ Raises:
717
+ NotImplementedError: a free variable has the spelling of a constant of the
718
+ premises (a structure holds one entry per name).
719
+ """
720
+ sentences, _ = parameterize(list(premises), after_variables=True)
721
+ return sentences
722
+
723
+
724
+ def _build_program(sentences: List[Node], size: int) -> Tuple[str, "_AspEncoder", "_Signature"]:
725
+ """Scan ``sentences`` for their signature and assemble the full ASP program.
726
+
727
+ Shared by :func:`asp_find_model` and :func:`asp_minimal_models` — the
728
+ only difference between the two callers is how many answer sets clingo
729
+ is asked for, not how the program is built.
730
+
731
+ Raises:
732
+ TypeError: ``size`` is not a plain ``int``, or a member of
733
+ ``sentences`` still has a free variable (an internal invariant —
734
+ see :func:`_closed_sentences`, applied by both public callers
735
+ before this function ever runs).
736
+ ValueError: ``size < 1``, or a sentence uses a construct outside
737
+ this module's fragment (see :func:`_check_fragment`).
738
+ """
739
+ if isinstance(size, bool) or not isinstance(size, int) or size < 1:
740
+ raise ValueError(f"asp_models: size must be an int >= 1, got {size!r}.")
741
+ _check_fragment(sentences)
742
+
743
+ sig = _Signature()
744
+ for s in sentences:
745
+ sig.scan(s)
746
+
747
+ enc = _AspEncoder(size)
748
+ enc.declare_signature(sig)
749
+ enc.emit_base_facts()
750
+ for s in sentences:
751
+ head_ref, free = enc.encode(s, {})
752
+ if free:
753
+ raise TypeError(
754
+ f"asp_models: sentence {s.to_unicode_str()!r} still has free "
755
+ f"variable(s) {free} after the parameters were substituted — internal "
756
+ "invariant violation."
757
+ )
758
+ enc.rules.append(f":- not {head_ref}.")
759
+
760
+ return "\n".join(enc.rules), enc, sig
761
+
762
+
763
+ # =============================================================================
764
+ # Public API
765
+ # =============================================================================
766
+
767
+ def asp_find_model(premises: Iterable[Node], size: int = 3) -> Optional[Structure]:
768
+ """Return one finite :class:`~unicode_logic_kit.semantics.tarski.Structure`
769
+ of exactly ``size`` individuals satisfying every one of ``premises``, or
770
+ ``None``.
771
+
772
+ The ASP analog of
773
+ :func:`~unicode_logic_kit.semantics.modelfinder.find_model`, but grounded
774
+ at exactly ``size`` rather than searching ``1 … max_size`` — see the
775
+ module docstring's "``size`` is a single domain size, not a bound"
776
+ section; a caller wanting the multi-size search calls this in a loop.
777
+
778
+ Args:
779
+ premises: the sentences to satisfy together. A free variable is a
780
+ PARAMETER of the problem, one unknown element shared by every
781
+ premise (:func:`_closed_sentences`), as in
782
+ :func:`~unicode_logic_kit.semantics.modelfinder.find_model`:
783
+ ``P(x), ¬P(y)`` has a model, ``P(x), ¬P(x)`` has none, and the
784
+ returned structure reports the element under the variable's name,
785
+ ``constants['x']``.
786
+ size: the exact domain size to search, ``>= 1``.
787
+
788
+ Returns:
789
+ A structure over the domain ``0 … size-1`` satisfying every premise,
790
+ or ``None`` if clingo finds the grounded program unsatisfiable at
791
+ this size (NOT evidence of unsatisfiability at any other size, or of
792
+ first-order unsatisfiability — the same one-sided-boundedness every
793
+ finite-model search in this kit carries).
794
+
795
+ Raises:
796
+ ValueError: ``size < 1``, or a premise uses a construct outside this
797
+ module's fragment (see the module docstring).
798
+ NotImplementedError: a free variable has the spelling of a constant of
799
+ the premises.
800
+ """
801
+ import clingo
802
+
803
+ sentences = _closed_sentences(premises)
804
+ program, enc, sig = _build_program(sentences, size)
805
+
806
+ ctl = clingo.Control(["1"], logger=_silent)
807
+ ctl.add("base", [], program)
808
+ ctl.ground([("base", [])])
809
+
810
+ with ctl.solve(yield_=True) as handle:
811
+ for model in handle:
812
+ constants, functions, predicates = _decode_model(model, enc, sig, size)
813
+ return Structure(tuple(range(size)), constants=constants,
814
+ functions=functions, predicates=predicates)
815
+ return None
816
+
817
+
818
+ def asp_minimal_models(premises: Iterable[Node], circumscribed: Optional[Set[str]] = None,
819
+ size: int = 3) -> List[Structure]:
820
+ """Return the ≤-minimal finite models of ``premises`` at exactly ``size``.
821
+
822
+ clingo enumerates EVERY model of the grounded ``premises`` at this
823
+ domain size (the fragment gate, choice rules and per-connective encoding
824
+ are described in the module docstring); this function then applies
825
+ :mod:`~unicode_logic_kit.semantics.nonmonotonic`'s own, UNMODIFIED
826
+ minimality filter (``_fixed_key`` groups models sharing a domain,
827
+ constants, functions and every non-circumscribed predicate; within each
828
+ group, ``_circ_profile``/``_strictly_below`` keep the ones no other group
829
+ member's circumscribed-predicate extensions are a strict subset of) — the
830
+ exact same filter
831
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models` applies to
832
+ its own, brute-force-enumerated candidates. See the module docstring's
833
+ "Why reuse rather than reimplement the filter" section for why sharing
834
+ this code, rather than writing a second copy of the same comparison, is
835
+ what makes this function's answer trustworthy.
836
+
837
+ Args:
838
+ premises: the sentences every returned model satisfies. A free
839
+ variable is a PARAMETER, one constant of its own name shared by
840
+ every premise (:func:`_closed_sentences`), exactly as
841
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`
842
+ reads it; the parameter belongs to the fixed part two models must
843
+ share to be compared.
844
+ circumscribed: the predicate NAMES to minimise (arity is whatever the
845
+ premises use it at). ``None`` (the default) minimises every
846
+ predicate the premises mention — the same default
847
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`
848
+ uses, and the same "bare name, not a ``(name, arity)`` pair"
849
+ convention (unlike
850
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.circumscription_formula`'s
851
+ ``CircSpec``, which additionally accepts explicit arities — not
852
+ needed here since every predicate this function can minimise
853
+ was, by construction, already found by scanning ``premises``).
854
+ size: the exact domain size to search, ``>= 1`` — see the module
855
+ docstring's "``size`` is a single domain size, not a bound"
856
+ section for how this differs from ``minimal_models``'s
857
+ ``max_size``.
858
+
859
+ Returns:
860
+ Every ≤-minimal structure (see
861
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models`'s own
862
+ docstring for the exact ``≤`` relation) over the domain
863
+ ``0 … size-1`` that satisfies every premise. Empty if no model
864
+ exists at this size at all.
865
+
866
+ Raises:
867
+ ValueError: ``size < 1``, or a premise uses a construct outside this
868
+ module's fragment (see the module docstring).
869
+ NotImplementedError: a free variable has the spelling of a constant of
870
+ the premises.
871
+ """
872
+ import clingo
873
+
874
+ sentences = _closed_sentences(premises)
875
+ program, enc, sig = _build_program(sentences, size)
876
+
877
+ pred_sig = sorted(sig.predicates)
878
+ circ = set(circumscribed) if circumscribed is not None else {n for n, _ in pred_sig}
879
+
880
+ ctl = clingo.Control(["0"], logger=_silent)
881
+ ctl.add("base", [], program)
882
+ ctl.ground([("base", [])])
883
+
884
+ found = []
885
+ with ctl.solve(yield_=True) as handle:
886
+ for model in handle:
887
+ constants, functions, predicates = _decode_model(model, enc, sig, size)
888
+ structure = Structure(tuple(range(size)), constants=constants,
889
+ functions=functions, predicates=predicates)
890
+ found.append((constants, functions, predicates, structure))
891
+
892
+ # Group by fixed part (domain + constants + functions + non-circumscribed
893
+ # predicate extensions); within each group keep only the models no other
894
+ # member's circumscribed-predicate profile is strictly below — verbatim
895
+ # the same grouping loop
896
+ # :func:`~unicode_logic_kit.semantics.nonmonotonic.minimal_models` runs over
897
+ # its OWN (brute-force-enumerated) `found`, so the two functions can only
898
+ # ever disagree about which models were found, never about which of them
899
+ # count as minimal.
900
+ groups: Dict[tuple, list] = {}
901
+ for entry in found:
902
+ constants, functions, predicates, _ = entry
903
+ key = _fixed_key(constants, functions, predicates, pred_sig, circ)
904
+ profile = _circ_profile(predicates, pred_sig, circ)
905
+ groups.setdefault(key, []).append((entry, profile))
906
+
907
+ result: List[Structure] = []
908
+ for grp in groups.values():
909
+ for (entry, profile) in grp:
910
+ if not any(_strictly_below(other, profile) for (e2, other) in grp if e2 is not entry):
911
+ result.append(entry[3])
912
+ return result
913
+
914
+
915
+ # =============================================================================
916
+ # Single-block second-order checking (roadmap C24): asp_holds_so
917
+ # =============================================================================
918
+ #
919
+ # secondorder.satisfies_so evaluates ∀P/∃P by brute-force enumeration of every
920
+ # relation of P's arity — 2 ** (n ** k), doubly exponential (see that module's
921
+ # docstring). The exact mechanism this module already uses for an ORDINARY
922
+ # predicate — "a free ASP choice, propagation-pruned by clingo instead of
923
+ # materialised in Python" (module docstring, "The ASP encoding") — applies
924
+ # just as well to a SECOND-ORDER-BOUND predicate, PROVIDED the quantifier
925
+ # nesting is simple enough that "propagation-pruned choice + one solve" is a
926
+ # SOUND reduction: a single leading block of SAME-polarity SecondOrderQuantifier
927
+ # occurrences (∃P1…∃Pk or ∀P1…∀Pk, nothing else of that kind anywhere in the
928
+ # sentence). One ∃-block is an ordinary satisfiability check (NP-flavoured,
929
+ # exactly what clingo solves). One ∀-block reduces to a single
930
+ # UNSAT-of-the-negation check (co-NP-flavoured — ask whether any answer set
931
+ # witnesses the negation; none existing means the block holds for every
932
+ # choice). Genuine ALTERNATION (∀P∃Q…) is a strictly harder complexity class
933
+ # (Σ2p/Π2p) that plain, non-disjunctive clingo choice rules do not capture
934
+ # soundly — see secondorder.py's own module docstring and roadmap C24's
935
+ # existing_coverage for the argument — so it is refused here, loudly, rather
936
+ # than silently mishandled.
937
+
938
+ def _so_quantifier_chain(sentence: Node) -> Tuple[Optional[str], List[SecondOrderQuantifier]]:
939
+ """Validate ``sentence``'s SecondOrderQuantifier occurrences; return its block.
940
+
941
+ Returns ``(None, [])`` if ``sentence`` has no SecondOrderQuantifier at all
942
+ (a purely classical sentence — :func:`asp_holds_so` accepts this as the
943
+ degenerate zero-quantifier case). Otherwise returns ``("forall" | "exists",
944
+ chain)``, ``chain`` being the block's nodes in outer-to-inner order.
945
+
946
+ "A single block" means every SecondOrderQuantifier occurrence in
947
+ ``sentence`` — wherever it sits in the tree, including nested inside
948
+ ordinary classical structure such as
949
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.circumscription_entails_so`'s
950
+ ``Implies``/``And`` — is reachable from exactly ONE entry node by
951
+ following ``.formula`` through SecondOrderQuantifier nodes ONLY, all of
952
+ the SAME polarity, until reaching a body with no further
953
+ SecondOrderQuantifier anywhere inside it. A second, unrelated occurrence
954
+ (a sibling block, or one merely nested a connective away rather than
955
+ directly wrapping the next) is rejected exactly like true alternation —
956
+ both are outside the fragment :func:`asp_holds_so` soundly covers.
957
+
958
+ Raises:
959
+ ValueError: mixed ``∀``/``∃`` polarities, an unrecognised quantifier
960
+ spelling, more than one entry point (disconnected or
961
+ connective-separated occurrences), or a SecondOrderQuantifier
962
+ nested inside the block's own innermost body — naming the
963
+ offending sentence in every case.
964
+ """
965
+ all_so = [n for n in sentence.walk() if isinstance(n, SecondOrderQuantifier)]
966
+ if not all_so:
967
+ return None, []
968
+
969
+ types = {n.type for n in all_so}
970
+ forall_seen = types & set(_FORALL)
971
+ exists_seen = types & set(_EXISTS)
972
+ unknown = types - forall_seen - exists_seen
973
+ if unknown:
974
+ raise ValueError(
975
+ f"asp_holds_so: unknown SecondOrderQuantifier type(s) {sorted(unknown)!r} "
976
+ f"in {sentence.to_unicode_str()!r} — only unsorted "
977
+ f"{_FORALL + _EXISTS} are supported here."
978
+ )
979
+ if forall_seen and exists_seen:
980
+ raise ValueError(
981
+ f"asp_holds_so: {sentence.to_unicode_str()!r} mixes ∀ and ∃ "
982
+ "second-order quantifiers (alternation) — only a single leading "
983
+ "block of the SAME polarity is supported here; use "
984
+ "secondorder.satisfies_so for alternating second-order "
985
+ "quantification."
986
+ )
987
+ block_type = "forall" if forall_seen else "exists"
988
+
989
+ bodies_that_are_so = {
990
+ id(n.formula) for n in all_so if isinstance(n.formula, SecondOrderQuantifier)
991
+ }
992
+ entry_points = [n for n in all_so if id(n) not in bodies_that_are_so]
993
+ if len(entry_points) != 1:
994
+ raise ValueError(
995
+ f"asp_holds_so: {sentence.to_unicode_str()!r} has "
996
+ f"{len(entry_points)} separate second-order-quantifier chains "
997
+ "(nested through a non-quantifier connective, or genuinely "
998
+ "scattered) — only a single leading block, with nothing else of "
999
+ "its kind anywhere in the sentence, is supported here; use "
1000
+ "secondorder.satisfies_so instead."
1001
+ )
1002
+
1003
+ chain: List[SecondOrderQuantifier] = []
1004
+ cur: Node = entry_points[0]
1005
+ while isinstance(cur, SecondOrderQuantifier):
1006
+ chain.append(cur)
1007
+ cur = cur.formula
1008
+
1009
+ if any(isinstance(n, SecondOrderQuantifier) for n in cur.walk()):
1010
+ raise ValueError(
1011
+ f"asp_holds_so: a SecondOrderQuantifier is nested inside the "
1012
+ f"single block's own body in {sentence.to_unicode_str()!r} "
1013
+ "(quantifier alternation) — use secondorder.satisfies_so instead."
1014
+ )
1015
+ if len(chain) != len(all_so):
1016
+ raise ValueError( # pragma: no cover - defensive; unreachable given the checks above
1017
+ f"asp_holds_so: the second-order quantifiers in "
1018
+ f"{sentence.to_unicode_str()!r} do not form a single connected "
1019
+ "chain — use secondorder.satisfies_so instead."
1020
+ )
1021
+ return block_type, chain
1022
+
1023
+
1024
+ def _replace_so_block(node: Node, entry: SecondOrderQuantifier, replacement: Node) -> Node:
1025
+ """Rebuild ``node``, replacing the ONE occurrence ``entry`` (by identity) with ``replacement``.
1026
+
1027
+ ``entry`` is found by object identity (``is``), not structural equality —
1028
+ the single node :func:`_so_quantifier_chain` identified as the block's
1029
+ entry point, wherever it sits in ``node``. Used to splice the block's own
1030
+ (already-computed) truth value back into the surrounding classical
1031
+ sentence — see :func:`asp_holds_so`.
1032
+ """
1033
+ if node is entry:
1034
+ return replacement
1035
+ return node.map_children(lambda c: _replace_so_block(c, entry, replacement))
1036
+
1037
+
1038
+ def asp_holds_so(sentence: Node, structure: Structure) -> bool:
1039
+ """Whether ``structure`` satisfies second-order ``sentence`` (ASP-grounded).
1040
+
1041
+ Mirrors :func:`~unicode_logic_kit.semantics.secondorder.holds`'s signature,
1042
+ and — on the fragment described below — its exact answer (verified
1043
+ differentially against ``secondorder.satisfies_so``/``holds`` in this
1044
+ module's test suite; see roadmap C24). Restricted to sentences whose
1045
+ :class:`~unicode_logic_kit.fol.nodes.SecondOrderQuantifier` occurrences form
1046
+ a SINGLE, contiguous, SAME-polarity block — see
1047
+ :func:`_so_quantifier_chain` for exactly what that means (the block need
1048
+ not be at ``sentence``'s outermost node: see
1049
+ :func:`~unicode_logic_kit.semantics.nonmonotonic.circumscription_entails_so`'s
1050
+ output, whose ``∀``-block sits inside an ``Implies``/``And``). A sentence
1051
+ with NO second-order quantifier at all is accepted too, checked as an
1052
+ ordinary closed classical formula against ``structure``.
1053
+
1054
+ THE ENCODING is a TWO-STEP evaluation, not a single whole-sentence ASP
1055
+ solve — a single solve over the WHOLE sentence with the block's wrapper
1056
+ merely stripped is UNSOUND once the block sits in a position where sign
1057
+ matters (e.g. the ANTECEDENT of an ``Implies``, as
1058
+ ``circumscription_entails_so``'s own output does): negating the whole
1059
+ stripped sentence to test the ``∀`` case conflates the block's own
1060
+ polarity with the surrounding connective's, silently turning a nested
1061
+ ``∀`` into something that answers like an ``∃`` (caught by this module's
1062
+ own differential tests against ``satisfies_so`` — a hand-checked
1063
+ ``Implies(∀P(P(a)→Q(a)), Q(a))`` example disagreed before this two-step
1064
+ design). Instead:
1065
+
1066
+ 1. The block's OWN truth value is computed in ISOLATION: its innermost
1067
+ body (``chain[-1].formula`` — the part with no further
1068
+ ``SecondOrderQuantifier``) is ASP-encoded on its own — the SO-bound
1069
+ predicate name(s) get the ordinary free ASP choice any OTHER declared
1070
+ predicate gets (no per-node quantifier encoding is needed; see the
1071
+ module docstring's "The ASP encoding"), and every other symbol this
1072
+ body uses is PINNED to ``structure``'s own extension
1073
+ (:meth:`_AspEncoder.emit_fixed_facts`). One clingo solve then asks:
1074
+ for a ``∃``-block, is the body true in SOME answer set (SAT, ``:- not
1075
+ head.``); for a ``∀``-block, is the body's NEGATION true in NO answer
1076
+ set (UNSAT). This is exactly ``asp_find_model``'s own SAT-checking
1077
+ shape, just scoped to the block's body and a partly-pinned rather than
1078
+ fully-free signature.
1079
+ 2. That single Boolean is spliced back into ``sentence`` in place of the
1080
+ block (:func:`_replace_so_block`, by object identity) as a fresh
1081
+ nullary atom whose extension in an EXTENDED copy of ``structure`` is
1082
+ exactly that Boolean, and the RESULT — an ordinary classical sentence,
1083
+ since the block is now just a 0-ary atom — is handed to
1084
+ :func:`~unicode_logic_kit.semantics.tarski.satisfies`, the real
1085
+ recursive Tarskian evaluator, which threads the surrounding
1086
+ ``Implies``/``And``/``Not``/… polarity correctly because it is not
1087
+ re-derived here, just reused.
1088
+
1089
+ The block's own body may not have any free object variable — genuinely
1090
+ free in ``sentence`` (which must be closed) or bound by an object-level
1091
+ quantifier OUTSIDE the block — since step 1 evaluates it as a
1092
+ self-contained closed sentence; see the ``Raises`` section.
1093
+
1094
+ Requires ``structure``'s functions the block's body uses to be
1095
+ interpreted TOTALLY over its own domain, matching this module's own
1096
+ "total relation" function encoding; see
1097
+ :meth:`_AspEncoder.emit_fixed_facts`.
1098
+
1099
+ Args:
1100
+ sentence: a closed (no free object variable) second-order sentence,
1101
+ single-block as above; the block's own body must, once isolated,
1102
+ be within :func:`_check_fragment`'s classical fragment
1103
+ (``Atom``/``Not``/``And``/``Or``/``Xor``/``Implies``/``Iff``/
1104
+ ``Quantifier``/``Count`` over
1105
+ ``Variable``/``Constant``/``Number``/``Function`` terms; see the
1106
+ module docstring's "Fragment supported here" section) — the
1107
+ surrounding classical structure the block sits inside, if any,
1108
+ is evaluated by the FULL Tarskian evaluator instead (step 2
1109
+ above), so it is not limited to this narrower fragment.
1110
+ structure: the structure to check ``sentence`` against — an
1111
+ arbitrary :class:`~unicode_logic_kit.semantics.tarski.Structure`
1112
+ (any hashable domain, not necessarily the ``0..n-1`` integers
1113
+ :func:`asp_find_model`/:func:`asp_minimal_models` produce; this
1114
+ function builds its own index for the ASP encoding).
1115
+
1116
+ Returns:
1117
+ Whether ``structure`` satisfies ``sentence``.
1118
+
1119
+ Raises:
1120
+ ValueError: the SecondOrderQuantifier occurrences in ``sentence`` do
1121
+ not form a single same-polarity block (alternation, nesting,
1122
+ mixed polarity, or more than one chain — naming the offending
1123
+ sentence); the block's own body uses a construct outside
1124
+ :func:`_check_fragment`'s fragment, or has a free object variable
1125
+ (genuinely free in ``sentence``, or bound outside the block —
1126
+ either way, out of scope for the isolated per-block solve above);
1127
+ or a constant or function the block's body needs from
1128
+ ``structure`` has no interpretation there, a pinned value is not
1129
+ itself a member of ``structure``'s domain, or a function is not
1130
+ total over it — a declared PREDICATE with no interpretation
1131
+ never raises (see :meth:`_AspEncoder.emit_fixed_facts`'s own
1132
+ ``Raises``).
1133
+ ImportError: ``clingo`` (the optional ``asp`` extra) is not
1134
+ installed — never a silent fallback to the brute-force evaluator.
1135
+ """
1136
+ import clingo
1137
+ from .tarski import satisfies
1138
+ from ..atp.sequent import _all_pred_names, _fresh_pred_name
1139
+
1140
+ block_type, chain = _so_quantifier_chain(sentence)
1141
+
1142
+ if not chain:
1143
+ return satisfies(sentence, structure, {})
1144
+
1145
+ entry = chain[0]
1146
+ body = chain[-1].formula
1147
+ _check_fragment([body])
1148
+
1149
+ free_vars = _free_var_names_local(body)
1150
+ if free_vars:
1151
+ raise ValueError(
1152
+ f"asp_holds_so: the second-order block in {sentence.to_unicode_str()!r} "
1153
+ f"has free object variable(s) {sorted(free_vars)} in its own body "
1154
+ "— either genuinely free in `sentence` (which must be closed) or "
1155
+ "bound by an object-level quantifier OUTSIDE the block, neither "
1156
+ "of which this function's isolated per-block solve supports; use "
1157
+ "secondorder.satisfies_so instead."
1158
+ )
1159
+
1160
+ so_names = {(n.predicate, n.arity) for n in chain}
1161
+
1162
+ sig = _Signature()
1163
+ sig.scan(body)
1164
+ sig.predicates |= so_names # a vacuously-quantified SO predicate still needs a choice
1165
+
1166
+ index_of: Dict[Any, int] = {d: i for i, d in enumerate(structure.domain)}
1167
+ size = len(structure.domain)
1168
+
1169
+ enc = _AspEncoder(size)
1170
+ enc.declare_signature(sig)
1171
+ enc.emit_fixed_facts(structure, index_of, so_names)
1172
+
1173
+ target: Node = Not(body) if block_type == "forall" else body
1174
+ head_ref, _free = enc.encode(target, {})
1175
+ enc.rules.append(f":- not {head_ref}.")
1176
+ program = "\n".join(enc.rules)
1177
+
1178
+ ctl = clingo.Control(["1"], logger=_silent)
1179
+ ctl.add("base", [], program)
1180
+ ctl.ground([("base", [])])
1181
+
1182
+ found = False
1183
+ with ctl.solve(yield_=True) as handle:
1184
+ for _model in handle:
1185
+ found = True
1186
+ break
1187
+
1188
+ block_value = (not found) if block_type == "forall" else found
1189
+
1190
+ if entry is sentence:
1191
+ return block_value
1192
+
1193
+ fresh_name = _fresh_pred_name("_so_block", _all_pred_names(sentence))
1194
+ substituted = _replace_so_block(sentence, entry, Atom(fresh_name, ()))
1195
+ ext_predicates = dict(structure.predicates)
1196
+ ext_predicates[(fresh_name, 0)] = block_value
1197
+ ext_structure = Structure(structure.domain, constants=structure.constants,
1198
+ functions=structure.functions, predicates=ext_predicates,
1199
+ sorts=structure.sorts)
1200
+ return satisfies(substituted, ext_structure, {})