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,53 @@
1
+ """Exact probabilistic logic — no sampling, no floats, every result an exact Fraction.
2
+
3
+ Two independent, complementary readings of "probabilistic first-order-ish
4
+ logic", both decided EXACTLY (rational arithmetic throughout — a probability
5
+ enters as a :class:`~fractions.Fraction` and every bound/query result leaves
6
+ as one; nothing in this package ever samples, approximates, or routes a
7
+ probability through ``float``):
8
+
9
+ - :mod:`unicode_logic_kit.prob.nilsson` — **Nilsson's probabilistic entailment**
10
+ (Nilsson, *Probabilistic Logic*, Artificial Intelligence 28, 1986). Given
11
+ probability INTERVAL constraints on a set of propositional formulas
12
+ (unconditional ``lower ≤ P(φ) ≤ upper``, or conditional
13
+ ``lower ≤ P(ψ|φ) ≤ upper``), :func:`~unicode_logic_kit.prob.nilsson.entailment_bounds`
14
+ computes the tightest interval those constraints entail for a conclusion —
15
+ an exact linear program over one probability variable per possible world,
16
+ solved with :mod:`z3`'s ``Optimize``. Classical entailment is the corner
17
+ case where every constraint pins probability 1 and the bounds collapse to
18
+ exactly ``(1, 1)``.
19
+
20
+ - :mod:`unicode_logic_kit.prob.distribution` — **Sato's distribution semantics**
21
+ (Sato, *A Statistical Learning Method for Logic Programs*, ICLP 1995), the
22
+ semantic core of ProbLog (De Raedt, Kimmig & Toivonen, IJCAI 2007). Given a
23
+ set of independent Bernoulli-distributed ground facts and a DEFINITE logic
24
+ program (rules with a single positive-atom head and a positive-atom-only
25
+ body) over a finite Herbrand universe, :func:`~unicode_logic_kit.prob.distribution.query`
26
+ sums the weight of every "total choice" of facts whose resulting (unique)
27
+ least Herbrand model satisfies a query — exactly, over ``2^k`` choices for
28
+ ``k`` relevant facts (with correctness-preserving dependency-cone pruning
29
+ cutting ``k`` down to the facts that could actually matter).
30
+
31
+ Both modules refuse loudly, with ``ValueError``, rather than silently
32
+ approximate: a quantified formula in :mod:`nilsson` (no finite space of
33
+ possible worlds), or negation/disjunctive-heads/existentials/free variables in
34
+ a :mod:`distribution` rule (outside the definite-clause fragment the
35
+ semantics is defined over) — the kit-wide convention that an unsupported
36
+ fragment is an error, never a quiet best-effort guess.
37
+
38
+ Both are PROPOSITIONAL / FINITE-DOMAIN in scope: :mod:`nilsson` evaluates over
39
+ the ``2^n`` truth-value assignments to a formula's distinct atoms (no
40
+ quantifiers at all); :mod:`distribution` grounds over the finite constants
41
+ occurring in the program and goal (no function symbols, no infinite Herbrand
42
+ universe). Both document the resulting exponential blow-up honestly and
43
+ expose an explicit, overridable brake on it (``max_atoms`` /
44
+ ``max_choice_facts``) rather than hiding a silent cutoff.
45
+ """
46
+
47
+ from .nilsson import ProbConstraint, ProbBounds, entailment_bounds
48
+ from .distribution import ProbFact, ProbProgram, query
49
+
50
+ __all__ = [
51
+ "ProbConstraint", "ProbBounds", "entailment_bounds",
52
+ "ProbFact", "ProbProgram", "query",
53
+ ]
@@ -0,0 +1,225 @@
1
+ """In-house Reduced Ordered BDD (ROBDD) engine — exact weighted model counting.
2
+
3
+ A minimal, self-contained Boolean-function representation used by
4
+ :mod:`unicode_logic_kit.prob.distribution`'s ``method="compile"`` evaluation
5
+ route (kept local, mirroring the sibling module's own "no cross-module
6
+ dependency" convention — this file has no dependency on ``distribution.py``
7
+ or ``nilsson.py``; the dependency runs the other way).
8
+
9
+ **Representation.** A node is ``(var_index, low, high)``: the Shannon
10
+ cofactors of the function when its ``var_index``-th variable is false
11
+ (``low``) and true (``high``). Two sentinel integers, :data:`FALSE` = 0 and
12
+ :data:`TRUE` = 1, are the terminal nodes; every internal node gets an integer
13
+ id >= 2. Nodes are canonicalised through a *unique table*
14
+ (:class:`BDDManager._unique`) keyed by ``(var_index, low, high)``, with the
15
+ standard ROBDD reduction rule applied on construction: ``low == high``
16
+ collapses to that shared child directly (the node is redundant — the
17
+ variable doesn't matter) rather than being allocated. Canonicalisation means
18
+ structural equality is REFERENCE (id) equality: two BDD node ids are equal
19
+ iff the Boolean functions they represent are identical, for any input order
20
+ of construction — this is what lets the least-fixpoint loop in
21
+ ``distribution.py`` detect "no change" by a plain ``!=`` on node ids.
22
+
23
+ **Fixed variable order, no dynamic reordering.** The order is whatever order
24
+ the caller assigns indices in (``distribution.py`` uses the pruned
25
+ relevant-fact list order) and never changes — this is a CORRECTNESS tool, not
26
+ a competitive weighted-model-counting engine; a bad order can blow up node
27
+ counts where a good order collapses them, and no attempt is made here to
28
+ find (or shuffle into) a good one. The :data:`max_bdd_nodes`-style brake
29
+ (``max_nodes`` below) exists precisely because worst-case Boolean functions
30
+ have exponentially many ROBDD nodes under ANY fixed order — weighted model
31
+ counting is #P-hard, so no algorithm (fixed-order ROBDDs included) escapes
32
+ that in general; this module refuses loudly rather than silently grinding.
33
+
34
+ **AND / OR** are the standard memoized recursive Shannon-expansion ``apply``:
35
+ recurse on the pair's earlier (smaller-index) top variable, combine the
36
+ cofactored results, and re-canonicalise via the unique table. **NOT** is
37
+ deliberately NOT built as ``ite(f, FALSE, TRUE)`` through that same
38
+ general two-argument machinery; it is its own single-argument memoized
39
+ recursion whose base case is exactly a 0/1-terminal swap (``NOT(FALSE) =
40
+ TRUE``, ``NOT(TRUE) = FALSE``) and whose recursive case swaps the same two
41
+ terminals arbitrarily far down the DAG, memoized per node id so any node's
42
+ negation is built (or looked up) once no matter how many times it is asked
43
+ for — cheaper, and simpler to verify correct, than routing every negation
44
+ through the ternary apply used for AND/OR.
45
+
46
+ Public API: :data:`FALSE`, :data:`TRUE`, :class:`BDDManager`,
47
+ :func:`weighted_model_count`.
48
+ """
49
+
50
+ from fractions import Fraction
51
+ from typing import Dict, List, Sequence, Tuple
52
+
53
+ __all__ = ["FALSE", "TRUE", "BDDManager", "weighted_model_count"]
54
+
55
+
56
+ FALSE: int = 0
57
+ TRUE: int = 1
58
+
59
+
60
+ class BDDManager:
61
+ """Owns one ROBDD's node universe over a fixed set of ``num_vars`` Boolean variables.
62
+
63
+ ``variable(i)`` returns the (canonical, cached) single-variable node for
64
+ variable ``i``; :meth:`AND`, :meth:`OR`, :meth:`NOT` combine existing
65
+ nodes into new canonical ones. ``max_nodes`` bounds the TOTAL node count
66
+ (the two terminals plus every internal node ever allocated) — the same
67
+ style of loud, explicit, overridable brake
68
+ :func:`~unicode_logic_kit.prob.distribution.query`'s ``max_choice_facts``
69
+ already is for the enumeration route, here for the compiled route's own
70
+ failure mode (node blow-up instead of choice-count blow-up).
71
+ """
72
+
73
+ def __init__(self, num_vars: int, *, max_nodes: int = 100_000):
74
+ if num_vars < 0:
75
+ raise ValueError(f"BDDManager: num_vars must be >= 0, got {num_vars}.")
76
+ if max_nodes < 2:
77
+ raise ValueError(f"BDDManager: max_nodes must be >= 2 (room for both terminals), got {max_nodes}.")
78
+ self.num_vars = num_vars
79
+ self.max_nodes = max_nodes
80
+ # Internal node id `n` (n >= 2) is stored at self._nodes[n - 2]; ids 0
81
+ # and 1 are the FALSE/TRUE terminals and are never stored here.
82
+ self._nodes: List[Tuple[int, int, int]] = []
83
+ self._unique: Dict[Tuple[int, int, int], int] = {}
84
+ self._and_memo: Dict[Tuple[int, int], int] = {}
85
+ self._or_memo: Dict[Tuple[int, int], int] = {}
86
+ self._not_memo: Dict[int, int] = {}
87
+ self._var_nodes: List[int] = [self._make_node(i, FALSE, TRUE) for i in range(num_vars)]
88
+
89
+ # -- construction -------------------------------------------------------
90
+
91
+ def variable(self, index: int) -> int:
92
+ """Return the canonical node for "variable ``index`` is true" alone."""
93
+ if not (0 <= index < self.num_vars):
94
+ raise ValueError(f"BDDManager: variable index {index} out of range [0, {self.num_vars}).")
95
+ return self._var_nodes[index]
96
+
97
+ def node(self, node_id: int) -> Tuple[int, int, int]:
98
+ """Return ``(var_index, low, high)`` for an internal node id (never a terminal)."""
99
+ if self.is_terminal(node_id):
100
+ raise ValueError(f"BDDManager.node: {node_id} is a terminal, not an internal node id.")
101
+ return self._nodes[node_id - 2]
102
+
103
+ def is_terminal(self, node_id: int) -> bool:
104
+ return node_id == FALSE or node_id == TRUE
105
+
106
+ def size(self) -> int:
107
+ """Total node count so far, terminals included (what ``max_nodes`` bounds)."""
108
+ return len(self._nodes) + 2
109
+
110
+ def _make_node(self, var: int, low: int, high: int) -> int:
111
+ """Canonical constructor: reduction rule, then unique-table dedup, then the brake."""
112
+ if low == high:
113
+ return low
114
+ key = (var, low, high)
115
+ existing = self._unique.get(key)
116
+ if existing is not None:
117
+ return existing
118
+ new_id = len(self._nodes) + 2
119
+ if new_id >= self.max_nodes:
120
+ raise ValueError(
121
+ f"BDDManager: node count would exceed max_nodes={self.max_nodes}. "
122
+ "Weighted model counting is #P-hard -- worst-case Boolean functions have "
123
+ "exponentially many ROBDD nodes under any fixed variable order, so no "
124
+ "algorithm escapes that blow-up in general; reduce the program, tighten "
125
+ "the goal (which tightens dependency-cone pruning), or pass a larger "
126
+ "max_bdd_nodes explicitly if that blow-up is intended."
127
+ )
128
+ self._nodes.append(key)
129
+ self._unique[key] = new_id
130
+ return new_id
131
+
132
+ def _top_var(self, node: int) -> int:
133
+ """The node's own variable index, or ``num_vars`` (past every real variable) for a terminal."""
134
+ return self.num_vars if self.is_terminal(node) else self._nodes[node - 2][0]
135
+
136
+ def _cofactor(self, node: int, var: int) -> Tuple[int, int]:
137
+ """``(node|var=False, node|var=True)``: the node's own children if it tests ``var``, else unchanged."""
138
+ if self.is_terminal(node) or self._nodes[node - 2][0] != var:
139
+ return node, node
140
+ _, low, high = self._nodes[node - 2]
141
+ return low, high
142
+
143
+ # -- Boolean combinators --------------------------------------------------
144
+
145
+ def AND(self, a: int, b: int) -> int:
146
+ """Conjunction of two nodes; memoized recursive Shannon-expansion apply."""
147
+ if a == FALSE or b == FALSE:
148
+ return FALSE
149
+ if a == TRUE:
150
+ return b
151
+ if b == TRUE:
152
+ return a
153
+ if a == b:
154
+ return a
155
+ key = (a, b) if a <= b else (b, a)
156
+ cached = self._and_memo.get(key)
157
+ if cached is not None:
158
+ return cached
159
+ top = min(self._top_var(a), self._top_var(b))
160
+ a_lo, a_hi = self._cofactor(a, top)
161
+ b_lo, b_hi = self._cofactor(b, top)
162
+ result = self._make_node(top, self.AND(a_lo, b_lo), self.AND(a_hi, b_hi))
163
+ self._and_memo[key] = result
164
+ return result
165
+
166
+ def OR(self, a: int, b: int) -> int:
167
+ """Disjunction of two nodes; memoized recursive Shannon-expansion apply."""
168
+ if a == TRUE or b == TRUE:
169
+ return TRUE
170
+ if a == FALSE:
171
+ return b
172
+ if b == FALSE:
173
+ return a
174
+ if a == b:
175
+ return a
176
+ key = (a, b) if a <= b else (b, a)
177
+ cached = self._or_memo.get(key)
178
+ if cached is not None:
179
+ return cached
180
+ top = min(self._top_var(a), self._top_var(b))
181
+ a_lo, a_hi = self._cofactor(a, top)
182
+ b_lo, b_hi = self._cofactor(b, top)
183
+ result = self._make_node(top, self.OR(a_lo, b_lo), self.OR(a_hi, b_hi))
184
+ self._or_memo[key] = result
185
+ return result
186
+
187
+ def NOT(self, a: int) -> int:
188
+ """Negation, via a dedicated memoized unary 0/1-terminal-swap recursion (see module docstring)."""
189
+ if a == FALSE:
190
+ return TRUE
191
+ if a == TRUE:
192
+ return FALSE
193
+ cached = self._not_memo.get(a)
194
+ if cached is not None:
195
+ return cached
196
+ var, low, high = self._nodes[a - 2]
197
+ result = self._make_node(var, self.NOT(low), self.NOT(high))
198
+ self._not_memo[a] = result
199
+ return result
200
+
201
+
202
+ def weighted_model_count(manager: BDDManager, root: int, weights: Sequence[Fraction]) -> Fraction:
203
+ """Exact weighted model count of ``root``, ``weights[i]`` the weight of "variable ``i`` true".
204
+
205
+ ``WMC(node) = p_v * WMC(high) + (1 - p_v) * WMC(low)``, terminals
206
+ ``TRUE -> 1``, ``FALSE -> 0``, memoized bottom-up over the node DAG so
207
+ every SHARED node is priced exactly once. That sharing is the entire
208
+ performance claim of the compiled route: a chain/tree/diamond-shaped
209
+ program collapses to ``O(#nodes)`` here instead of the ``O(2^k)`` exact
210
+ weights the enumeration route sums for the same answer. Arithmetic is
211
+ exact ``Fraction`` throughout, never a float.
212
+ """
213
+ memo: Dict[int, Fraction] = {FALSE: Fraction(0), TRUE: Fraction(1)}
214
+
215
+ def rec(node: int) -> Fraction:
216
+ cached = memo.get(node)
217
+ if cached is not None:
218
+ return cached
219
+ var, low, high = manager.node(node)
220
+ p = weights[var]
221
+ value = p * rec(high) + (Fraction(1) - p) * rec(low)
222
+ memo[node] = value
223
+ return value
224
+
225
+ return rec(root)