binary-decision-diagram 4.0.0 → 5.0.0

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 (185) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +211 -31
  3. package/dist/cjs/src/abstract-node.js +7 -4
  4. package/dist/cjs/src/abstract-node.js.map +1 -1
  5. package/dist/cjs/src/branches.d.ts +1 -0
  6. package/dist/cjs/src/branches.js +2 -1
  7. package/dist/cjs/src/branches.js.map +1 -1
  8. package/dist/cjs/src/create-bdd-from-truth-table.js.map +1 -1
  9. package/dist/cjs/src/index.d.ts +2 -1
  10. package/dist/cjs/src/index.es5.js +17 -7
  11. package/dist/cjs/src/index.es5.js.map +1 -1
  12. package/dist/cjs/src/index.js +2 -1
  13. package/dist/cjs/src/index.js.map +1 -1
  14. package/dist/cjs/src/internal-node.js +6 -3
  15. package/dist/cjs/src/internal-node.js.map +1 -1
  16. package/dist/cjs/src/optimize-brute-force.js +5 -7
  17. package/dist/cjs/src/optimize-brute-force.js.map +1 -1
  18. package/dist/cjs/src/ordering/build-bdd.d.ts +13 -0
  19. package/dist/cjs/src/ordering/build-bdd.js +64 -0
  20. package/dist/cjs/src/ordering/build-bdd.js.map +1 -0
  21. package/dist/cjs/src/ordering/dense-truth-table.d.ts +42 -0
  22. package/dist/cjs/src/ordering/dense-truth-table.js +125 -0
  23. package/dist/cjs/src/ordering/dense-truth-table.js.map +1 -0
  24. package/dist/cjs/src/ordering/exact-cost-ordering.d.ts +91 -0
  25. package/dist/cjs/src/ordering/exact-cost-ordering.js +351 -0
  26. package/dist/cjs/src/ordering/exact-cost-ordering.js.map +1 -0
  27. package/dist/cjs/src/ordering/exact-ordering.d.ts +89 -0
  28. package/dist/cjs/src/ordering/exact-ordering.js +260 -0
  29. package/dist/cjs/src/ordering/exact-ordering.js.map +1 -0
  30. package/dist/cjs/src/ordering/index.d.ts +22 -0
  31. package/dist/cjs/src/ordering/index.js +38 -0
  32. package/dist/cjs/src/ordering/index.js.map +1 -0
  33. package/dist/cjs/src/ordering/int-pair-map.d.ts +20 -0
  34. package/dist/cjs/src/ordering/int-pair-map.js +85 -0
  35. package/dist/cjs/src/ordering/int-pair-map.js.map +1 -0
  36. package/dist/cjs/src/ordering/local-search.d.ts +114 -0
  37. package/dist/cjs/src/ordering/local-search.js +317 -0
  38. package/dist/cjs/src/ordering/local-search.js.map +1 -0
  39. package/dist/cjs/src/ordering/optimize-ordering.d.ts +153 -0
  40. package/dist/cjs/src/ordering/optimize-ordering.js +406 -0
  41. package/dist/cjs/src/ordering/optimize-ordering.js.map +1 -0
  42. package/dist/cjs/src/ordering/ordering-evaluator.d.ts +198 -0
  43. package/dist/cjs/src/ordering/ordering-evaluator.js +384 -0
  44. package/dist/cjs/src/ordering/ordering-evaluator.js.map +1 -0
  45. package/dist/cjs/src/ordering/shared-diagram.d.ts +85 -0
  46. package/dist/cjs/src/ordering/shared-diagram.js +295 -0
  47. package/dist/cjs/src/ordering/shared-diagram.js.map +1 -0
  48. package/dist/cjs/src/packed/index.d.ts +2 -0
  49. package/dist/cjs/src/{minimal-string → packed}/index.js +2 -5
  50. package/dist/cjs/src/packed/index.js.map +1 -0
  51. package/dist/cjs/src/packed/packed-bdd-base64-string.d.ts +3 -0
  52. package/dist/cjs/src/packed/packed-bdd-base64-string.js +46 -0
  53. package/dist/cjs/src/packed/packed-bdd-base64-string.js.map +1 -0
  54. package/dist/cjs/src/packed/packed-bdd.d.ts +13 -0
  55. package/dist/cjs/src/packed/packed-bdd.js +70 -0
  56. package/dist/cjs/src/packed/packed-bdd.js.map +1 -0
  57. package/dist/cjs/src/root-node.d.ts +1 -2
  58. package/dist/cjs/src/root-node.js +1 -5
  59. package/dist/cjs/src/root-node.js.map +1 -1
  60. package/dist/cjs/src/types.d.ts +0 -12
  61. package/dist/cjs/src/util.js +14 -5
  62. package/dist/cjs/src/util.js.map +1 -1
  63. package/dist/esm/src/abstract-node.js +7 -4
  64. package/dist/esm/src/abstract-node.js.map +1 -1
  65. package/dist/esm/src/branches.d.ts +1 -0
  66. package/dist/esm/src/branches.js +1 -0
  67. package/dist/esm/src/branches.js.map +1 -1
  68. package/dist/esm/src/create-bdd-from-truth-table.js.map +1 -1
  69. package/dist/esm/src/index.d.ts +2 -1
  70. package/dist/esm/src/index.js +2 -1
  71. package/dist/esm/src/index.js.map +1 -1
  72. package/dist/esm/src/internal-node.js +7 -4
  73. package/dist/esm/src/internal-node.js.map +1 -1
  74. package/dist/esm/src/optimize-brute-force.js +5 -7
  75. package/dist/esm/src/optimize-brute-force.js.map +1 -1
  76. package/dist/esm/src/ordering/build-bdd.d.ts +13 -0
  77. package/dist/esm/src/ordering/build-bdd.js +61 -0
  78. package/dist/esm/src/ordering/build-bdd.js.map +1 -0
  79. package/dist/esm/src/ordering/dense-truth-table.d.ts +42 -0
  80. package/dist/esm/src/ordering/dense-truth-table.js +117 -0
  81. package/dist/esm/src/ordering/dense-truth-table.js.map +1 -0
  82. package/dist/esm/src/ordering/exact-cost-ordering.d.ts +91 -0
  83. package/dist/esm/src/ordering/exact-cost-ordering.js +347 -0
  84. package/dist/esm/src/ordering/exact-cost-ordering.js.map +1 -0
  85. package/dist/esm/src/ordering/exact-ordering.d.ts +89 -0
  86. package/dist/esm/src/ordering/exact-ordering.js +256 -0
  87. package/dist/esm/src/ordering/exact-ordering.js.map +1 -0
  88. package/dist/esm/src/ordering/index.d.ts +22 -0
  89. package/dist/esm/src/ordering/index.js +17 -0
  90. package/dist/esm/src/ordering/index.js.map +1 -0
  91. package/dist/esm/src/ordering/int-pair-map.d.ts +20 -0
  92. package/dist/esm/src/ordering/int-pair-map.js +81 -0
  93. package/dist/esm/src/ordering/int-pair-map.js.map +1 -0
  94. package/dist/esm/src/ordering/local-search.d.ts +114 -0
  95. package/dist/esm/src/ordering/local-search.js +308 -0
  96. package/dist/esm/src/ordering/local-search.js.map +1 -0
  97. package/dist/esm/src/ordering/optimize-ordering.d.ts +153 -0
  98. package/dist/esm/src/ordering/optimize-ordering.js +399 -0
  99. package/dist/esm/src/ordering/optimize-ordering.js.map +1 -0
  100. package/dist/esm/src/ordering/ordering-evaluator.d.ts +198 -0
  101. package/dist/esm/src/ordering/ordering-evaluator.js +377 -0
  102. package/dist/esm/src/ordering/ordering-evaluator.js.map +1 -0
  103. package/dist/esm/src/ordering/shared-diagram.d.ts +85 -0
  104. package/dist/esm/src/ordering/shared-diagram.js +289 -0
  105. package/dist/esm/src/ordering/shared-diagram.js.map +1 -0
  106. package/dist/esm/src/packed/index.d.ts +2 -0
  107. package/dist/esm/src/packed/index.js +3 -0
  108. package/dist/esm/src/packed/index.js.map +1 -0
  109. package/dist/esm/src/packed/packed-bdd-base64-string.d.ts +3 -0
  110. package/dist/esm/src/packed/packed-bdd-base64-string.js +42 -0
  111. package/dist/esm/src/packed/packed-bdd-base64-string.js.map +1 -0
  112. package/dist/esm/src/packed/packed-bdd.d.ts +13 -0
  113. package/dist/esm/src/packed/packed-bdd.js +66 -0
  114. package/dist/esm/src/packed/packed-bdd.js.map +1 -0
  115. package/dist/esm/src/root-node.d.ts +1 -2
  116. package/dist/esm/src/root-node.js +1 -5
  117. package/dist/esm/src/root-node.js.map +1 -1
  118. package/dist/esm/src/types.d.ts +0 -12
  119. package/dist/esm/src/util.js +14 -5
  120. package/dist/esm/src/util.js.map +1 -1
  121. package/docs/variable-ordering.md +306 -0
  122. package/package.json +11 -9
  123. package/src/abstract-node.ts +7 -5
  124. package/src/branches.ts +2 -0
  125. package/src/create-bdd-from-truth-table.ts +1 -1
  126. package/src/index.ts +2 -1
  127. package/src/internal-node.ts +7 -4
  128. package/src/optimize-brute-force.ts +5 -7
  129. package/src/ordering/build-bdd.ts +70 -0
  130. package/src/ordering/dense-truth-table.ts +161 -0
  131. package/src/ordering/exact-cost-ordering.ts +425 -0
  132. package/src/ordering/exact-ordering.ts +317 -0
  133. package/src/ordering/index.ts +57 -0
  134. package/src/ordering/int-pair-map.ts +85 -0
  135. package/src/ordering/local-search.ts +369 -0
  136. package/src/ordering/optimize-ordering.ts +570 -0
  137. package/src/ordering/ordering-evaluator.ts +479 -0
  138. package/src/ordering/shared-diagram.ts +310 -0
  139. package/src/packed/index.ts +2 -0
  140. package/src/packed/packed-bdd-base64-string.ts +47 -0
  141. package/src/packed/packed-bdd.ts +98 -0
  142. package/src/root-node.ts +2 -8
  143. package/src/types.ts +0 -18
  144. package/src/util.ts +15 -6
  145. package/dist/cjs/src/minimal-string/bdd-to-minimal-string.d.ts +0 -8
  146. package/dist/cjs/src/minimal-string/bdd-to-minimal-string.js +0 -59
  147. package/dist/cjs/src/minimal-string/bdd-to-minimal-string.js.map +0 -1
  148. package/dist/cjs/src/minimal-string/bdd-to-simple-bdd.d.ts +0 -11
  149. package/dist/cjs/src/minimal-string/bdd-to-simple-bdd.js +0 -23
  150. package/dist/cjs/src/minimal-string/bdd-to-simple-bdd.js.map +0 -1
  151. package/dist/cjs/src/minimal-string/index.d.ts +0 -5
  152. package/dist/cjs/src/minimal-string/index.js.map +0 -1
  153. package/dist/cjs/src/minimal-string/minimal-string-to-simple-bdd.d.ts +0 -2
  154. package/dist/cjs/src/minimal-string/minimal-string-to-simple-bdd.js +0 -57
  155. package/dist/cjs/src/minimal-string/minimal-string-to-simple-bdd.js.map +0 -1
  156. package/dist/cjs/src/minimal-string/resolve-with-simple-bdd.d.ts +0 -2
  157. package/dist/cjs/src/minimal-string/resolve-with-simple-bdd.js +0 -18
  158. package/dist/cjs/src/minimal-string/resolve-with-simple-bdd.js.map +0 -1
  159. package/dist/cjs/src/minimal-string/string-format.d.ts +0 -9
  160. package/dist/cjs/src/minimal-string/string-format.js +0 -61
  161. package/dist/cjs/src/minimal-string/string-format.js.map +0 -1
  162. package/dist/esm/src/minimal-string/bdd-to-minimal-string.d.ts +0 -8
  163. package/dist/esm/src/minimal-string/bdd-to-minimal-string.js +0 -55
  164. package/dist/esm/src/minimal-string/bdd-to-minimal-string.js.map +0 -1
  165. package/dist/esm/src/minimal-string/bdd-to-simple-bdd.d.ts +0 -11
  166. package/dist/esm/src/minimal-string/bdd-to-simple-bdd.js +0 -19
  167. package/dist/esm/src/minimal-string/bdd-to-simple-bdd.js.map +0 -1
  168. package/dist/esm/src/minimal-string/index.d.ts +0 -5
  169. package/dist/esm/src/minimal-string/index.js +0 -6
  170. package/dist/esm/src/minimal-string/index.js.map +0 -1
  171. package/dist/esm/src/minimal-string/minimal-string-to-simple-bdd.d.ts +0 -2
  172. package/dist/esm/src/minimal-string/minimal-string-to-simple-bdd.js +0 -54
  173. package/dist/esm/src/minimal-string/minimal-string-to-simple-bdd.js.map +0 -1
  174. package/dist/esm/src/minimal-string/resolve-with-simple-bdd.d.ts +0 -2
  175. package/dist/esm/src/minimal-string/resolve-with-simple-bdd.js +0 -15
  176. package/dist/esm/src/minimal-string/resolve-with-simple-bdd.js.map +0 -1
  177. package/dist/esm/src/minimal-string/string-format.d.ts +0 -9
  178. package/dist/esm/src/minimal-string/string-format.js +0 -54
  179. package/dist/esm/src/minimal-string/string-format.js.map +0 -1
  180. package/src/minimal-string/bdd-to-minimal-string.ts +0 -72
  181. package/src/minimal-string/bdd-to-simple-bdd.ts +0 -31
  182. package/src/minimal-string/index.ts +0 -5
  183. package/src/minimal-string/minimal-string-to-simple-bdd.ts +0 -67
  184. package/src/minimal-string/resolve-with-simple-bdd.ts +0 -23
  185. package/src/minimal-string/string-format.ts +0 -67
