bmssp 1.1.1 → 2.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/src/tieBreak.mjs CHANGED
@@ -38,9 +38,11 @@
38
38
  * source is a hop-0 root) and "no predecessor" (which never loses a tie).
39
39
  */
40
40
 
41
- // Sentinel predecessor for sources: compares below every real id, so a
42
- // source's own label never loses an equal-(length, hops) comparison.
43
- const NO_PRED = -Infinity;
41
+ // Sentinel predecessor for sources: compares below every real vertex index,
42
+ // so a source's own label never loses an equal-(length, hops) comparison.
43
+ // Since #205 the engine works on dense vertex indices (>= 0), so -1 is a
44
+ // valid sentinel AND fits the Int32Array holding the predecessors.
45
+ const NO_PRED = -1;
44
46
 
45
47
  // Running count of compareKeys calls — the paper's cost metric ("comparisons
46
48
  // between path lengths"). Every BMSSP-side comparison funnels through
@@ -78,6 +80,26 @@ function compareKeys(a, b) {
78
80
  return 0;
79
81
  }
80
82
 
83
+ /**
84
+ * Lexicographic comparison of an UNPACKED key (length, hops, id) against a
85
+ * packed one — identical order to compareKeys without materializing the
86
+ * left-hand array (#168: the hot relax/routing loops call this instead of
87
+ * allocating a throwaway key per test). Counts as one comparison.
88
+ * @param {number} length - Left key's length component
89
+ * @param {number} hops - Left key's hops component
90
+ * @param {*} id - Left key's id component
91
+ * @param {[number, number, *]} key - Right key, packed
92
+ * @returns {number} Negative when (length, hops, id) < key, positive when
93
+ * greater, 0 when equal
94
+ */
95
+ function compareKeyParts(length, hops, id, key) {
96
+ comparisonCount += 1;
97
+ if (length !== key[0]) return length < key[0] ? -1 : 1;
98
+ if (hops !== key[1]) return hops < key[1] ? -1 : 1;
99
+ if (id !== key[2]) return id < key[2] ? -1 : 1;
100
+ return 0;
101
+ }
102
+
81
103
  /**
82
104
  * Normalize a bound to a composite key. A scalar B becomes the infimum of
83
105
  * all keys of length B, preserving the strict "distance < B" contract;
@@ -90,7 +112,9 @@ function toBound(B) {
90
112
  }
91
113
 
92
114
  /**
93
- * Bundle (or create) the tie-break label maps that accompany d̂.
115
+ * Bundle (or create) the tie-break label maps that accompany d̂ at the PUBLIC
116
+ * boundary (the BMSSP class mirrors the engine's arrays into these Maps,
117
+ * keyed by original node ids).
94
118
  * @param {Map<*, number>} [hops] - Canonical path edge counts
95
119
  * @param {Map<*, *>} [preds] - Canonical predecessor of each vertex
96
120
  * @returns {{ hops: Map<*, number>, preds: Map<*, *> }}
@@ -99,6 +123,31 @@ function makeTies(hops = new Map(), preds = new Map()) {
99
123
  return { hops, preds };
100
124
  }
101
125
 
126
+ /**
127
+ * Allocate the engine's label state for n vertices (#205): d̂, hops and
128
+ * canonical predecessors as typed arrays over dense vertex indices.
129
+ * Unlabeled vertices read as distance Infinity, hop 0, no predecessor —
130
+ * the same defaults the pre-#205 Maps gave via `?? Infinity` / `?? 0`.
131
+ * @param {number} n - Vertex count
132
+ * @returns {{ dist: Float64Array, hops: Uint32Array, preds: Int32Array }}
133
+ */
134
+ function makeLabels(n) {
135
+ const dist = new Float64Array(n).fill(Infinity);
136
+ const hops = new Uint32Array(n);
137
+ const preds = new Int32Array(n).fill(NO_PRED);
138
+ return { dist, hops, preds };
139
+ }
140
+
141
+ /**
142
+ * The frontier-ordering key of vertex index v under the engine labels.
143
+ * @param {number} v - Dense vertex index
144
+ * @param {{ dist: Float64Array, hops: Uint32Array }} labels
145
+ * @returns {[number, number, number]} [dist[v], hops[v], v]
146
+ */
147
+ function labelKey(v, labels) {
148
+ return [labels.dist[v], labels.hops[v], v];
149
+ }
150
+
102
151
  /**
103
152
  * The frontier-ordering key of a vertex under its current labels.
104
153
  * @param {*} v - Vertex id
@@ -130,45 +179,78 @@ function orderKey(v, dHat, ties) {
130
179
  * tied alternative parents; callers filter already-completed vertices to
131
180
  * keep the re-enqueue finite, exactly like the paper.
132
181
  *
133
- * @param {*} u - Edge tail (its labels are read)
134
- * @param {*} v - Edge head (its labels may be updated)
182
+ * Allocation-free since #168: the result is one of the three RELAX_* codes,
183
+ * and a caller that needs v's (possibly updated) order key materializes it
184
+ * with orderKey(v, dHat, ties) — only on the rare paths that enqueue v,
185
+ * instead of on every attempt.
186
+ *
187
+ * Since #205 the labels live in typed arrays over dense vertex indices
188
+ * (see makeLabels); u and v are indices, and index order equals original-id
189
+ * order (the BMSSP class assigns indices by sorted id), so the canonical
190
+ * choices are identical to the pre-#205 Map engine's.
191
+ *
192
+ * @param {number} u - Edge tail index (its labels are read)
193
+ * @param {number} v - Edge head index (its labels may be updated)
135
194
  * @param {number} weight - Edge weight, >= 0
136
- * @param {Map<*, number>} dHat - Distance estimates d̂[·], updated in place
137
- * @param {{ hops: Map<*, number>, preds: Map<*, *> }} ties - Updated in place
195
+ * @param {{ dist: Float64Array, hops: Uint32Array, preds: Int32Array }} labels
196
+ * - Engine label state, updated in place
138
197
  * @param {[number, number, *]} [bound] - Optional gate: skip (no d̂ update)
139
198
  * unless the resulting order key would be strictly below this bound
140
- * @returns {{ key: [number, number, *], improved: boolean }|null} v's order
141
- * key [d̂[v], hops[v], v] with `improved: true` when the labels were
142
- * updated, `improved: false` when the candidate exactly matches v's
143
- * canonical label; null when the candidate loses (or the bound gates it)
199
+ * @returns {number} RELAX_IMPROVED when the labels were updated,
200
+ * RELAX_EQUAL when the candidate exactly matches v's canonical label
201
+ * (the re-enqueue signal), RELAX_LOST when the candidate loses or the
202
+ * bound gates it (labels untouched)
144
203
  */
