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,570 @@
1
+ import type { TruthTable } from '../types.js';
2
+ import { RootNode } from '../root-node.js';
3
+ import {
4
+ parseTruthTable,
5
+ denseTruthTableToMap,
6
+ ensureValidOrdering,
7
+ DenseTruthTable
8
+ } from './dense-truth-table.js';
9
+ import { OrderingEvaluator, OrderingEvaluation, CostModel } from './ordering-evaluator.js';
10
+ import {
11
+ LocalOrderingSearch,
12
+ seedOrderings,
13
+ OrderingProgressCallback,
14
+ DEFAULT_WINDOW_SIZE,
15
+ ensureWindowSize,
16
+ ensureNonNegativeInteger
17
+ } from './local-search.js';
18
+ import { ExactOrderingSearch } from './exact-ordering.js';
19
+ import { ExactCostOrderingSearch } from './exact-cost-ordering.js';
20
+ import { buildBddFromDiagram } from './build-bdd.js';
21
+ import type { AbstractNode } from '../abstract-node.js';
22
+ import type { InternalNode } from '../internal-node.js';
23
+
24
+ export const DEFAULT_MAX_EXACT_VARIABLES = 20;
25
+ export const DEFAULT_MAX_EXACT_NODE_COUNT = 5000;
26
+
27
+ export interface FindOrderingInput {
28
+ truthTable: TruthTable;
29
+ /**
30
+ * Value of the rows that can never occur at runtime.
31
+ * These rows are resolved exactly like removeIrrelevantLeafNodes() does it:
32
+ * a branch that only leads to don't-care rows is replaced by its sibling.
33
+ * Which ordering profits most from that is part of the search, but the
34
+ * rule itself is a heuristic, not the smallest possible completion.
35
+ */
36
+ dontCareValue?: number;
37
+ /**
38
+ * Cost of calling each boolean function: functionCosts[i] belongs to the
39
+ * boolean function with index i (the i-th character of the truth table keys).
40
+ * When given, the optimizer minimizes the expected cost of one resolve() call,
41
+ * that is the sum of the costs of the boolean functions that are called on the
42
+ * way from the root to the leaf, averaged over the rows, instead of the amount
43
+ * of nodes. Among orderings with the same expected cost, fewer nodes are
44
+ * preferred (best effort, not part of the optimality proof).
45
+ */
46
+ functionCosts?: number[];
47
+ /**
48
+ * How often each row occurs at runtime relative to the other rows,
49
+ * used together with functionCosts. Default: every row that is not a
50
+ * don't-care row occurs equally often. Don't-care rows never occur.
51
+ */
52
+ rowWeight?: (key: string, value: number) => number;
53
+ /**
54
+ * Additional start orderings for the local search, for example the
55
+ * best ordering of a previous run. ordering[level] is the index
56
+ * of the boolean function tested at that level.
57
+ */
58
+ initialOrderings?: number[][];
59
+ /**
60
+ * - 'auto' (default): run the exact search when the truth table has at
61
+ * most maxExactVariables boolean functions and the BDD found by the local
62
+ * search has at most maxExactNodeCount internal nodes (unstructured
63
+ * functions have big BDDs, and on them the exact search is slow and
64
+ * cannot gain much)
65
+ * - true: always run the exact search (within the budgets below)
66
+ * - false: never
67
+ */
68
+ exact?: boolean | 'auto';
69
+ /** Default: 20 */
70
+ maxExactVariables?: number;
71
+ /** Default: 5000 */
72
+ maxExactNodeCount?: number;
73
+ /**
74
+ * Budget of the exact search per candidate completion of the truth table.
75
+ * A candidate whose search exceeds it cannot be proven optimal.
76
+ * Default: 200000
77
+ */
78
+ maxExactExpansions?: number;
79
+ /**
80
+ * Memory budget of the exact search: the maximum amount of nodes of its
81
+ * internal diagram (roughly 100 bytes each).
82
+ * Default: 4000000
83
+ */
84
+ maxExactDiagramNodes?: number;
85
+ /**
86
+ * Size of the windows the local search permutes exhaustively,
87
+ * at most 8 (the work grows with windowSize!).
88
+ * Default: 3
89
+ */
90
+ windowSize?: number;
91
+ /**
92
+ * Maximum amount of sifting + window permutation rounds per start ordering.
93
+ * Default: 50
94
+ */
95
+ maxRounds?: number;
96
+ log?: boolean;
97
+ /** called whenever a better ordering was found */
98
+ onImprovement?: OrderingProgressCallback;
99
+ }
100
+
101
+ export interface FindOrderingResult {
102
+ /** ordering[level] = index of the boolean function tested at that level */
103
+ ordering: number[];
104
+ /** amount of nodes of the BDD, as RootNode.countNodes() returns it */
105
+ nodeCount: number;
106
+ /**
107
+ * Expected cost of one resolve() call with the returned ordering,
108
+ * only set when functionCosts were given.
109
+ */
110
+ expectedCost?: number;
111
+ /**
112
+ * true when the exact search has proven that no other ordering gives a
113
+ * BDD with fewer nodes (or, with functionCosts, a lower expected cost)
114
+ * for the truth table in `truthTable`. With don't-care rows that is one
115
+ * completion of the input: another ordering can occasionally reach a smaller
116
+ * BDD by resolving the don't-care rows differently.
117
+ */
118
+ optimal: boolean;
119
+ /**
120
+ * The truth table the BDD represents: the input table with the don't-care
121
+ * rows resolved, with the same keys as the input (iterated in row order).
122
+ * createBddWithOrdering(result.truthTable, result.ordering) rebuilds the BDD.
123
+ * When don't-care rows exist, this completion can differ from the one that
124
+ * createBddWithOrdering(input, result.ordering, dontCareValue) would pick.
125
+ */
126
+ truthTable: TruthTable;
127
+ stats: OptimizeOrderingStats;
128
+ }
129
+
130
+ export interface OptimizeOrderingStats {
131
+ /** amount of orderings that were evaluated */
132
+ evaluations: number;
133
+ /** amount of states the exact search expanded */
134
+ exactExpansions: number;
135
+ /** the amount of nodes found by the local search alone */
136
+ localSearchNodeCount: number;
137
+ durationMs: number;
138
+ }
139
+
140
+ interface BestOrdering {
141
+ ordering: number[];
142
+ evaluation: OrderingEvaluation;
143
+ }
144
+
145
+ export interface OptimizeOrderingInput extends FindOrderingInput {
146
+ /**
147
+ * Hook that runs on the created BDD before it is minimized,
148
+ * for compatibility with optimizeBruteForce().
149
+ * The optimizer cannot account for what the hook does, so prefer dontCareValue
150
+ * over removeIrrelevantLeafNodes() in the hook. When the hook changes the BDD,
151
+ * `optimal` is false, and `truthTable` and `expectedCost` describe the BDD
152
+ * before the hook.
153
+ */
154
+ afterBddCreation?: (bdd: RootNode) => void;
155
+ }
156
+
157
+ export interface OptimizeOrderingResult extends FindOrderingResult {
158
+ bdd: RootNode;
159
+ }
160
+
161
+ /**
162
+ * Validates functionCosts and rowWeight and turns them into a cost model.
163
+ */
164
+ export function buildCostModel(
165
+ table: DenseTruthTable,
166
+ functionCosts?: number[],
167
+ rowWeight?: (key: string, value: number) => number
168
+ ): CostModel | null {
169
+ if (!functionCosts) {
170
+ if (rowWeight) {
171
+ throw new Error('rowWeight can only be used together with functionCosts');
172
+ }
173
+ return null;
174
+ }
175
+ const n = table.variableCount;
176
+ if (functionCosts.length !== n) {
177
+ throw new Error('functionCosts must have one entry per boolean function (' + n + ')');
178
+ }
179
+ const costs = new Float64Array(n);
180
+ for (let v = 0; v < n; v++) {
181
+ const cost = functionCosts[v];
182
+ if (typeof cost !== 'number' || !isFinite(cost) || cost < 0) {
183
+ throw new Error('functionCosts[' + v + '] must be a non-negative number');
184
+ }
185
+ costs[v] = cost;
186
+ }
187
+ const weights = new Float64Array(table.rowCount);
188
+ let totalWeight = 0;
189
+ for (let row = 0; row < table.rowCount; row++) {
190
+ const id = table.terminalIds[row];
191
+ if (id === table.dontCareTerminal) {
192
+ continue;
193
+ }
194
+ let weight = 1;
195
+ if (rowWeight) {
196
+ const key = row.toString(2).padStart(n, '0');
197
+ weight = rowWeight(key, table.terminalValues[id]);
198
+ if (typeof weight !== 'number' || !isFinite(weight) || weight < 0) {
199
+ throw new Error('rowWeight must return a non-negative number, got ' + weight + ' for ' + key);
200
+ }
201
+ }
202
+ weights[row] = weight;
203
+ totalWeight += weight;
204
+ }
205
+ if (totalWeight <= 0) {
206
+ throw new Error('all row weights are zero');
207
+ }
208
+ let maxCost = 0;
209
+ costs.forEach(cost => maxCost = Math.max(maxCost, cost));
210
+ if (!isFinite(totalWeight) || !isFinite(maxCost * totalWeight)) {
211
+ throw new Error('the function costs or row weights are too large, scale them down');
212
+ }
213
+ return {
214
+ functionCosts: costs,
215
+ rowWeights: weights,
216
+ totalWeight
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Deterministically finds a variable ordering with a small BDD
222
+ * (or, with functionCosts, with a cheap resolve() call).
223
+ * See docs/variable-ordering.md for how it works.
224
+ * Does not create BDD nodes, use optimizeOrdering() for that.
225
+ */
226
+ export function findOrdering(input: FindOrderingInput): FindOrderingResult {
227
+ return findOrderingWithTable(input).result;
228
+ }
229
+
230
+ /**
231
+ * Checks the options before any expensive work starts.
232
+ */
233
+ function ensureValidOptions(input: FindOrderingInput) {
234
+ if (input.windowSize !== undefined) {
235
+ ensureWindowSize(input.windowSize);
236
+ }
237
+ if (input.maxRounds !== undefined) {
238
+ ensureNonNegativeInteger(input.maxRounds, 'maxRounds');
239
+ }
240
+ const budgets: (keyof FindOrderingInput)[] = ['maxExactVariables', 'maxExactNodeCount', 'maxExactExpansions', 'maxExactDiagramNodes'];
241
+ budgets.forEach(name => {
242
+ const value = input[name];
243
+ if (value !== undefined) {
244
+ ensureNonNegativeInteger(value as number, name);
245
+ }
246
+ });
247
+ if (input.exact !== undefined && input.exact !== true && input.exact !== false && input.exact !== 'auto') {
248
+ throw new Error('exact must be true, false or \'auto\'');
249
+ }
250
+ }
251
+
252
+ function findOrderingWithTable(input: FindOrderingInput): { result: FindOrderingResult, table: DenseTruthTable } {
253
+ const startTime = Date.now();
254
+ ensureValidOptions(input);
255
+ const log = input.log ? (message: string) => console.log('findOrdering(): ' + message) : () => null;
256
+ const onImprovement = input.onImprovement ? input.onImprovement : () => null;
257
+ const base = parseTruthTable(input.truthTable, input.dontCareValue);
258
+ const n = base.variableCount;
259
+ const costModel = buildCostModel(base, input.functionCosts, input.rowWeight);
260
+ const baseEvaluator = new OrderingEvaluator(base, costModel);
261
+ const windowSize = input.windowSize === undefined ? DEFAULT_WINDOW_SIZE : input.windowSize;
262
+ let evaluations = 0;
263
+ const describe = (evaluation: OrderingEvaluation) => evaluation.nodeCount + ' nodes' +
264
+ (costModel ? ', expected cost ' + evaluation.expectedCost : '');
265
+
266
+ /**
267
+ * phase 1: deterministic seeds, each improved by sifting and window permutation
268
+ */
269
+ const userSeeds = (input.initialOrderings ? input.initialOrderings : []).map((ordering, index) => {
270
+ ensureValidOrdering(ordering, n);
271
+ return { name: 'initial-' + index, ordering: Array.from(ordering) };
272
+ });
273
+ const seenSeeds: Set<string> = new Set();
274
+ const seeds = userSeeds.concat(seedOrderings(baseEvaluator)).filter(seed => {
275
+ const key = seed.ordering.join(',');
276
+ if (seenSeeds.has(key)) {
277
+ return false;
278
+ }
279
+ seenSeeds.add(key);
280
+ return true;
281
+ });
282
+ let localBest: BestOrdering | null = null;
283
+ const seedResults: BestOrdering[] = [];
284
+ // the last evaluation passed to onImprovement
285
+ const reported: { evaluation: OrderingEvaluation | null } = { evaluation: null };
286
+ for (const seed of seeds) {
287
+ const search = new LocalOrderingSearch(baseEvaluator, seed.ordering, {
288
+ windowSize,
289
+ maxRounds: input.maxRounds,
290
+ log: message => log('[' + seed.name + '] ' + message),
291
+ onImprovement: progress => {
292
+ if (!localBest || baseEvaluator.isBetter(search.best, localBest.evaluation)) {
293
+ reported.evaluation = search.best;
294
+ onImprovement(progress);
295
+ }
296
+ }
297
+ });
298
+ const result = search.run();
299
+ log('seed ' + seed.name + ': ' + describe(result.evaluation));
300
+ seedResults.push({ ordering: result.ordering, evaluation: result.evaluation });
301
+ if (!localBest || baseEvaluator.isBetter(result.evaluation, localBest.evaluation)) {
302
+ localBest = { ordering: result.ordering, evaluation: result.evaluation };
303
+ if (!reported.evaluation || baseEvaluator.isBetter(result.evaluation, reported.evaluation)) {
304
+ // the seed itself was better than everything before, the search did not report it
305
+ reported.evaluation = result.evaluation;
306
+ onImprovement({
307
+ ordering: result.ordering.slice(),
308
+ nodeCount: result.evaluation.nodeCount,
309
+ expectedCost: result.evaluation.expectedCost,
310
+ phase: 'seed'
311
+ });
312
+ }
313
+ }
314
+ }
315
+ if (!localBest) {
316
+ throw new Error('no ordering found, this should not happen');
317
+ }
318
+ let best: BestOrdering = localBest;
319
+ const localSearchNodeCount = best.evaluation.nodeCount;
320
+
321
+ /**
322
+ * phase 2: exact search
323
+ *
324
+ * The exact search needs a fully specified function, so it runs on the
325
+ * truth table with the don't-care rows resolved. Different orderings resolve
326
+ * them differently, so every completion the local search came across is a
327
+ * candidate. Whenever a new best ordering is found, its own completion
328
+ * becomes a candidate too.
329
+ */
330
+ let completed = baseEvaluator.complete(best.ordering);
331
+ let optimal = false;
332
+ let exactExpansions = 0;
333
+ const exactMode = input.exact === undefined ? 'auto' : input.exact;
334
+ const maxExactVariables = input.maxExactVariables === undefined ? DEFAULT_MAX_EXACT_VARIABLES : input.maxExactVariables;
335
+ const maxExactNodeCount = input.maxExactNodeCount === undefined ? DEFAULT_MAX_EXACT_NODE_COUNT : input.maxExactNodeCount;
336
+ const runExact = exactMode === true || (
337
+ exactMode === 'auto' &&
338
+ n <= maxExactVariables &&
339
+ best.evaluation.internalNodeCount <= maxExactNodeCount
340
+ );
341
+ if (exactMode === 'auto' && !runExact) {
342
+ log('exact search skipped: ' + n + ' boolean functions, ' + best.evaluation.internalNodeCount + ' internal nodes');
343
+ }
344
+ if (runExact) {
345
+ const candidates: { ordering: number[], table: DenseTruthTable }[] = [];
346
+ const seenCompletions: Set<string> = new Set();
347
+ const addCandidate = (ordering: number[], front: boolean) => {
348
+ const table = baseEvaluator.complete(ordering);
349
+ const key = completionKey(table);
350
+ if (seenCompletions.has(key)) {
351
+ return;
352
+ }
353
+ seenCompletions.add(key);
354
+ const candidate = { ordering, table };
355
+ if (front) {
356
+ candidates.unshift(candidate);
357
+ } else {
358
+ candidates.push(candidate);
359
+ }
360
+ };
361
+ addCandidate(best.ordering, false);
362
+ if (base.dontCareTerminal >= 0) {
363
+ seedResults.forEach(result => addCandidate(result.ordering, false));
364
+ }
365
+
366
+ /**
367
+ * completions for which the exact search finished: on them, no ordering
368
+ * beats the best evaluation known at that time (which is never worse than
369
+ * the final one)
370
+ */
371
+ const solvedCompletions: Set<string> = new Set();
372
+ while (candidates.length > 0) {
373
+ const candidate = candidates.shift() as { ordering: number[], table: DenseTruthTable };
374
+ const candidateKey = completionKey(candidate.table);
375
+ let exactOrdering: number[] | null;
376
+ let solved: boolean;
377
+ let expansions: number;
378
+ const searchOptions = { maxDiagramNodes: input.maxExactDiagramNodes };
379
+ if (costModel) {
380
+ const exactResult = new ExactCostOrderingSearch(candidate.table, costModel, searchOptions).run({
381
+ maxExpansions: input.maxExactExpansions,
382
+ upperBoundOrdering: candidate.ordering,
383
+ // only orderings that beat the best known one are interesting
384
+ upperBound: best.evaluation.expectedCost,
385
+ log
386
+ });
387
+ exactOrdering = exactResult.ordering;
388
+ solved = exactResult.solved;
389
+ expansions = exactResult.expansions;
390
+ } else {
391
+ const exactResult = new ExactOrderingSearch(candidate.table, searchOptions).run({
392
+ maxExpansions: input.maxExactExpansions,
393
+ upperBoundOrdering: candidate.ordering,
394
+ upperBound: best.evaluation.nodeCount - candidate.table.leafCount,
395
+ log
396
+ });
397
+ exactOrdering = exactResult.ordering;
398
+ solved = exactResult.solved;
399
+ expansions = exactResult.expansions;
400
+ }
401
+ exactExpansions += expansions;
402
+ if (!solved) {
403
+ log('exact search gave up on one completion of the truth table');
404
+ continue;
405
+ }
406
+ solvedCompletions.add(candidateKey);
407
+ if (!exactOrdering) {
408
+ continue;
409
+ }
410
+ const candidateEvaluator = new OrderingEvaluator(candidate.table, costModel);
411
+ let exactEvaluation = candidateEvaluator.evaluate(exactOrdering);
412
+ if (costModel) {
413
+ /**
414
+ * The exact cost search only minimizes the expected cost. Among the
415
+ * orderings with the same cost, fewer nodes are better, which the
416
+ * local search on the same completion takes care of.
417
+ */
418
+ const polished = new LocalOrderingSearch(candidateEvaluator, exactOrdering, {
419
+ windowSize,
420
+ maxRounds: input.maxRounds
421
+ }).run();
422
+ exactOrdering = polished.ordering;
423
+ exactEvaluation = polished.evaluation;
424
+ }
425
+ evaluations += candidateEvaluator.evaluations;
426
+ log('exact search: ' + describe(exactEvaluation) + ' after ' + expansions + ' expansions');
427
+ if (baseEvaluator.isBetter(exactEvaluation, best.evaluation)) {
428
+ best = { ordering: exactOrdering, evaluation: exactEvaluation };
429
+ completed = candidate.table;
430
+ onImprovement({
431
+ ordering: best.ordering.slice(),
432
+ nodeCount: exactEvaluation.nodeCount,
433
+ expectedCost: exactEvaluation.expectedCost,
434
+ phase: 'exact'
435
+ });
436
+ if (base.dontCareTerminal >= 0) {
437
+ // the don't-care rows might resolve even better with the new ordering
438
+ addCandidate(best.ordering, true);
439
+ }
440
+ }
441
+ }
442
+
443
+ /**
444
+ * When the completion the ordering picks itself is just as good,
445
+ * prefer it: then createBddWithOrdering(input, ordering, dontCareValue)
446
+ * rebuilds the same BDD.
447
+ */
448
+ if (base.dontCareTerminal >= 0) {
449
+ const own = baseEvaluator.complete(best.ordering);
450
+ if (completionKey(own) !== completionKey(completed)) {
451
+ const ownEvaluation = baseEvaluator.evaluate(best.ordering);
452
+ if (baseEvaluator.compare(ownEvaluation, best.evaluation) === 0) {
453
+ best = { ordering: best.ordering, evaluation: ownEvaluation };
454
+ completed = own;
455
+ }
456
+ }
457
+ }
458
+ optimal = solvedCompletions.has(completionKey(completed));
459
+ }
460
+ evaluations += baseEvaluator.evaluations;
461
+
462
+ return {
463
+ result: {
464
+ ordering: best.ordering,
465
+ nodeCount: best.evaluation.nodeCount,
466
+ expectedCost: costModel ? best.evaluation.expectedCost : undefined,
467
+ optimal,
468
+ truthTable: denseTruthTableToMap(completed),
469
+ stats: {
470
+ evaluations,
471
+ exactExpansions,
472
+ localSearchNodeCount,
473
+ durationMs: Date.now() - startTime
474
+ }
475
+ },
476
+ table: completed
477
+ };
478
+ }
479
+
480
+ /**
481
+ * identifies the content of a (completed) table
482
+ */
483
+ function completionKey(table: DenseTruthTable): string {
484
+ let h1 = 0x811C9DC5;
485
+ let h2 = 0x01000193;
486
+ const ids = table.terminalIds;
487
+ for (let i = 0; i < ids.length; i++) {
488
+ h1 = Math.imul(h1 ^ ids[i], 0x01000193);
489
+ h2 = Math.imul(h2 + ids[i], 0x9E3779B1) ^ (h2 >>> 13);
490
+ }
491
+ return (h1 >>> 0).toString(16) + ':' + (h2 >>> 0).toString(16) + ':' + ids.length;
492
+ }
493
+
494
+ /**
495
+ * Deterministic replacement for optimizeBruteForce():
496
+ * finds a good variable ordering and returns the minimized BDD for it.
497
+ * All work is synchronous, the promise exists for compatibility with optimizeBruteForce().
498
+ */
499
+ export async function optimizeOrdering(input: OptimizeOrderingInput): Promise<OptimizeOrderingResult> {
500
+ const { result: found, table } = findOrderingWithTable(input);
501
+ const bdd = buildBddForOrdering(table, found.ordering);
502
+ let optimal = found.optimal;
503
+ if (input.afterBddCreation) {
504
+ const before = fingerprint(bdd);
505
+ input.afterBddCreation(bdd);
506
+ bdd.minimize();
507
+ if (fingerprint(bdd) !== before) {
508
+ // the hook changed the BDD, the proof does not hold for it anymore
509
+ optimal = false;
510
+ }
511
+ }
512
+ return {
513
+ bdd,
514
+ ordering: found.ordering,
515
+ nodeCount: bdd.countNodes(),
516
+ expectedCost: found.expectedCost,
517
+ optimal,
518
+ truthTable: found.truthTable,
519
+ stats: found.stats
520
+ };
521
+ }
522
+
523
+ /**
524
+ * Describes the structure and the leaf values of a BDD,
525
+ * equal BDDs give equal strings.
526
+ */
527
+ function fingerprint(bdd: RootNode): string {
528
+ const ids: Map<AbstractNode, number> = new Map();
529
+ const parts: string[] = [];
530
+ const visit = (node: AbstractNode): number => {
531
+ const known = ids.get(node);
532
+ if (known !== undefined) {
533
+ return known;
534
+ }
535
+ let part: string;
536
+ if (node.isLeafNode()) {
537
+ part = 'v' + String(node.asLeafNode().value);
538
+ } else {
539
+ const branches = (node as InternalNode).branches;
540
+ part = node.level + ':' + visit(branches.getBranch('0')) + ',' + visit(branches.getBranch('1'));
541
+ }
542
+ const id = parts.length;
543
+ parts.push(part);
544
+ ids.set(node, id);
545
+ return id;
546
+ };
547
+ visit(bdd);
548
+ return parts.join(';');
549
+ }
550
+
551
+ /**
552
+ * Creates the minimized BDD of a truth table for a given ordering
553
+ * without going through the unreduced decision tree.
554
+ */
555
+ export function createBddWithOrdering(
556
+ truthTable: TruthTable,
557
+ ordering: ArrayLike<number>,
558
+ dontCareValue?: number
559
+ ): RootNode {
560
+ const base = parseTruthTable(truthTable, dontCareValue);
561
+ const evaluator = new OrderingEvaluator(base);
562
+ const completed = evaluator.complete(ordering);
563
+ return buildBddForOrdering(completed, ordering);
564
+ }
565
+
566
+ function buildBddForOrdering(table: DenseTruthTable, ordering: ArrayLike<number>): RootNode {
567
+ const evaluator = new OrderingEvaluator(table);
568
+ const diagram = evaluator.buildDiagram(ordering);
569
+ return buildBddFromDiagram(diagram, table);
570
+ }