@@ -0,0 +1,306 @@
1
+ # Deterministic variable ordering for binary decision diagrams
2
+
3
+ The size of a (reduced, ordered) binary decision diagram depends heavily on the
4
+ order in which the boolean functions are tested. For the same truth table one
5
+ ordering can need ten times as many nodes as another, and finding the best
6
+ ordering is NP-complete (Bollig and Wegener, 1996).
7
+
8
+ Before `optimizeOrdering()` was added, this library only offered `optimizeBruteForce()`, which shuffles
9
+ the ordering randomly, builds the whole decision tree, minimizes it and keeps
10
+ the best result. That works, but it is slow (every iteration builds a tree with
11
+ `2^(n+1)` node objects), it never knows when it is done, and two runs give two
12
+ different results.
13
+
14
+ `optimizeOrdering()` replaces this with a deterministic method. It is
15
+ implementation independent: everything it needs is the truth table, so the
16
+ same algorithm can be used as a pre-pass by any BDD implementation in any
17
+ language. This document describes the method so that it can be ported.
18
+
19
+ None of the underlying ideas are new: the method combines results from the
20
+ BDD literature of the 1990s and 2000s (see section 9) and applies them to a
21
+ function that is given as a truth table.
22
+
23
+ ## 1. Evaluate orderings without building a BDD
24
+
25
+ The central building block is `OrderingEvaluator.evaluate(ordering)`, which
26
+ returns the exact number of nodes of the reduced BDD for an ordering in
27
+ `O(2^n)` integer operations and without creating a single node object.
28
+
29
+ The `2^n` rows are the leaves of the decision tree. A *block* on level `k` is
30
+ the set of rows that agree on the boolean functions above level `k`; the
31
+ reduced BDD is built bottom-up, level by level, by merging the two child
32
+ blocks of every block:
33
+
34
+ ```
35
+ ids[n] = terminal id of every row
36
+ for level k = n-1 down to 0:
37
+ for every block b of level k:
38
+ (lo, hi) = ids[k+1] of the two children of b (function ordering[k] = 0 / 1)
39
+ if lo == DONT_CARE: id = hi // don't-care collapse
40
+ elif hi == DONT_CARE: id = lo
41
+ elif lo == hi: id = lo // reduction rule
42
+ else: id = hashcons(lo, hi) // elimination rule
43
+ ids[k][b] = id
44
+ nodes[k] = number of ids created by hashcons on this level
45
+ ```
46
+
47
+ Blocks are indexed by the assignment to the functions above them in their
48
+ natural (index) order, not in level order. The blocks of a level therefore only
49
+ depend on the *set* of functions above it, which is the key to the cheap local
50
+ search below.
51
+
52
+ `hashcons` returns the same id for the same `(lo, hi)` pair, so isomorphic
53
+ sub-diagrams share one node. Equal children are not represented by a node at
54
+ all. Therefore the number of ids created on a level is exactly the number of
55
+ nodes of the reduced BDD that test the boolean function of that level, and
56
+ their sum (plus the root and the leaves) is what `RootNode.countNodes()`
57
+ returns after `minimize()`. The test suite checks this equality against the
58
+ library's own pipeline on random tables.
59
+
60
+ Don't-care rows (rows that can never occur at runtime, see
61
+ `removeIrrelevantLeafNodes()`) are handled in the same pass: a branch that is
62
+ entirely don't-care takes over its sibling branch, exactly what
63
+ `removeIrrelevantLeafNodes()` does on the node structure. It can be shown by
64
+ induction that every partially defined block becomes either fully don't-care
65
+ or fully defined this way, so the resulting completion is unique for an
66
+ ordering and `complete(ordering)` returns the truth table with the don't-care
67
+ rows resolved. The optimizer returns this completed table together with the
68
+ BDD.
69
+
70
+ `OrderingCursor` keeps the `ids[k]` arrays of an ordering. When the functions
71
+ of two levels are swapped, the levels below the lower one keep their blocks and
72
+ ids (same sets of functions above them), only the levels from the lower one up
73
+ to the root are recomputed. Moving a function from the top to the bottom with
74
+ adjacent swaps therefore costs `O(2^n)` in total instead of `O(n * 2^n)`. For
75
+ the 17 variable table of the test suite a full evaluation takes about two
76
+ milliseconds and a whole sifting round well under a second; the old approach
77
+ needs seconds per ordering.
78
+
79
+ ## 2. Deterministic local search
80
+
81
+ With evaluations this cheap, classic dynamic reordering techniques can be run
82
+ directly on the truth table:
83
+
84
+ 1. **Seeds.** Four deterministic start orderings: the natural order, the
85
+ reversed order, a greedy top-down construction (at each level pick the
86
+ boolean function that gives the smallest BDD with the remaining functions
87
+ in index order) and the analogous greedy bottom-up construction. Users can
88
+ add their own via `initialOrderings`.
89
+ 2. **Sifting** (Rudell, 1993). Each boolean function, largest level first, is
90
+ moved through all positions with adjacent swaps and is placed where the BDD
91
+ is smallest. Only strict improvements are accepted, so the procedure always
92
+ terminates.
93
+ 3. **Window permutation.** All permutations of every window of three adjacent
94
+ levels are tried.
95
+
96
+ Sifting and window permutation alternate until a full round does not improve
97
+ anything. Ties are broken by index, no randomness is involved anywhere, so the
98
+ same truth table always gives the same ordering.
99
+
100
+ ## 3. Exact search in the subset lattice
101
+
102
+ The number of nodes on a level of a reduced BDD depends only on the *set* of
103
+ boolean functions above that level, not on their order: it is the number of
104
+ distinct cofactors with respect to that set which still depend on the level's
105
+ function (Friedman and Supowit, 1990). Hence
106
+
107
+ ```
108
+ size(ordering) = sum over levels k of cost(S_k, x_k)
109
+ ```
110
+
111
+ where `S_k` is the set of functions above level `k` and `x_k` the function on
112
+ level `k`. The optimal ordering is a shortest path from the empty set to the
113
+ full set through the lattice of variable subsets, where the edge `S -> S + {x}`
114
+ costs `cost(S, x)`.
115
+
116
+ `ExactOrderingSearch` finds that shortest path with an A* search:
117
+
118
+ - Every subset `S` is represented by the set of its distinct non-constant
119
+ cofactors. All cofactors are nodes of one shared, hash-consed diagram in the
120
+ natural variable order, so two cofactors are the same function if and only
121
+ if they have the same id. Going from `S` to `S + {x}` restricts every
122
+ cofactor on `x = 0` and `x = 1` and removes duplicates.
123
+ - `cost(S, x)` is the number of cofactors of `S` whose support contains `x`
124
+ (support masks are stored per node).
125
+ - The estimate for the remaining cost of a subset is the number of its distinct
126
+ non-constant cofactors (each needs its own node below) or the number of
127
+ remaining relevant functions, whichever is larger. Both are lower bounds, so
128
+ the first time the full set is taken from the queue its cost is optimal.
129
+ - The best ordering of the local search is an upper bound: subsets that cannot
130
+ beat it are never queued. With a tight bound the search visits a small
131
+ fraction of the `2^n` subsets. For the 17 variable test table the searches
132
+ on the four candidate completions (see below) expand about 48 000 subsets in
133
+ total, about 12 000 of the 131 072 per completion, and prove that the 77 node
134
+ ordering found by the local search is optimal. The whole run takes a few
135
+ seconds.
136
+ - Budgets make the search give up gracefully, the result then says
137
+ `optimal: false`: a limit on expanded subsets, on stored cofactor sets and
138
+ on the nodes of the shared diagram (`restrict` creates nodes for the
139
+ restricted functions, which adds up on unstructured functions).
140
+ - By default the exact search only runs when the local search found a BDD with
141
+ at most 5000 internal nodes. Unstructured functions have big BDDs, every
142
+ ordering is about equally bad for them, and the lower bound prunes almost
143
+ nothing, so the search would be slow for no gain. `exact: true` forces it.
144
+
145
+ Don't-care rows make the function depend on the ordering, so the exact search
146
+ runs on completed tables: the completion of the best ordering and the
147
+ completions the local search came across are candidates, and whenever an exact
148
+ run finds a better ordering, the completion of that ordering becomes a new
149
+ candidate. `optimal: true` means: for the returned (completed) truth table no
150
+ ordering gives fewer nodes.
151
+
152
+ ## 4. Cheapest resolve() calls instead of fewest nodes
153
+
154
+ The boolean functions behind a BDD rarely cost the same to call: one reads a
155
+ flag, another sorts a result set. What matters at runtime is then not the
156
+ amount of nodes but the cost of the functions that are actually called on the
157
+ way from the root to the leaf. With `functionCosts` the optimizer minimizes
158
+ that:
159
+
160
+ ```
161
+ expectedCost(ordering) = sum over levels k of cost(x_k) * P_k
162
+ ```
163
+
164
+ where `P_k` is the share of the (weighted) rows whose path visits a node on
165
+ level `k`. A row visits a node on level `k` exactly when its cofactor with
166
+ respect to the functions above still depends on `x_k`; the root node always
167
+ calls its function. The bottom-up pass of section 1 computes `P_k` for free by
168
+ carrying the row weights along: whenever a pair of children becomes a node, the
169
+ weight of its block is added to the level. By default every row that is not a
170
+ don't-care row counts the same, `rowWeight` gives rows individual frequencies.
171
+
172
+ The local search of section 2 works unchanged with the new objective (fewer
173
+ nodes only break ties between equal costs). The exact search of section 3 works too, because the
174
+ expected cost decomposes over the subset lattice the same way: the edge
175
+ `S -> S + {x}` costs `cost(x)` times the weight of the rows whose cofactor with
176
+ respect to `S` depends on `x`. A state of `ExactCostOrderingSearch` keeps the
177
+ rows with positive weight grouped by cofactor (rows whose cofactor became
178
+ constant are dropped), the admissible estimate for the rest of the path is,
179
+ for every function not yet placed, its cost times the weight of the rows that
180
+ are sensitive to it, because those rows pay for it in every ordering.
181
+
182
+ The exact cost search only minimizes the cost. Its result is polished with
183
+ the local search on the same completion, which can only find orderings with
184
+ the same cost and fewer nodes; that tie-break is best effort and not part of
185
+ the proof.
186
+
187
+ On the 17 variable test table with the sample costs
188
+ `[1, 5, 1, 20, 1, 1, 50, 2, 1, 1, 10, 1, 1, 3, 1, 100, 1]`:
189
+
190
+ | ordering | nodes | expected cost per resolve() |
191
+ | --- | --- | --- |
192
+ | natural order | 187 | 122.6 |
193
+ | fewest nodes (`optimizeOrdering` without costs) | 77 | 112.5 |
194
+ | cheapest calls (`optimizeOrdering` with `functionCosts`) | 154 | **38.3**, proven optimal |
195
+
196
+ The expensive functions move to the bottom where only the rows that really
197
+ need them pay for them. The cost reported by the optimizer equals the cost
198
+ measured by calling `resolve()` with counting resolver functions on every row.
199
+
200
+ ## 5. Building the BDD
201
+
202
+ The best ordering is turned into the library's node structure directly from the
203
+ reduced diagram (`createBddWithOrdering()`), so the unreduced tree with
204
+ `2^(n+1)` nodes is never built and no `minimize()` call is needed. The result
205
+ has the same shape as the BDDs of `optimizeBruteForce()`: the level of a node
206
+ is the index of the boolean function it tests, and `bddToPackedBdd()`
207
+ works as before.
208
+
209
+ ## 6. Results
210
+
211
+ Numbers from an ordinary laptop, `npm run build` of this repository.
212
+ "Brute force" is `optimizeBruteForce()` with the given number of iterations,
213
+ "deterministic" is `optimizeOrdering()` with default options.
214
+
215
+ | truth table | variables | natural order | brute force | deterministic |
216
+ | --- | --- | --- | --- | --- |
217
+ | event-reduce table from the test suite (576 defined rows, rest don't-care) | 17 | 187 nodes | 112 nodes, 150 iterations, 351 s | **77 nodes, proven optimal, 4 s** |
218
+ | structured, 50 % don't-care | 8 | 60 | 36 (100 iterations, 1.5 s) | **30, proven optimal, 0.04 s** |
219
+ | structured, 50 % don't-care | 10 | 156 | 33 (60 iterations, 7.4 s) | **31, proven optimal, 0.07 s** |
220
+ | structured, 70 % don't-care | 12 | 344 | 78 (25 iterations, 16.8 s) | **30, proven optimal, 0.16 s** |
221
+ | uniformly random values | 8 | 263 | 263 (100 iterations, 2.7 s) | 263, proven optimal, 0.01 s |
222
+ | uniformly random values | 10 | 1023 | 1023 (60 iterations, 25 s) | 1023, proven optimal, 0.09 s |
223
+
224
+ Uniformly random functions have no structure to exploit, every ordering is
225
+ equally bad; the exact search proves that in milliseconds instead of leaving
226
+ the brute force running forever.
227
+
228
+ ## 7. Porting the method to another BDD implementation
229
+
230
+ Only `evaluate()` (section 1) and `restrict`/`cost` (section 3) touch the
231
+ function representation, and both only need the truth table. Everything else
232
+ is plain integer arithmetic. A port needs:
233
+
234
+ 1. the truth table as a flat array indexed by the row bits,
235
+ 2. the bottom-up hash-consing pass of section 1 with one id array per level
236
+ (plus one weight array per level for the cost objective of section 4),
237
+ 3. level swaps that recompute only the levels above the swapped ones,
238
+ 4. the local search of section 2,
239
+ 5. optionally the A* searches of sections 3 and 4, which need a hash-consed
240
+ diagram with `restrict` and per-node support masks.
241
+
242
+ The code in `src/ordering/` is written to be read in that order.
243
+
244
+ ## 8. Limitations and further work
245
+
246
+ - The truth table must be complete (`2^n` rows), so the method is for
247
+ functions with up to about 21 to 23 boolean functions (21 take about half a
248
+ minute and 0.5 GB, 23 about three minutes and 1.7 GB). Dynamic reordering
249
+ inside a BDD package stays the tool for larger functions.
250
+ - Don't-care rows are resolved greedily (a don't-care branch takes its sibling
251
+ branch, as `removeIrrelevantLeafNodes()` does). Choosing the completion that
252
+ gives the globally smallest BDD is itself NP-hard; the alternation between
253
+ completion and exact ordering search is a heuristic for it.
254
+ - Symmetric variables could be grouped to shrink the search space further.
255
+
256
+ ## 9. Prior work
257
+
258
+ The building blocks of `optimizeOrdering()` and where they come from:
259
+
260
+ - **Level sizes depend only on the set of functions above a level**, which
261
+ turns the search for the optimal ordering into a shortest path problem in
262
+ the lattice of variable subsets (section 3). S. J. Friedman and K. J.
263
+ Supowit, "Finding the optimal variable ordering for binary decision
264
+ diagrams", IEEE Transactions on Computers 39(5), 1990 (first presented at
265
+ DAC 1987). Their dynamic program needs O(n² 3ⁿ) time.
266
+ - **Exact minimization with lower bounds**, which visits only a fraction of the
267
+ subsets: R. Drechsler, N. Drechsler and W. Günther, "Fast exact minimization
268
+ of BDDs", DAC 1998 / IEEE Transactions on CAD 19(3), 2000; R. Ebendt, W.
269
+ Günther and R. Drechsler, "An improved branch and bound algorithm for exact
270
+ BDD minimization", IEEE Transactions on CAD 22(12), 2003, and "Combining
271
+ ordered best-first search with branch and bound for exact BDD minimization",
272
+ IEEE Transactions on CAD 24(10), 2005. The A* search of section 3 and its
273
+ lower bound (the number of distinct non-constant cofactors) follow this work.
274
+ - **Sifting** (section 2): R. Rudell, "Dynamic variable ordering for ordered
275
+ binary decision diagrams", ICCAD 1993.
276
+ - **Window permutation** (section 2): N. Ishiura, H. Sawada and S. Yajima,
277
+ "Minimization of binary decision diagrams based on exchanges of variables",
278
+ ICCAD 1991; M. Fujita, Y. Matsunaga and T. Kakuda, "On variable ordering of
279
+ binary decision diagrams for the application of multi-level logic
280
+ synthesis", EDAC 1991.
281
+ - **Minimizing the expected cost of an evaluation instead of the node count**
282
+ (section 4): Y.-Y. Liu, K.-H. Wang, T. T. Hwang and C. L. Liu, "Binary
283
+ decision diagrams with minimum expected path length", DATE 2001; S. Nagayama,
284
+ A. Mishchenko, T. Sasao and J. T. Butler, "Minimization of average path
285
+ length in BDDs by variable reordering", IWLS 2003; R. Ebendt and R.
286
+ Drechsler, "Minimization of the expected path length in BDDs based on local
287
+ changes", ASP-DAC 2005, and "Exact BDD minimization for path-related
288
+ objective functions", 2005/2006. Costs per boolean function and weights per
289
+ row are a small generalization of the expected path length.
290
+ - **Don't-care rows**: resolving them to make the BDD smaller is the subject of
291
+ O. Coudert and J. C. Madre, "A unified framework for the formal verification
292
+ of sequential circuits", ICCAD 1990 (the restrict and constrain operators);
293
+ T. R. Shiple, R. Hojati, A. L. Sangiovanni-Vincentelli and R. K. Brayton,
294
+ "Heuristic minimization of BDDs using don't cares", DAC 1994; Y. Hong, P. A.
295
+ Beerel, J. R. Burch and K. L. McMillan, "Safe BDD minimization using don't
296
+ cares", DAC 1997. The sibling collapse of `removeIrrelevantLeafNodes()` is a
297
+ simple special case.
298
+ - **NP-completeness of finding the optimal ordering**: B. Bollig and I.
299
+ Wegener, "Improving the variable ordering of OBDDs is NP-complete", IEEE
300
+ Transactions on Computers 45(9), 1996.
301
+
302
+ What this library adds is the engineering around these results: the
303
+ evaluation of orderings directly on the truth table without node objects, the
304
+ block indexing that lets a swap recompute only the levels above it, the exact
305
+ search seeded with the local search result, and the alternation between the
306
+ don't-care completion and the exact search.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "binary-decision-diagram",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "A library to create, minimize and optimize binary decision diagrams",
5
5
  "keywords": [
6
6
  "binary decision diagram",
@@ -41,22 +41,24 @@
41
41
  "types": "./dist/esm/src/index.d.ts",
42
42
  "sideEffects": false,
43
43
  "scripts": {
44
- "lint": "tslint --project .",
45
- "lint:fix": "tslint --project . --fix",
44
+ "lint": "eslint .",
45
+ "lint:fix": "eslint . --fix",
46
46
  "transpile": "tsc -p ./ --module commonjs && echo '# transpile es5 (require) sucess!'",
47
47
  "transpile:es": "tsc -p ./ --outDir ./dist/esm && echo '{ \"type\": \"module\", \"sideEffects\": false }' > dist/esm/src/package.json && echo '# transpile esm (modules) sucess!'",
48
- "build": "rimraf -r ./dist && npm run transpile && npm run transpile:es",
48
+ "build": "rimraf ./dist && npm run transpile && npm run transpile:es",
49
49
  "test": "npm run build && mocha ./dist/cjs/test/unit/index.test.js --timeout 40000 --bail",
50
50
  "test:performance": "npm run build && mocha ./dist/cjs/test/performance/index.test.js --timeout 120000 --bail"
51
51
  },
52
52
  "devDependencies": {
53
+ "@stylistic/eslint-plugin": "5.10.0",
53
54
  "@types/mocha": "10.0.10",
54
- "@types/node": "24.12.0",
55
+ "@types/node": "26.6.5",
55
56
  "assert": "2.1.0",
56
57
  "async-test-util": "2.5.0",
57
- "mocha": "11.7.5",
58
- "rimraf": "3.0.2",
59
- "tslint": "6.1.3",
60
- "typescript": "5.5.4"
58
+ "eslint": "10.12.0",
59
+ "mocha": "12.0.3",
60
+ "rimraf": "6.1.3",
61
+ "typescript": "6.0.3",
62
+ "typescript-eslint": "8.71.2"
61
63
  }
62
64
  }
