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,372 @@
1
+ """Non-monotonic reasoning — minimal-model entailment (predicate circumscription).
2
+
3
+ Classical entailment is *monotonic*: adding premises never retracts a conclusion.
4
+ Common-sense reasoning is not — "Tweety is a bird" lets you conclude "Tweety flies"
5
+ until you learn "Tweety is a penguin". **Circumscription** (McCarthy) captures this by
6
+ believing only what holds in the **minimal** models of a theory: those that make the
7
+ *circumscribed* predicates as small (false of as many things) as possible.
8
+
9
+ ``Γ ⊨_circ φ`` (circumscribing the predicates in ``circumscribed``) holds iff ``φ`` is
10
+ true in every model of ``Γ`` that is ≤-minimal — where ``M ≤ M'`` means ``M`` and ``M'``
11
+ share a domain, constants, functions and the *non-circumscribed* (fixed) predicates,
12
+ and every circumscribed predicate's extension in ``M`` is a subset of its extension in
13
+ ``M'``. With ``circumscribed=None`` *all* predicates are minimised (closed-world-style
14
+ minimal-model semantics), so e.g. ``∅ ⊨_circ ¬P(a)``.
15
+
16
+ The search reuses the finite model finder, so it is **bounded** (domains up to
17
+ ``max_size``): ``True`` is minimal-model entailment over models within the bound, not a
18
+ proof over all (possibly infinite) models. The relation is genuinely non-monotonic —
19
+ adding premises can flip a ``True`` to ``False``.
20
+
21
+ **A free variable is a parameter.** It names ONE unknown element, the same in every premise
22
+ and in the conclusion; it is replaced, in all the formulas of a call together, by a constant
23
+ of its own name (:func:`~unicode_logic_kit.fol._free_parameters.parameterize`), and a constant
24
+ is part of the fixed part two models must share to be compared. A premise is never closed
25
+ universally: ``P(x) ⊨_circ P(alpha)`` fails (the minimal models make ``P`` true of ``x``
26
+ alone, and ``alpha`` may be another element), while ``∀x P(x) ⊨_circ P(alpha)`` holds. A
27
+ free variable that has the spelling of a constant of the problem is refused
28
+ (``NotImplementedError``): a structure holds one entry per name.
29
+
30
+ **Second-order circumscription axiom.** :func:`circumscription_formula` builds McCarthy's
31
+ textbook SO sentence for the same ``Γ ⊨_circ φ`` relation — ``T(P) ∧ ∀P'((T(P') ∧ P'⊆P) →
32
+ P⊆P')`` — so it can be checked (or exported to Isabelle/THF via
33
+ :mod:`unicode_logic_kit.hol.secondorder`) with the finite second-order evaluator
34
+ :mod:`unicode_logic_kit.semantics.secondorder` instead of the bounded model search above; the
35
+ two are cross-checked against each other in the test suite.
36
+
37
+ Public API: :func:`minimal_models`, :func:`minimal_entails`, :func:`circumscription_formula`,
38
+ :func:`circumscription_entails_so`.
39
+ """
40
+
41
+ from typing import Dict, Iterable, List, Optional, Tuple, Union
42
+
43
+ from ..fol._free_parameters import parameterize
44
+ from ..fol.nodes import (
45
+ Node, Atom, And, Implies, Quantifier, Variable, SecondOrderQuantifier,
46
+ )
47
+ from .tarski import Structure, models, _refuse_cardinality_as_individual
48
+ from .modelfinder import (
49
+ _Signature, _interpretations, _candidate_count, MAX_CANDIDATES,
50
+ )
51
+
52
+ #: A circumscribed-predicate spec: either a bare name (arity inferred from the
53
+ #: premises) or an explicit ``(name, arity)`` pair (needed when the predicate is
54
+ #: never applied in the premises, e.g. with empty premises).
55
+ CircSpec = Union[str, Tuple[str, int]]
56
+
57
+
58
+ def _func_key(functions) -> tuple:
59
+ """A hashable key for a function interpretation."""
60
+ return tuple(sorted((sig, tuple(sorted(table.items())))
61
+ for sig, table in functions.items()))
62
+
63
+
64
+ def _fixed_key(constants, functions, predicates, pred_sig, circ) -> tuple:
65
+ """The 'fixed part' (everything not minimised) two comparable models must share."""
66
+ fixed_preds = tuple(
67
+ (name, ar, frozenset(predicates.get((name, ar), frozenset())))
68
+ for (name, ar) in pred_sig if name not in circ)
69
+ return (tuple(sorted(constants.items())), _func_key(functions), fixed_preds)
70
+
71
+
72
+ def _circ_profile(predicates, pred_sig, circ) -> dict:
73
+ """The extensions of the circumscribed predicates (what minimality compares)."""
74
+ return {(name, ar): frozenset(predicates.get((name, ar), frozenset()))
75
+ for (name, ar) in pred_sig if name in circ}
76
+
77
+
78
+ def _strictly_below(prof_a: dict, prof_b: dict) -> bool:
79
+ """True iff ``prof_a`` ≤ ``prof_b`` componentwise with at least one strict ⊂."""
80
+ if any(not (prof_a[k] <= prof_b[k]) for k in prof_a):
81
+ return False
82
+ return any(prof_a[k] < prof_b[k] for k in prof_a)
83
+
84
+
85
+ def minimal_models(premises, circumscribed: Optional[set] = None,
86
+ max_size: int = 4,
87
+ max_candidates: int = MAX_CANDIDATES,
88
+ extra_signature=()) -> List[Structure]:
89
+ """Return the ≤-minimal finite models of ``premises`` up to ``max_size`` (bounded).
90
+
91
+ ``circumscribed`` names the predicates to minimise (default: every predicate).
92
+ Models are compared only within a shared domain and fixed part, so the result is
93
+ the union of the minimal models found at each domain size. ``extra_signature`` adds
94
+ formulas whose symbols the models must interpret without being constrained by them
95
+ (so a goal's constants/predicates are total in every enumerated model).
96
+
97
+ A free variable of ``premises`` and ``extra_signature`` is a parameter shared by all of
98
+ them (see the module docstring): a constant of its own name that every model
99
+ interprets, and that two models must interpret alike to be compared.
100
+
101
+ Raises:
102
+ NotImplementedError: a free variable has the spelling of a constant of the
103
+ formulas, or a cardinality term ``|{v : φ}|`` is not an operand of a comparison
104
+ with a number (see :func:`~unicode_logic_kit.semantics.tarski.satisfies`).
105
+ """
106
+ premises = list(premises)
107
+ closed, _ = parameterize(premises + list(extra_signature), after_variables=True)
108
+ _refuse_cardinality_as_individual(closed, "semantics.nonmonotonic")
109
+ sentences = closed[:len(premises)]
110
+ sig = _Signature()
111
+ for s in closed:
112
+ sig.scan(s)
113
+ pred_sig = sorted(sig.predicates)
114
+ circ = set(circumscribed) if circumscribed is not None else {n for n, _ in pred_sig}
115
+
116
+ result: List[Structure] = []
117
+ for k in range(1, max_size + 1):
118
+ if _candidate_count(sig, k) > max_candidates:
119
+ continue
120
+ domain = tuple(range(k))
121
+ found = []
122
+ for constants, functions, predicates in _interpretations(sig, domain):
123
+ structure = Structure(domain, constants=constants,
124
+ functions=functions, predicates=predicates)
125
+ if all(models(s, structure) for s in sentences):
126
+ found.append((constants, functions, predicates, structure))
127
+ # Group by fixed part; keep the circ-minimal models of each group.
128
+ groups = {}
129
+ for entry in found:
130
+ constants, functions, predicates, _ = entry
131
+ key = _fixed_key(constants, functions, predicates, pred_sig, circ)
132
+ profile = _circ_profile(predicates, pred_sig, circ)
133
+ groups.setdefault(key, []).append((entry, profile))
134
+ for grp in groups.values():
135
+ for (entry, profile) in grp:
136
+ if not any(_strictly_below(other, profile)
137
+ for (e2, other) in grp if e2 is not entry):
138
+ result.append(entry[3])
139
+ return result
140
+
141
+
142
+ def minimal_entails(premises, conclusion: Node, circumscribed: Optional[set] = None,
143
+ max_size: int = 4, max_candidates: int = MAX_CANDIDATES) -> bool:
144
+ """Return whether ``premises`` circumscriptively entail ``conclusion`` (bounded).
145
+
146
+ ``True`` iff ``conclusion`` holds in *every* minimal model of ``premises`` found up to
147
+ ``max_size`` (see :func:`minimal_models`). Non-monotonic: strengthening ``premises`` can
148
+ turn a ``True`` into a ``False``. A free variable is a parameter shared by the premises
149
+ and the conclusion (see the module docstring): the conclusion is evaluated with each
150
+ model's own element for it, so ``P(x)`` circumscribing ``P`` does not entail ``P(alpha)``.
151
+
152
+ Raises:
153
+ NotImplementedError: a free variable has the spelling of a constant of the
154
+ formulas.
155
+ """
156
+ premises = list(premises)
157
+ closed, _ = parameterize(premises + [conclusion], after_variables=True)
158
+ goal = closed[-1]
159
+ for structure in minimal_models(closed[:-1], circumscribed, max_size, max_candidates,
160
+ extra_signature=[goal]):
161
+ if not models(goal, structure):
162
+ return False
163
+ return True
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Second-order circumscription axiom (McCarthy's textbook SO formula).
168
+ # ---------------------------------------------------------------------------
169
+ #
170
+ # T(P) ∧ ∀P'((T(P') ∧ P'⊆P) → P⊆P'): T holds of P, and no relation P' that is a
171
+ # PROPER subset of P (in a model that otherwise agrees on the domain, constants,
172
+ # functions, and every FIXED — non-circumscribed — predicate, since those occur
173
+ # as the SAME free symbol in both T(P) and T(P') and so are literally the same
174
+ # extension in any one structure the SO evaluator checks) also satisfies T. This
175
+ # is exactly what minimal_models compares within one ``_fixed_key`` group at one
176
+ # domain size, which is why the two are cross-checked against each other below.
177
+ #
178
+ # Predicate substitution T(P) -> T(P') is just a global RENAME of the predicate
179
+ # symbol P to a fresh P' (same arity, so no comprehension/lambda machinery is
180
+ # needed) — reused, lazily, from the sequent calculus's predicate-substitution
181
+ # helpers (atp.sequent) to avoid re-implementing capture-avoiding renaming.
182
+
183
+ def _pred_arity(sentences: List[Node], name: str) -> Optional[int]:
184
+ """Infer ``name``'s arity from its applications in ``sentences`` (or ``None``).
185
+
186
+ Raises ValueError if ``name`` is applied at more than one arity (an
187
+ ill-formed signature for a single circumscribed predicate).
188
+ """
189
+ sig = _Signature()
190
+ for s in sentences:
191
+ sig.scan(s)
192
+ arities = {ar for (nm, ar) in sig.predicates if nm == name}
193
+ if len(arities) > 1:
194
+ raise ValueError(
195
+ f"circumscription_formula: predicate {name!r} occurs at conflicting "
196
+ f"arities {sorted(arities)} in the premises."
197
+ )
198
+ return next(iter(arities), None)
199
+
200
+
201
+ def _subset_formula(sub_name: str, sup_name: str, arity: int) -> Node:
202
+ """Build ``sub ⊆ sup``: ``∀x̄ (sub(x̄) → sup(x̄))``, or a bare Implies at arity 0."""
203
+ if arity == 0:
204
+ return Implies(Atom(sub_name, ()), Atom(sup_name, ()))
205
+ xs = tuple(Variable(f"_circ_x{i}") for i in range(arity))
206
+ body = Implies(Atom(sub_name, xs), Atom(sup_name, xs))
207
+ return _forall_chain_circ(xs, body)
208
+
209
+
210
+ def _forall_chain_circ(xs, body: Node) -> Node:
211
+ """Fold ``∀x1 ∀x2 … body`` over ``xs`` (local helper, mirrors modelfinder's style)."""
212
+ result = body
213
+ for x in reversed(xs):
214
+ result = Quantifier("∀", x, result)
215
+ return result
216
+
217
+
218
+ def _conjoin_all(sentences: List[Node]) -> Optional[Node]:
219
+ """Left-fold ``sentences`` into a binary And-tree, or ``None`` if the list is empty."""
220
+ if not sentences:
221
+ return None
222
+ result = sentences[0]
223
+ for s in sentences[1:]:
224
+ result = And(result, s)
225
+ return result
226
+
227
+
228
+ def circumscription_formula(premises, circumscribed: Iterable[CircSpec], varied: tuple = ()) -> Node:
229
+ """Build the second-order circumscription axiom for ``premises`` w.r.t. ``circumscribed``.
230
+
231
+ ``T(P̄) ∧ ∀P̄'((T(P̄') ∧ P̄'⊆P̄) → P̄⊆P̄')`` — ``T`` is the conjunction of the
232
+ ``premises``, in which a free variable is a PARAMETER (a constant of its own name,
233
+ the same in every premise; the premises are never closed universally);
234
+ ``P̄`` are the ``circumscribed`` predicates
235
+ (one or more), each replaced by a FRESH predicate ``P'`` inside a second copy
236
+ of ``T``, with that ``P'`` bound by a second-order quantifier — **universal**
237
+ (``∀P'``), since the axiom says NO strictly-smaller ``P'`` also satisfies
238
+ ``T``. Every OTHER
239
+ predicate is FIXED automatically: it occurs as the same free symbol in both
240
+ copies of ``T``, so it denotes the same extension in any one structure a
241
+ second-order evaluator checks — no extra bookkeeping is needed for the
242
+ "everything else fixed" reading.
243
+
244
+ ``circumscribed`` is a non-empty iterable of predicate specs: a bare name
245
+ (its arity is inferred from an application in ``premises``) or an explicit
246
+ ``(name, arity)`` pair (needed when the predicate is never applied in
247
+ ``premises`` — e.g. empty premises; see :func:`circumscription_entails_so`,
248
+ which also scans the conclusion for the arity).
249
+
250
+ Only ``varied=()`` (the default) is implemented: every non-circumscribed
251
+ predicate is FIXED. Passing a non-empty ``varied`` (letting some predicates
252
+ float freely — neither minimised nor held fixed) raises ``NotImplementedError``;
253
+ that reading needs a second family of second-order-quantified predicates kept
254
+ OUTSIDE the ``⊆`` comparison, which this builder does not construct.
255
+
256
+ Raises:
257
+ NotImplementedError: for a non-empty ``varied``.
258
+ ValueError: if ``circumscribed`` is empty, or a bare name's arity cannot
259
+ be inferred (never applied in ``premises``), or a predicate is
260
+ applied at conflicting arities in ``premises``.
261
+ NotImplementedError: also when a free variable has the spelling of a constant
262
+ of ``premises`` (a structure holds one entry per name).
263
+ """
264
+ if tuple(varied):
265
+ raise NotImplementedError(
266
+ "circumscription_formula: only the simple reading — every predicate "
267
+ "NOT circumscribed is FIXED — is implemented; a non-empty `varied` "
268
+ "(letting some predicates float freely, neither minimised nor held "
269
+ "fixed) needs a second family of second-order-quantified predicates "
270
+ "kept OUTSIDE the P'⊆P / P⊆P' comparison, which this builder does "
271
+ "not construct. Pass varied=() (the default)."
272
+ )
273
+ from ..atp.sequent import _rename_pred, _all_pred_names, _fresh_pred_name
274
+
275
+ sentences, _ = parameterize(list(premises), after_variables=True)
276
+
277
+ circ_list = sorted(circumscribed, key=lambda e: e[0] if isinstance(e, tuple) else e)
278
+ if not circ_list:
279
+ raise ValueError("circumscription_formula: `circumscribed` must name at least one predicate.")
280
+
281
+ resolved: List[Tuple[str, int]] = []
282
+ for entry in circ_list:
283
+ if isinstance(entry, tuple):
284
+ name, arity = entry
285
+ else:
286
+ name = entry
287
+ arity = _pred_arity(sentences, name)
288
+ if arity is None:
289
+ raise ValueError(
290
+ f"circumscription_formula: cannot infer the arity of "
291
+ f"circumscribed predicate {name!r} — it is never applied in "
292
+ "`premises`. Pass an explicit (name, arity) pair in "
293
+ "`circumscribed`, or build the axiom via "
294
+ "circumscription_entails_so(premises, circumscribed, "
295
+ "conclusion), which also scans the conclusion for the arity."
296
+ )
297
+ resolved.append((name, arity))
298
+
299
+ avoid = {name for name, _ in resolved}
300
+ for s in sentences:
301
+ avoid |= _all_pred_names(s)
302
+
303
+ primed: Dict[str, str] = {}
304
+ for name, _ in resolved:
305
+ p2 = _fresh_pred_name(name, avoid)
306
+ avoid.add(p2)
307
+ primed[name] = p2
308
+
309
+ conj = _conjoin_all(sentences)
310
+ conj_primed = conj
311
+ if conj_primed is not None:
312
+ for name, _ in resolved:
313
+ conj_primed = _rename_pred(conj_primed, name, primed[name])
314
+
315
+ le_primed_orig = _conjoin_all([_subset_formula(primed[n], n, ar) for n, ar in resolved])
316
+ le_orig_primed = _conjoin_all([_subset_formula(n, primed[n], ar) for n, ar in resolved])
317
+
318
+ antecedent = le_primed_orig if conj_primed is None else And(conj_primed, le_primed_orig)
319
+ minimality = Implies(antecedent, le_orig_primed)
320
+
321
+ quantified = minimality
322
+ for name, arity in resolved:
323
+ quantified = SecondOrderQuantifier("∀", primed[name], arity, quantified)
324
+
325
+ return quantified if conj is None else And(conj, quantified)
326
+
327
+
328
+ def circumscription_entails_so(premises, circumscribed: Iterable[CircSpec], conclusion: Node) -> Node:
329
+ """Build ``circumscription_formula(premises, circumscribed) → conclusion`` (a closed sentence).
330
+
331
+ The caller decides validity — e.g. with
332
+ :func:`unicode_logic_kit.semantics.secondorder.so_is_valid_finite` (bounded, but
333
+ the natural finite oracle for a second-order sentence) — or exports the
334
+ result with :mod:`unicode_logic_kit.hol.secondorder`. This function only BUILDS
335
+ the implication node; it never evaluates it.
336
+
337
+ Unlike :func:`circumscription_formula`, a bare predicate name in
338
+ ``circumscribed`` has its arity inferred from BOTH ``premises`` and
339
+ ``conclusion`` — so e.g. ``circumscription_entails_so([], {"P"}, ¬P(b))``
340
+ (empty premises, ``P``'s arity known only from the conclusion) works.
341
+
342
+ A free variable is a PARAMETER shared by the premises and the conclusion: it is
343
+ replaced, in all of them together, by a constant of its own name before the axiom
344
+ is built, so the sentence is closed and says what ``minimal_entails`` decides
345
+ (``P(x)`` circumscribing ``P`` does not entail ``P(alpha)``).
346
+
347
+ Raises:
348
+ ValueError: a bare predicate name is never applied in the premises or the
349
+ conclusion.
350
+ NotImplementedError: a free variable has the spelling of a constant of the
351
+ formulas.
352
+ """
353
+ premises = list(premises)
354
+ parameterized, _ = parameterize(premises + [conclusion], after_variables=True)
355
+ sentences, closed_conclusion = parameterized[:-1], parameterized[-1]
356
+
357
+ resolved_circumscribed: List[Tuple[str, int]] = []
358
+ for entry in circumscribed:
359
+ if isinstance(entry, tuple):
360
+ resolved_circumscribed.append(entry)
361
+ continue
362
+ arity = _pred_arity(sentences + [closed_conclusion], entry)
363
+ if arity is None:
364
+ raise ValueError(
365
+ f"circumscription_entails_so: predicate {entry!r} is never "
366
+ "applied in the premises or the conclusion, so its arity "
367
+ "cannot be inferred."
368
+ )
369
+ resolved_circumscribed.append((entry, arity))
370
+
371
+ axiom = circumscription_formula(sentences, resolved_circumscribed)
372
+ return Implies(axiom, closed_conclusion)