145
- function relaxEdge(u, v, weight, dHat, ties, bound) {
146
- const length = dHat.get(u) + weight;
147
- const hopCount = (ties.hops.get(u) ?? 0) + 1;
148
- if (bound !== undefined && compareKeys([length, hopCount, v], bound) >= 0) {
149
- return null;
204
+ function relaxEdge(u, v, weight, labels, bound) {
205
+ const length = labels.dist[u] + weight;
206
+ const hopCount = labels.hops[u] + 1;
207
+ if (bound !== undefined && compareKeyParts(length, hopCount, v, bound) >= 0) {
208
+ return RELAX_LOST;
209
+ }
210
+ // Candidate path key [length, hopCount, u] vs. v's current path key
211
+ // [dist[v], hops[v], preds[v]] — compareKeys inlined on the unpacked
212
+ // components (one counted comparison, no arrays)
213
+ comparisonCount += 1;
214
+ let cmp;
215
+ const currentLength = labels.dist[v];
216
+ if (length !== currentLength) {
217
+ cmp = length < currentLength ? -1 : 1;
218
+ } else {
219
+ const currentHops = labels.hops[v];
220
+ if (hopCount !== currentHops) {
221
+ cmp = hopCount < currentHops ? -1 : 1;
222
+ } else {
223
+ const currentPred = labels.preds[v];
224
+ cmp = u !== currentPred ? (u < currentPred ? -1 : 1) : 0;
225
+ }
150
226
  }
151
- const currentPathKey = [
152
- dHat.get(v) ?? Infinity,
153
- ties.hops.get(v) ?? 0,
154
- ties.preds.has(v) ? ties.preds.get(v) : NO_PRED,
155
- ];
156
- const cmp = compareKeys([length, hopCount, u], currentPathKey);
157
- if (cmp > 0) return null;
158
- if (cmp === 0) return { key: [length, hopCount, v], improved: false };
159
- dHat.set(v, length);
160
- ties.hops.set(v, hopCount);
161
- ties.preds.set(v, u);
162
- return { key: [length, hopCount, v], improved: true };
227
+ if (cmp > 0) return RELAX_LOST;
228
+ if (cmp === 0) return RELAX_EQUAL;
229
+ labels.dist[v] = length;
230
+ labels.hops[v] = hopCount;
231
+ labels.preds[v] = u;
232
+ return RELAX_IMPROVED;
163
233
  }
164
234
 
235
+ // relaxEdge result codes (#168): distinct, and only RELAX_LOST is falsy-like
236
+ // in comparisons — callers must compare against the constants, not truthiness
237
+ const RELAX_LOST = -1;
238
+ const RELAX_EQUAL = 0;
239
+ const RELAX_IMPROVED = 1;
240
+
165
241
  export {
166
242
  compareKeys,
243
+ compareKeyParts,
167
244
  toBound,
168
245
  makeTies,
246
+ makeLabels,
169
247
  orderKey,
248
+ labelKey,
170
249
  relaxEdge,
171
250
  NO_PRED,
251
+ RELAX_LOST,
252
+ RELAX_EQUAL,
253
+ RELAX_IMPROVED,
172
254
  resetComparisonCount,
173
255
  getComparisonCount,
174
256
  };