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,492 @@
1
+ """Classical-FOL normal forms: NNF, PNF, CNF, Skolemization, and a Horn check.
2
+
3
+ All entry points first call ``to_fol`` to eliminate sort annotations, so they
4
+ accept FOL and MSFOL inputs. Łukasiewicz (fuzzy) connectives are REJECTED with a
5
+ pointer instead of being silently collapsed: the collapse changes the logic (the
6
+ classical image of fuzzy ``P ∨ ¬P`` is valid, the fuzzy original is not), and it
7
+ used to leak wrong verdicts into ``is_valid_resolution``. Collapse explicitly
8
+ with ``to_fol(node)`` first if the classical skeleton is really wanted. Lambda
9
+ terms must be beta-reduced and lambda-eliminated first (``to_fol`` will raise
10
+ otherwise).
11
+
12
+ - ``to_nnf`` — negation normal form (eliminate → ↔ ⊕, push ¬ to atoms).
13
+ - ``to_pnf`` — prenex normal form (quantifier prefix + quantifier-free NNF
14
+ matrix), with bound variables standardised apart. Equivalence-preserving.
15
+ - ``to_cnf`` — prenex form whose matrix is a conjunction of clauses.
16
+ Equivalence-preserving.
17
+ - ``to_dnf`` — prenex form whose matrix is a disjunction of conjunctive
18
+ clauses (the dual of ``to_cnf``). Equivalence-preserving.
19
+ - ``to_tseitin_cnf`` — EQUISATISFIABLE CNF of the matrix via the Tseitin /
20
+ definitional encoding (fresh 0-ary auxiliary predicates); avoids the
21
+ exponential blow-up of the distributive ``to_cnf``. NOT equivalence-preserving.
22
+ - ``skolemize`` — prenex NNF with existentials replaced by Skolem terms over
23
+ the universals in scope; universal prefix retained. Satisfiability-preserving
24
+ (NOT equivalence-preserving).
25
+ - ``is_horn`` — True iff the clausal form (skolemise → drop ∀ → CNF) consists of
26
+ Horn clauses (each clause has at most one positive literal).
27
+ """
28
+
29
+ from .nodes import (
30
+ Node, Atom, Not, And, Or, Xor, Implies, Iff, Quantifier,
31
+ Variable, Constant, Number, Function, to_fol,
32
+ )
33
+ from ._msfl_nodes import _rename
34
+
35
+ _FORALL = ("∀", "forall")
36
+ _EXISTS = ("∃", "exists")
37
+
38
+
39
+ def _unsupported_hint(n: Node) -> str:
40
+ """A pointer at the right tool for a node the classical normal forms reject."""
41
+ from .nodes import (
42
+ Box, Diamond, Knows, Believes, Says, Wants, Obligatory, Permitted,
43
+ Next, Always, Eventually, Until, Historically, Once, Previous, Since,
44
+ Nominal, At, Would, Might, Cardinality, SortedCardinality,
45
+ SecondOrderQuantifier,
46
+ )
47
+ if isinstance(n, (Would, Might)):
48
+ return (" — the counterfactuals □→/◇→ have no first-order reading; use "
49
+ "cf_valid / cf_satisfies (sphere semantics) or "
50
+ "isabelle_decide_counterfactual.")
51
+ if isinstance(n, (Until, Since)):
52
+ return (" — Until/Since are not first-order definable (they need a "
53
+ "transitive-closure fixpoint); evaluate with satisfies_modal or "
54
+ "decide with isabelle_decide_modal.")
55
+ if isinstance(n, (Box, Diamond, Knows, Believes, Says, Wants, Obligatory,
56
+ Permitted, Next, Always, Eventually, Historically, Once,
57
+ Previous, Nominal, At)):
58
+ return (" — modal/temporal/hybrid operators have no classical rule here; "
59
+ "lower the propositional-modal fragment with "
60
+ "standard_translation, or use the modal tableau / "
61
+ "isabelle_decide_modal (is_valid_resolution and the Fitch/tableau "
62
+ "entry points route automatically).")
63
+ if isinstance(n, (Cardinality, SortedCardinality)):
64
+ return (" — set cardinality is second-order; evaluate with "
65
+ "semantics.tarski.satisfies or export to HOL via hol.secondorder.")
66
+ if isinstance(n, SecondOrderQuantifier):
67
+ return (" — second-order quantification has no sound classical "
68
+ "Skolemization; use the sequent calculus's SO rules, "
69
+ "satisfies_so, or hol.secondorder.")
70
+ from ._linear_nodes import Tensor, With, OPlus, LinearImplies, OfCourse, One
71
+ if isinstance(n, (Tensor, With, OPlus, LinearImplies, OfCourse, One)):
72
+ return (" — substructural (linear-logic) connectives have no classical "
73
+ "normal form: weakening/contraction are exactly what the collapse "
74
+ "would smuggle back in; decide derivability with atp.linear "
75
+ "(ill_prove / ill_derivable).")
76
+ from ._lambek_nodes import Product, Under, Over
77
+ if isinstance(n, (Product, Under, Over)):
78
+ return (" — Lambek-calculus connectives are order-sensitive; a classical "
79
+ "normal form would erase exactly that; decide derivability with "
80
+ "atp.lambek (lambek_prove / lambek_derivable).")
81
+ from ._team_nodes import Dependence, SlashedExists
82
+ if isinstance(n, (Dependence, SlashedExists)):
83
+ return (" — dependence/IF constructs are team-semantic (existential "
84
+ "second-order strength); evaluate with team_satisfies / "
85
+ "team_models.")
86
+ return ""
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Negation normal form
91
+ # ---------------------------------------------------------------------------
92
+
93
+ def _reject_fuzzy(node: Node, entry: str) -> None:
94
+ """Raise on Łukasiewicz input: the classical collapse changes the LOGIC.
95
+
96
+ The fuzzy connectives' ``to_msfol`` image (min/max/t-norm → And/Or/…) is a
97
+ different logic — classical ``P ∨ ¬P`` is valid, the fuzzy original is not —
98
+ so collapsing silently here leaked wrong verdicts into
99
+ ``is_valid_resolution``. The collapse stays available, explicitly:
100
+ ``to_fol(node)`` first.
101
+ """
102
+ from ..semantics._modal_reject import FUZZY_TYPES
103
+ for sub in node.walk():
104
+ if isinstance(sub, FUZZY_TYPES):
105
+ raise TypeError(
106
+ f"{entry}: {type(sub).__name__} is a Łukasiewicz connective — "
107
+ "classical normal forms (and the resolution prover on top of "
108
+ "them) would silently decide the WRONG logic. Evaluate with "
109
+ "semantics.fuzzy.evaluate or decide with "
110
+ "atp.z3_fuzzy.fuzzy_is_valid; collapse explicitly with "
111
+ "to_fol(node) first if the classical skeleton is wanted.")
112
+
113
+
114
+ def to_nnf(node: Node) -> Node:
115
+ """Return the negation normal form of node (after reducing to classical FOL).
116
+
117
+ Raises:
118
+ TypeError: on Łukasiewicz (fuzzy) input — the classical collapse changes
119
+ the logic, so it must be requested explicitly via ``to_fol(node)``.
120
+ """
121
+ _reject_fuzzy(node, "to_nnf")
122
+ return _nnf(to_fol(node))
123
+
124
+
125
+ def _nnf(n: Node) -> Node:
126
+ if isinstance(n, Atom):
127
+ return n
128
+ if isinstance(n, Not):
129
+ return _nnf_neg(n.formula)
130
+ if isinstance(n, And):
131
+ return And(_nnf(n.left), _nnf(n.right))
132
+ if isinstance(n, Or):
133
+ return Or(_nnf(n.left), _nnf(n.right))
134
+ if isinstance(n, Implies):
135
+ return Or(_nnf(Not(n.left)), _nnf(n.right))
136
+ if isinstance(n, Iff):
137
+ return And(_nnf(Implies(n.left, n.right)), _nnf(Implies(n.right, n.left)))
138
+ if isinstance(n, Xor):
139
+ return Or(And(_nnf(n.left), _nnf(Not(n.right))),
140
+ And(_nnf(Not(n.left)), _nnf(n.right)))
141
+ if isinstance(n, Quantifier):
142
+ return Quantifier(n.type, n.variable, _nnf(n.formula))
143
+ raise TypeError(
144
+ f"to_nnf: unexpected node type {type(n).__name__}{_unsupported_hint(n)}")
145
+
146
+
147
+ def _nnf_neg(n: Node) -> Node:
148
+ """Return the NNF of ¬n."""
149
+ if isinstance(n, Atom):
150
+ return Not(n)
151
+ if isinstance(n, Not):
152
+ return _nnf(n.formula) # ¬¬a → a
153
+ if isinstance(n, And):
154
+ return Or(_nnf_neg(n.left), _nnf_neg(n.right))
155
+ if isinstance(n, Or):
156
+ return And(_nnf_neg(n.left), _nnf_neg(n.right))
157
+ if isinstance(n, Implies): # ¬(a → b) ≡ a ∧ ¬b
158
+ return And(_nnf(n.left), _nnf_neg(n.right))
159
+ if isinstance(n, Iff): # ¬(a ↔ b) ≡ (a ∧ ¬b) ∨ (¬a ∧ b)
160
+ return Or(And(_nnf(n.left), _nnf_neg(n.right)),
161
+ And(_nnf_neg(n.left), _nnf(n.right)))
162
+ if isinstance(n, Xor): # ¬(a ⊕ b) ≡ a ↔ b
163
+ return _nnf(Iff(n.left, n.right))
164
+ if isinstance(n, Quantifier):
165
+ dual = "∃" if n.type in _FORALL else "∀"
166
+ return Quantifier(dual, n.variable, _nnf_neg(n.formula))
167
+ raise TypeError(
168
+ f"to_nnf: unexpected node type {type(n).__name__}{_unsupported_hint(n)}")
169
+
170
+
171
+ # ---------------------------------------------------------------------------
172
+ # Fresh-name plumbing
173
+ # ---------------------------------------------------------------------------
174
+
175
+ def _all_names(node: Node) -> set:
176
+ """Collect every predicate, function, constant, and variable name in node."""
177
+ names = set()
178
+ for n in node.walk():
179
+ cls = type(n).__name__
180
+ if cls == "Atom":
181
+ names.add(n.predicate)
182
+ elif cls == "Function":
183
+ names.add(n.name)
184
+ elif cls in ("Variable", "Constant"):
185
+ names.add(n.name)
186
+ return names
187
+
188
+
189
+ def _gensym(base: str, counter: list, used: set) -> str:
190
+ """Return a fresh name base+N not already in used; record it in used."""
191
+ while True:
192
+ name = f"{base}{counter[0]}"
193
+ counter[0] += 1
194
+ if name not in used:
195
+ used.add(name)
196
+ return name
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Prenex normal form
201
+ # ---------------------------------------------------------------------------
202
+
203
+ def to_pnf(node: Node) -> Node:
204
+ """Return the prenex normal form: quantifier prefix over a quantifier-free
205
+ NNF matrix, with all bound variables standardised apart."""
206
+ _reject_fuzzy(node, "to_pnf")
207
+ nnf = _nnf(to_fol(node))
208
+ std = _standardize(nnf, [0], _all_names(nnf))
209
+ prefix, matrix = _prenex_split(std)
210
+ for qtype, qvar in reversed(prefix):
211
+ matrix = Quantifier(qtype, qvar, matrix)
212
+ return matrix
213
+
214
+
215
+ def _standardize(n: Node, counter: list, used: set) -> Node:
216
+ """Rename every bound variable to a globally unique fresh name."""
217
+ if isinstance(n, Quantifier):
218
+ fresh = Variable(_gensym("v", counter, used))
219
+ body = _rename(n.formula, n.variable, fresh)
220
+ return Quantifier(n.type, fresh, _standardize(body, counter, used))
221
+ if isinstance(n, (And, Or)):
222
+ return type(n)(_standardize(n.left, counter, used),
223
+ _standardize(n.right, counter, used))
224
+ if isinstance(n, Not):
225
+ return Not(_standardize(n.formula, counter, used))
226
+ return n # Atom
227
+
228
+
229
+ def _prenex_split(n: Node):
230
+ """Split a standardised-apart NNF formula into (prefix, quantifier-free matrix).
231
+
232
+ prefix is a list of (quantifier_type, Variable) in outer-to-inner order.
233
+ Sound because bound variables are disjoint, so quantifiers hoist freely.
234
+ """
235
+ if isinstance(n, Quantifier):
236
+ prefix, matrix = _prenex_split(n.formula)
237
+ return [(n.type, n.variable)] + prefix, matrix
238
+ if isinstance(n, (And, Or)):
239
+ pl, ml = _prenex_split(n.left)
240
+ pr, mr = _prenex_split(n.right)
241
+ return pl + pr, type(n)(ml, mr)
242
+ return [], n # Not(atom) or Atom
243
+
244
+
245
+ # ---------------------------------------------------------------------------
246
+ # Conjunctive normal form
247
+ # ---------------------------------------------------------------------------
248
+
249
+ def to_cnf(node: Node) -> Node:
250
+ """Return prenex form whose matrix is a conjunction of clauses. Equivalence-preserving."""
251
+ pnf = to_pnf(node)
252
+ prefix, matrix = _prenex_split(pnf)
253
+ cnf_matrix = _cnf(matrix)
254
+ for qtype, qvar in reversed(prefix):
255
+ cnf_matrix = Quantifier(qtype, qvar, cnf_matrix)
256
+ return cnf_matrix
257
+
258
+
259
+ def _cnf(n: Node) -> Node:
260
+ """Distribute ∨ over ∧ in a quantifier-free NNF formula."""
261
+ if isinstance(n, (Atom, Not)):
262
+ return n
263
+ if isinstance(n, And):
264
+ return And(_cnf(n.left), _cnf(n.right))
265
+ if isinstance(n, Or):
266
+ return _distribute(_cnf(n.left), _cnf(n.right))
267
+ raise TypeError(f"to_cnf: unexpected node type {type(n).__name__}")
268
+
269
+
270
+ def _distribute(left: Node, right: Node) -> Node:
271
+ if isinstance(left, And):
272
+ return And(_distribute(left.left, right), _distribute(left.right, right))
273
+ if isinstance(right, And):
274
+ return And(_distribute(left, right.left), _distribute(left, right.right))
275
+ return Or(left, right)
276
+
277
+
278
+ # ---------------------------------------------------------------------------
279
+ # Disjunctive normal form
280
+ # ---------------------------------------------------------------------------
281
+
282
+ def to_dnf(node: Node) -> Node:
283
+ """Return prenex form whose matrix is a disjunction of conjunctive clauses.
284
+
285
+ The dual of :func:`to_cnf`: reuse the prenex prefix from ``to_pnf`` and
286
+ distribute ∧ over ∨ in the quantifier-free matrix (each conjunctive clause
287
+ is a conjunction of literals). Equivalence-preserving.
288
+ """
289
+ pnf = to_pnf(node)
290
+ prefix, matrix = _prenex_split(pnf)
291
+ dnf_matrix = _dnf(matrix)
292
+ for qtype, qvar in reversed(prefix):
293
+ dnf_matrix = Quantifier(qtype, qvar, dnf_matrix)
294
+ return dnf_matrix
295
+
296
+
297
+ def _dnf(n: Node) -> Node:
298
+ """Distribute ∧ over ∨ in a quantifier-free NNF formula (dual of _cnf)."""
299
+ if isinstance(n, (Atom, Not)):
300
+ return n
301
+ if isinstance(n, Or):
302
+ return Or(_dnf(n.left), _dnf(n.right))
303
+ if isinstance(n, And):
304
+ return _distribute_and(_dnf(n.left), _dnf(n.right))
305
+ raise TypeError(f"to_dnf: unexpected node type {type(n).__name__}")
306
+
307
+
308
+ def _distribute_and(left: Node, right: Node) -> Node:
309
+ """Distribute ∧ over ∨ (dual of _distribute)."""
310
+ if isinstance(left, Or):
311
+ return Or(_distribute_and(left.left, right), _distribute_and(left.right, right))
312
+ if isinstance(right, Or):
313
+ return Or(_distribute_and(left, right.left), _distribute_and(left, right.right))
314
+ return And(left, right)
315
+
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # Tseitin (definitional) CNF
319
+ # ---------------------------------------------------------------------------
320
+
321
+ def to_tseitin_cnf(node: Node) -> Node:
322
+ """Return an EQUISATISFIABLE CNF via the Tseitin/definitional encoding.
323
+
324
+ Unlike :func:`to_cnf`, this is *not* equivalence-preserving: a fresh 0-ary
325
+ auxiliary predicate Atom is introduced for every compound subformula of the
326
+ quantifier-free NNF matrix, and the defining biconditionals (``aux ↔
327
+ op(children)``) are expanded into clauses and conjoined with the unit clause
328
+ asserting the root auxiliary. This avoids the exponential blow-up of the
329
+ distributive :func:`to_cnf` while preserving satisfiability.
330
+
331
+ The auxiliaries are *0-ary* (propositional) definitions, so they only
332
+ faithfully capture a subformula whose truth value is fixed — i.e. the
333
+ propositional / ground case the encoding targets. They are **not** sound
334
+ inside a quantifier prefix: a single global ``aux`` cannot track a
335
+ subformula such as ``P(x)`` whose truth value varies over the domain, which
336
+ breaks equisatisfiability. This function therefore requires a quantifier-free
337
+ (after prenexing) input and raises :class:`ValueError` otherwise.
338
+ """
339
+ pnf = to_pnf(node)
340
+ prefix, matrix = _prenex_split(pnf)
341
+ if prefix:
342
+ raise ValueError(
343
+ "to_tseitin_cnf only supports quantifier-free (propositional / "
344
+ "ground) formulas: the 0-ary auxiliary predicates are not sound "
345
+ "under a quantifier prefix. Eliminate quantifiers first (e.g. via "
346
+ "skolemize and grounding) or use to_cnf for an equivalence-"
347
+ "preserving prenex CNF."
348
+ )
349
+ used = _all_names(pnf)
350
+ counter = [0]
351
+
352
+ clauses = [] # accumulated CNF clauses (each a Node)
353
+ root = _tseitin(matrix, counter, used, clauses)
354
+ clauses.append(root) # unit clause asserting the root holds
355
+
356
+ cnf_matrix = clauses[0]
357
+ for clause in clauses[1:]:
358
+ cnf_matrix = And(cnf_matrix, clause)
359
+ return cnf_matrix
360
+
361
+
362
+ def _tseitin(n: Node, counter: list, used: set, clauses: list) -> Node:
363
+ """Encode subformula n, appending its defining clauses; return its literal.
364
+
365
+ A literal (Atom or ¬Atom) is returned unchanged — it needs no auxiliary.
366
+ For each compound node a fresh 0-ary Atom ``aux`` is created and the clauses
367
+ expressing ``aux ↔ op(children)`` are appended to ``clauses``.
368
+ """
369
+ if isinstance(n, Atom):
370
+ return n
371
+ if isinstance(n, Not):
372
+ # Inputs are NNF, so this wraps an atom: it is already a literal.
373
+ return n
374
+
375
+ if isinstance(n, And):
376
+ a = _tseitin(n.left, counter, used, clauses)
377
+ b = _tseitin(n.right, counter, used, clauses)
378
+ aux = Atom(_gensym("ts", counter, used), [])
379
+ # aux ↔ (a ∧ b):
380
+ # (¬aux ∨ a) ∧ (¬aux ∨ b) ∧ (¬a ∨ ¬b ∨ aux)
381
+ clauses.append(Or(Not(aux), a))
382
+ clauses.append(Or(Not(aux), b))
383
+ clauses.append(Or(Or(_neg(a), _neg(b)), aux))
384
+ return aux
385
+
386
+ if isinstance(n, Or):
387
+ a = _tseitin(n.left, counter, used, clauses)
388
+ b = _tseitin(n.right, counter, used, clauses)
389
+ aux = Atom(_gensym("ts", counter, used), [])
390
+ # aux ↔ (a ∨ b):
391
+ # (¬aux ∨ a ∨ b) ∧ (¬a ∨ aux) ∧ (¬b ∨ aux)
392
+ clauses.append(Or(Not(aux), Or(a, b)))
393
+ clauses.append(Or(_neg(a), aux))
394
+ clauses.append(Or(_neg(b), aux))
395
+ return aux
396
+
397
+ raise TypeError(f"to_tseitin_cnf: unexpected node type {type(n).__name__}")
398
+
399
+
400
+ def _neg(lit: Node) -> Node:
401
+ """Return the negation of a literal, collapsing ¬¬Atom back to Atom."""
402
+ if isinstance(lit, Not):
403
+ return lit.formula
404
+ return Not(lit)
405
+
406
+
407
+ # ---------------------------------------------------------------------------
408
+ # Skolemization
409
+ # ---------------------------------------------------------------------------
410
+
411
+ def skolemize(node: Node) -> Node:
412
+ """Return prenex NNF with existentials replaced by Skolem terms.
413
+
414
+ Each ∃-bound variable becomes a Skolem function of the universals in scope
415
+ (a Skolem constant if there are none); the existential quantifiers are
416
+ dropped and the universal prefix is retained. Satisfiability-preserving.
417
+ """
418
+ pnf = to_pnf(node)
419
+ prefix, matrix = _prenex_split(pnf)
420
+ used = _all_names(pnf)
421
+ sk_counter = [0]
422
+
423
+ universals = [] # Variables of the ∀ seen so far, in order
424
+ kept_prefix = [] # the ∀ Variables to re-wrap
425
+ subst = {} # existential var name -> Skolem term
426
+
427
+ for qtype, qvar in prefix:
428
+ if qtype in _FORALL:
429
+ universals.append(qvar)
430
+ kept_prefix.append(qvar)
431
+ else:
432
+ fname = _gensym("sk", sk_counter, used)
433
+ term = Function(fname, list(universals)) if universals else Constant(fname)
434
+ subst[qvar.name] = term
435
+
436
+ result = matrix
437
+ for name, term in subst.items():
438
+ result = _subst_term(result, name, term)
439
+ for qvar in reversed(kept_prefix):
440
+ result = Quantifier("∀", qvar, result)
441
+ return result
442
+
443
+
444
+ def _subst_term(n: Node, varname: str, term: Node) -> Node:
445
+ """Replace every free Variable(varname) with term inside a formula/term."""
446
+ if isinstance(n, Variable):
447
+ return term if n.name == varname else n
448
+ if isinstance(n, (Constant, Number)):
449
+ return n
450
+ if isinstance(n, Quantifier):
451
+ if n.variable.name == varname:
452
+ return n # shadowed (cannot happen after standardising apart, but safe)
453
+ return Quantifier(n.type, n.variable, _subst_term(n.formula, varname, term))
454
+ # Structural: Function, Atom, Not, And, Or — substitute into every child.
455
+ return n.map_children(lambda c: _subst_term(c, varname, term))
456
+
457
+
458
+ # ---------------------------------------------------------------------------
459
+ # Horn check
460
+ # ---------------------------------------------------------------------------
461
+
462
+ def is_horn(node: Node) -> bool:
463
+ """Return True iff node's clausal form consists of Horn clauses.
464
+
465
+ Syntactic/clausal definition: skolemise, drop the universal prefix, put the
466
+ matrix into CNF, split into clauses, and check that every clause has at most
467
+ one positive literal.
468
+ """
469
+ sk = skolemize(node)
470
+ _, matrix = _prenex_split(sk)
471
+ cnf = _cnf(matrix)
472
+ for clause in _clauses(cnf):
473
+ if sum(1 for lit in clause if isinstance(lit, Atom)) > 1:
474
+ return False
475
+ return True
476
+
477
+
478
+ def _conjuncts(n: Node) -> list:
479
+ if isinstance(n, And):
480
+ return _conjuncts(n.left) + _conjuncts(n.right)
481
+ return [n]
482
+
483
+
484
+ def _disjuncts(n: Node) -> list:
485
+ if isinstance(n, Or):
486
+ return _disjuncts(n.left) + _disjuncts(n.right)
487
+ return [n]
488
+
489
+
490
+ def _clauses(cnf: Node) -> list:
491
+ """Split a CNF matrix into a list of clauses, each a list of literals."""
492
+ return [_disjuncts(c) for c in _conjuncts(cnf)]