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.
- package/CHANGELOG.md +6 -0
- package/README.md +211 -31
- package/dist/cjs/src/abstract-node.js +7 -4
- package/dist/cjs/src/abstract-node.js.map +1 -1
- package/dist/cjs/src/branches.d.ts +1 -0
- package/dist/cjs/src/branches.js +2 -1
- package/dist/cjs/src/branches.js.map +1 -1
- package/dist/cjs/src/create-bdd-from-truth-table.js.map +1 -1
- package/dist/cjs/src/index.d.ts +2 -1
- package/dist/cjs/src/index.es5.js +17 -7
- package/dist/cjs/src/index.es5.js.map +1 -1
- package/dist/cjs/src/index.js +2 -1
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/cjs/src/internal-node.js +6 -3
- package/dist/cjs/src/internal-node.js.map +1 -1
- package/dist/cjs/src/optimize-brute-force.js +5 -7
- package/dist/cjs/src/optimize-brute-force.js.map +1 -1
- package/dist/cjs/src/ordering/build-bdd.d.ts +13 -0
- package/dist/cjs/src/ordering/build-bdd.js +64 -0
- package/dist/cjs/src/ordering/build-bdd.js.map +1 -0
- package/dist/cjs/src/ordering/dense-truth-table.d.ts +42 -0
- package/dist/cjs/src/ordering/dense-truth-table.js +125 -0
- package/dist/cjs/src/ordering/dense-truth-table.js.map +1 -0
- package/dist/cjs/src/ordering/exact-cost-ordering.d.ts +91 -0
- package/dist/cjs/src/ordering/exact-cost-ordering.js +351 -0
- package/dist/cjs/src/ordering/exact-cost-ordering.js.map +1 -0
- package/dist/cjs/src/ordering/exact-ordering.d.ts +89 -0
- package/dist/cjs/src/ordering/exact-ordering.js +260 -0
- package/dist/cjs/src/ordering/exact-ordering.js.map +1 -0
- package/dist/cjs/src/ordering/index.d.ts +22 -0
- package/dist/cjs/src/ordering/index.js +38 -0
- package/dist/cjs/src/ordering/index.js.map +1 -0
- package/dist/cjs/src/ordering/int-pair-map.d.ts +20 -0
- package/dist/cjs/src/ordering/int-pair-map.js +85 -0
- package/dist/cjs/src/ordering/int-pair-map.js.map +1 -0
- package/dist/cjs/src/ordering/local-search.d.ts +114 -0
- package/dist/cjs/src/ordering/local-search.js +317 -0
- package/dist/cjs/src/ordering/local-search.js.map +1 -0
- package/dist/cjs/src/ordering/optimize-ordering.d.ts +153 -0
- package/dist/cjs/src/ordering/optimize-ordering.js +406 -0
- package/dist/cjs/src/ordering/optimize-ordering.js.map +1 -0
- package/dist/cjs/src/ordering/ordering-evaluator.d.ts +198 -0
- package/dist/cjs/src/ordering/ordering-evaluator.js +384 -0
- package/dist/cjs/src/ordering/ordering-evaluator.js.map +1 -0
- package/dist/cjs/src/ordering/shared-diagram.d.ts +85 -0
- package/dist/cjs/src/ordering/shared-diagram.js +295 -0
- package/dist/cjs/src/ordering/shared-diagram.js.map +1 -0
- package/dist/cjs/src/packed/index.d.ts +2 -0
- package/dist/cjs/src/{minimal-string → packed}/index.js +2 -5
- package/dist/cjs/src/packed/index.js.map +1 -0
- package/dist/cjs/src/packed/packed-bdd-base64-string.d.ts +3 -0
- package/dist/cjs/src/packed/packed-bdd-base64-string.js +46 -0
- package/dist/cjs/src/packed/packed-bdd-base64-string.js.map +1 -0
- package/dist/cjs/src/packed/packed-bdd.d.ts +13 -0
- package/dist/cjs/src/packed/packed-bdd.js +70 -0
- package/dist/cjs/src/packed/packed-bdd.js.map +1 -0
- package/dist/cjs/src/root-node.d.ts +1 -2
- package/dist/cjs/src/root-node.js +1 -5
- package/dist/cjs/src/root-node.js.map +1 -1
- package/dist/cjs/src/types.d.ts +0 -12
- package/dist/cjs/src/util.js +14 -5
- package/dist/cjs/src/util.js.map +1 -1
- package/dist/esm/src/abstract-node.js +7 -4
- package/dist/esm/src/abstract-node.js.map +1 -1
- package/dist/esm/src/branches.d.ts +1 -0
- package/dist/esm/src/branches.js +1 -0
- package/dist/esm/src/branches.js.map +1 -1
- package/dist/esm/src/create-bdd-from-truth-table.js.map +1 -1
- package/dist/esm/src/index.d.ts +2 -1
- package/dist/esm/src/index.js +2 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/internal-node.js +7 -4
- package/dist/esm/src/internal-node.js.map +1 -1
- package/dist/esm/src/optimize-brute-force.js +5 -7
- package/dist/esm/src/optimize-brute-force.js.map +1 -1
- package/dist/esm/src/ordering/build-bdd.d.ts +13 -0
- package/dist/esm/src/ordering/build-bdd.js +61 -0
- package/dist/esm/src/ordering/build-bdd.js.map +1 -0
- package/dist/esm/src/ordering/dense-truth-table.d.ts +42 -0
- package/dist/esm/src/ordering/dense-truth-table.js +117 -0
- package/dist/esm/src/ordering/dense-truth-table.js.map +1 -0
- package/dist/esm/src/ordering/exact-cost-ordering.d.ts +91 -0
- package/dist/esm/src/ordering/exact-cost-ordering.js +347 -0
- package/dist/esm/src/ordering/exact-cost-ordering.js.map +1 -0
- package/dist/esm/src/ordering/exact-ordering.d.ts +89 -0
- package/dist/esm/src/ordering/exact-ordering.js +256 -0
- package/dist/esm/src/ordering/exact-ordering.js.map +1 -0
- package/dist/esm/src/ordering/index.d.ts +22 -0
- package/dist/esm/src/ordering/index.js +17 -0
- package/dist/esm/src/ordering/index.js.map +1 -0
- package/dist/esm/src/ordering/int-pair-map.d.ts +20 -0
- package/dist/esm/src/ordering/int-pair-map.js +81 -0
- package/dist/esm/src/ordering/int-pair-map.js.map +1 -0
- package/dist/esm/src/ordering/local-search.d.ts +114 -0
- package/dist/esm/src/ordering/local-search.js +308 -0
- package/dist/esm/src/ordering/local-search.js.map +1 -0
- package/dist/esm/src/ordering/optimize-ordering.d.ts +153 -0
- package/dist/esm/src/ordering/optimize-ordering.js +399 -0
- package/dist/esm/src/ordering/optimize-ordering.js.map +1 -0
- package/dist/esm/src/ordering/ordering-evaluator.d.ts +198 -0
- package/dist/esm/src/ordering/ordering-evaluator.js +377 -0
- package/dist/esm/src/ordering/ordering-evaluator.js.map +1 -0
- package/dist/esm/src/ordering/shared-diagram.d.ts +85 -0
- package/dist/esm/src/ordering/shared-diagram.js +289 -0
- package/dist/esm/src/ordering/shared-diagram.js.map +1 -0
- package/dist/esm/src/packed/index.d.ts +2 -0
- package/dist/esm/src/packed/index.js +3 -0
- package/dist/esm/src/packed/index.js.map +1 -0
- package/dist/esm/src/packed/packed-bdd-base64-string.d.ts +3 -0
- package/dist/esm/src/packed/packed-bdd-base64-string.js +42 -0
- package/dist/esm/src/packed/packed-bdd-base64-string.js.map +1 -0
- package/dist/esm/src/packed/packed-bdd.d.ts +13 -0
- package/dist/esm/src/packed/packed-bdd.js +66 -0
- package/dist/esm/src/packed/packed-bdd.js.map +1 -0
- package/dist/esm/src/root-node.d.ts +1 -2
- package/dist/esm/src/root-node.js +1 -5
- package/dist/esm/src/root-node.js.map +1 -1
- package/dist/esm/src/types.d.ts +0 -12
- package/dist/esm/src/util.js +14 -5
- package/dist/esm/src/util.js.map +1 -1
- package/docs/variable-ordering.md +306 -0
- package/package.json +11 -9
- package/src/abstract-node.ts +7 -5
- package/src/branches.ts +2 -0
- package/src/create-bdd-from-truth-table.ts +1 -1
- package/src/index.ts +2 -1
- package/src/internal-node.ts +7 -4
- package/src/optimize-brute-force.ts +5 -7
- package/src/ordering/build-bdd.ts +70 -0
- package/src/ordering/dense-truth-table.ts +161 -0
- package/src/ordering/exact-cost-ordering.ts +425 -0
- package/src/ordering/exact-ordering.ts +317 -0
- package/src/ordering/index.ts +57 -0
- package/src/ordering/int-pair-map.ts +85 -0
- package/src/ordering/local-search.ts +369 -0
- package/src/ordering/optimize-ordering.ts +570 -0
- package/src/ordering/ordering-evaluator.ts +479 -0
- package/src/ordering/shared-diagram.ts +310 -0
- package/src/packed/index.ts +2 -0
- package/src/packed/packed-bdd-base64-string.ts +47 -0
- package/src/packed/packed-bdd.ts +98 -0
- package/src/root-node.ts +2 -8
- package/src/types.ts +0 -18
- package/src/util.ts +15 -6
- package/dist/cjs/src/minimal-string/bdd-to-minimal-string.d.ts +0 -8
- package/dist/cjs/src/minimal-string/bdd-to-minimal-string.js +0 -59
- package/dist/cjs/src/minimal-string/bdd-to-minimal-string.js.map +0 -1
- package/dist/cjs/src/minimal-string/bdd-to-simple-bdd.d.ts +0 -11
- package/dist/cjs/src/minimal-string/bdd-to-simple-bdd.js +0 -23
- package/dist/cjs/src/minimal-string/bdd-to-simple-bdd.js.map +0 -1
- package/dist/cjs/src/minimal-string/index.d.ts +0 -5
- package/dist/cjs/src/minimal-string/index.js.map +0 -1
- package/dist/cjs/src/minimal-string/minimal-string-to-simple-bdd.d.ts +0 -2
- package/dist/cjs/src/minimal-string/minimal-string-to-simple-bdd.js +0 -57
- package/dist/cjs/src/minimal-string/minimal-string-to-simple-bdd.js.map +0 -1
- package/dist/cjs/src/minimal-string/resolve-with-simple-bdd.d.ts +0 -2
- package/dist/cjs/src/minimal-string/resolve-with-simple-bdd.js +0 -18
- package/dist/cjs/src/minimal-string/resolve-with-simple-bdd.js.map +0 -1
- package/dist/cjs/src/minimal-string/string-format.d.ts +0 -9
- package/dist/cjs/src/minimal-string/string-format.js +0 -61
- package/dist/cjs/src/minimal-string/string-format.js.map +0 -1
- package/dist/esm/src/minimal-string/bdd-to-minimal-string.d.ts +0 -8
- package/dist/esm/src/minimal-string/bdd-to-minimal-string.js +0 -55
- package/dist/esm/src/minimal-string/bdd-to-minimal-string.js.map +0 -1
- package/dist/esm/src/minimal-string/bdd-to-simple-bdd.d.ts +0 -11
- package/dist/esm/src/minimal-string/bdd-to-simple-bdd.js +0 -19
- package/dist/esm/src/minimal-string/bdd-to-simple-bdd.js.map +0 -1
- package/dist/esm/src/minimal-string/index.d.ts +0 -5
- package/dist/esm/src/minimal-string/index.js +0 -6
- package/dist/esm/src/minimal-string/index.js.map +0 -1
- package/dist/esm/src/minimal-string/minimal-string-to-simple-bdd.d.ts +0 -2
- package/dist/esm/src/minimal-string/minimal-string-to-simple-bdd.js +0 -54
- package/dist/esm/src/minimal-string/minimal-string-to-simple-bdd.js.map +0 -1
- package/dist/esm/src/minimal-string/resolve-with-simple-bdd.d.ts +0 -2
- package/dist/esm/src/minimal-string/resolve-with-simple-bdd.js +0 -15
- package/dist/esm/src/minimal-string/resolve-with-simple-bdd.js.map +0 -1
- package/dist/esm/src/minimal-string/string-format.d.ts +0 -9
- package/dist/esm/src/minimal-string/string-format.js +0 -54
- package/dist/esm/src/minimal-string/string-format.js.map +0 -1
- package/src/minimal-string/bdd-to-minimal-string.ts +0 -72
- package/src/minimal-string/bdd-to-simple-bdd.ts +0 -31
- package/src/minimal-string/index.ts +0 -5
- package/src/minimal-string/minimal-string-to-simple-bdd.ts +0 -67
- package/src/minimal-string/resolve-with-simple-bdd.ts +0 -23
- 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": "
|
|
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": "
|
|
45
|
-
"lint: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
|
|
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": "
|
|
55
|
+
"@types/node": "26.6.5",
|
|
55
56
|
"assert": "2.1.0",
|
|
56
57
|
"async-test-util": "2.5.0",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"typescript": "
|
|
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
|
}
|
package/src/abstract-node.ts
CHANGED
|
@@ -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
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
@@ -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';
|
package/src/internal-node.ts
CHANGED
|
@@ -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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|