@@ -1,6 +1,6 @@
1
1
  import { nextNodeId } from './util.js';
2
2
  import type { NonRootNode, NonLeafNode, NodeType } from './types.js';
3
- import { Branches } from './branches.js';
3
+ import { Branches, BRANCH_KEYS } from './branches.js';
4
4
  import { RootNode } from './root-node.js';
5
5
  import { InternalNode } from './internal-node.js';
6
6
  import { LeafNode } from './leaf-node.js';
@@ -177,10 +177,12 @@ export class AbstractNode {
177
177
  const ownParents = (this as any).parents.getAll();
178
178
  const parentsWithStrictEqualBranches: NonLeafNode[] = [];
179
179
  ownParents.forEach((parent: InternalNode) => {
180
- // console.log('ownParent: ' + parent.id);
181
- const branchKey = parent.branches.getKeyOfNode(this as any);
182
- // console.log('branchKey: ' + branchKey);
183
- parent.branches.setBranch(branchKey, other);
180
+ // a parent (the root node) can have this node in both branches
181
+ BRANCH_KEYS.forEach(branchKey => {
182
+ if (parent.branches.getBranch(branchKey) === (this as any)) {
183
+ parent.branches.setBranch(branchKey, other);
184
+ }
185
+ });
184
186
 
185
187
  if (parent.branches.areBranchesStrictEqual()) {
186
188
  parentsWithStrictEqualBranches.push(parent);
package/src/branches.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import type { BooleanString, NonRootNode, NonLeafNode } from './types.js';
2
2
  import { AbstractNode } from './abstract-node.js';
3
3
 
4
+ export const BRANCH_KEYS: BooleanString[] = ['0', '1'];
5
+
4
6
  /**
5
7
  * represents the branches of a single node
6
8
  */
@@ -9,7 +9,7 @@ export function createBddFromTruthTable(
9
9
  ): RootNode {
10
10
  const root = new RootNode();
11
11
 
12
- const firstKey = truthTable.keys().next().value;
12
+ const firstKey = truthTable.keys().next().value as string;
13
13
  const keyLength = firstKey.length;
14
14
  const mustBeSize = Math.pow(2, keyLength);
15
15
  if (truthTable.size !== mustBeSize) {
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from './minimal-string/index.js';
2
1
  export * from './abstract-node.js';
3
2
  export * from './branches.js';
4
3
  export * from './create-bdd-from-truth-table.js';
@@ -8,6 +7,8 @@ export * from './find-similar-node.js';
8
7
  export * from './internal-node.js';
9
8
  export * from './leaf-node.js';
10
9
  export * from './optimize-brute-force.js';
10
+ export * from './packed/index.js';
11
+ export * from './ordering/index.js';
11
12
  export * from './parents.js';
12
13
  export * from './root-node.js';
13
14
  export * from './util.js';
@@ -1,4 +1,4 @@
1
- import { Branches } from './branches.js';
1
+ import { Branches, BRANCH_KEYS } from './branches.js';
2
2
  import { Parents } from './parents.js';
3
3
  import { RootNode } from './root-node.js';
4
4
  import { AbstractNode } from './abstract-node.js';
@@ -35,9 +35,12 @@ export class InternalNode extends AbstractNode {
35
35
  // move own parents to keepBranch
36
36
  const ownParents = this.parents.getAll();
37
37
  ownParents.forEach(parent => {
38
- // console.log('ownParent: ' + parent.id);
39
- const branchKey = parent.branches.getKeyOfNode(this);
40
- parent.branches.setBranch(branchKey, keepBranch);
38
+ // a parent (the root node) can have this node in both branches
39
+ BRANCH_KEYS.forEach(branchKey => {
40
+ if (parent.branches.getBranch(branchKey) === this) {
41
+ parent.branches.setBranch(branchKey, keepBranch);
42
+ }
43
+ });
41
44
 
42
45
  // remove parents from own list
43
46
  // this will auto-remove the connection to the other '1'-branch
@@ -191,13 +191,11 @@ export function changeKeyOrder(
191
191
  ): string {
192
192
  const chars = oldKey
193
193
  .split('')
194
- .map((char, indexBefore) => {
195
- return {
196
- char,
197
- indexBefore,
198
- indexAfter: mappingBeforeToAfter[indexBefore]
199
- };
200
- })
194
+ .map((char, indexBefore) => ({
195
+ char,
196
+ indexBefore,
197
+ indexAfter: mappingBeforeToAfter[indexBefore]
198
+ }))
201
199
  .sort((a, b) => a.indexAfter - b.indexAfter)
202
200
  .map(charObj => charObj.char)
203
201
  .join('');
@@ -0,0 +1,70 @@
1
+ import { RootNode } from '../root-node.js';
2
+ import { InternalNode } from '../internal-node.js';
3
+ import { LeafNode } from '../leaf-node.js';
4
+ import type { BooleanString, NonLeafNode } from '../types.js';
5
+ import type { DenseTruthTable } from './dense-truth-table.js';
6
+ import type { ReducedDiagram } from './ordering-evaluator.js';
7
+
8
+ /**
9
+ * Creates the RootNode based BDD out of a reduced diagram.
10
+ * The result is already minimal, so no minimize() call is needed.
11
+ *
12
+ * Like the BDDs returned by optimizeBruteForce(), the level of each node
13
+ * is the index of the boolean function it tests (so resolve() works
14
+ * with the original resolver functions) and RootNode.levels lists the
15
+ * levels from top to bottom, followed by the leaf level.
16
+ */
17
+ export function buildBddFromDiagram(
18
+ diagram: ReducedDiagram,
19
+ table: DenseTruthTable
20
+ ): RootNode {
21
+ const n = diagram.variableCount;
22
+ const ordering = diagram.ordering;
23
+ const terminalCount = diagram.terminalCount;
24
+ const leafLevel = n;
25
+
26
+ const root = new RootNode();
27
+ root.level = ordering[0];
28
+ root.levels = ordering.slice();
29
+ root.levels.push(leafLevel);
30
+ root.nodesByLevel = new Map();
31
+ root.levels.forEach(level => root.nodesByLevel.set(level, new Set()));
32
+ (root.nodesByLevel.get(root.level) as Set<RootNode>).add(root);
33
+
34
+ const internalById: Map<number, InternalNode> = new Map();
35
+ const leafById: Map<number, LeafNode> = new Map();
36
+
37
+ const attach = (parent: NonLeafNode, branch: BooleanString, id: number) => {
38
+ if (id < terminalCount) {
39
+ let leaf = leafById.get(id);
40
+ if (!leaf) {
41
+ leaf = new LeafNode(leafLevel, root, table.terminalValues[id], parent);
42
+ leafById.set(id, leaf);
43
+ }
44
+ parent.branches.setBranch(branch, leaf);
45
+ return;
46
+ }
47
+ const existing = internalById.get(id);
48
+ if (existing) {
49
+ parent.branches.setBranch(branch, existing);
50
+ return;
51
+ }
52
+ const index = id - terminalCount;
53
+ const node = new InternalNode(ordering[diagram.nodeLevel[index]], root, parent);
54
+ internalById.set(id, node);
55
+ parent.branches.setBranch(branch, node);
56
+ attach(node, '0', diagram.nodeLo[index]);
57
+ attach(node, '1', diagram.nodeHi[index]);
58
+ };
59
+
60
+ const rootId = diagram.root;
61
+ if (rootId >= terminalCount && diagram.nodeLevel[rootId - terminalCount] === 0) {
62
+ attach(root, '0', diagram.nodeLo[rootId - terminalCount]);
63
+ attach(root, '1', diagram.nodeHi[rootId - terminalCount]);
64
+ } else {
65
+ // the top level variable is irrelevant, the root node points to the same node twice
66
+ attach(root, '0', rootId);
67
+ attach(root, '1', rootId);
68
+ }
69
+ return root;
70
+ }