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/bmssp.mjs CHANGED
@@ -1,19 +1,60 @@
1
1
  import { baseCase } from "./baseCase.mjs";
2
2
  import { findPivots } from "./findPivots.mjs";
3
3
  import { BlockList } from "./blockList.mjs";
4
+ import { normalizeGraphInput } from "./graph.mjs";
4
5
  import {
5
6
  compareKeys,
7
+ compareKeyParts,
6
8
  toBound,
7
9
  makeTies,
8
- orderKey,
10
+ makeLabels,
11
+ labelKey,
9
12
  relaxEdge,
13
+ NO_PRED,
14
+ RELAX_LOST,
10
15
  } from "./tieBreak.mjs";
11
16
 
17
+ /**
18
+ * BMSSP — the paper's Algorithm 3 behind a small class API.
19
+ *
20
+ * **Supported public API (stable as of 2.0.0 — see `MIGRATION.md`):**
21
+ * - `new BMSSP(input)` — construct from an edge array, an adjacency `Map`/object,
22
+ * or a {@link Graph} builder (see the constructor).
23
+ * - {@link BMSSP#calculateShortestPaths} — single-source SSSP.
24
+ * - {@link BMSSP#calculateShortestPathsFrom} — multi-source / bounded run (the
25
+ * ergonomic front door to the paper's `BMSSP(l, B, S)` generalization).
26
+ * - {@link BMSSP#bmssp} — the low-level bounded multi-source primitive (advanced;
27
+ * composite keys, returns `{ bound, boundKey, vertices }`).
28
+ * - {@link BMSSP#reconstructPath} — canonical path for the latest run.
29
+ * - {@link BMSSP#getEdges} — O(1) outgoing-edge lookup.
30
+ * - Public fields: `shortestPaths`, `nodeIDs`, `hops`, `preds`, `adjacency`,
31
+ * `graph` (all documented in the constructor).
32
+ *
33
+ * **Everything else on this class is `@internal`** — the dense-index engine
34
+ * (`csr`, `labels`, `ids`, `indexOf`, `bmsspIndex`, `syncLabelsIn/Out`,
35
+ * `boundToEngine`, `keyToPublic`, `buildIndex`, …) and the derived parameters
36
+ * (`k`, `t`, `topLevel`, `ties`). It is not part of the public contract and may
37
+ * change in a minor release. The algorithm-internal modules (`BlockList`,
38
+ * `MinHeap`, `baseCase`, `findPivots`, `BoundIndex`, `select`, `tieBreak`) are
39
+ * likewise not re-exported from `index.mjs`.
40
+ *
41
+ * @public
42
+ */
12
43
  class BMSSP {
44
+ /**
45
+ * @public
46
+ * @param {Array<[number,number,number]>|Map|Object|Graph} inputGraph - Any
47
+ * #172 input shape: an edge array, an adjacency `Map`/object, or a `Graph`.
48
+ */
13
49
  constructor(inputGraph) {
14
- if (!Array.isArray(inputGraph)) {
15
- throw new Error("Input graph must be an array of edges");
16
- }
50
+ // #172: accept several input shapes (edge array, adjacency map/object, or
51
+ // a Graph builder) and reduce them to a canonical { edges, vertices }.
52
+ // `vertices` is the EXPLICIT vertex universe — declared nodes, including
53
+ // isolated ones; edge endpoints are folded in below. An edge-array input
54
+ // yields an empty `vertices`, reproducing the pre-#172 "infer from edges"
55
+ // behavior exactly.
56
+ const { edges: inputEdges, vertices: declaredVertices } =
57
+ normalizeGraphInput(inputGraph);
17
58
 
18
59
  // Main graph represented as an array of edges
19
60
  this.graph = [];
@@ -22,18 +63,19 @@ class BMSSP {
22
63
  // Map to store shortest paths
23
64
  this.shortestPaths = new Map();
24
65
  // Adjacency map: nodeId -> array of [to, weight] outgoing edges.
25
- // Lets the algorithm fetch a node's edges in O(1) instead of scanning
26
- // the whole edge list on every lookup.
66
+ // The public O(1) edge-lookup view (getEdges); since #205 the algorithm
67
+ // itself runs on the CSR arrays built by buildIndex below.
27
68
  this.adjacency = new Map();
28
69
  // Canonical tie-break labels (#163): hops = edge count of the canonical
29
- // shortest path, preds = its predecessor pointer. Updated in lockstep
30
- // with shortestPaths by relaxEdge; together they realize the paper's
31
- // Assumption 2.1 (distinct path lengths) via [length, hops, id] keys.
70
+ // shortest path, preds = its predecessor pointer. Since #205 these Maps
71
+ // are the PUBLIC mirror of the engine's typed-array labels, refreshed
72
+ // whenever a run finishes; together with shortestPaths they realize the
73
+ // paper's Assumption 2.1 via [length, hops, id] keys.
32
74
  this.hops = new Map();
33
75
  this.preds = new Map();
34
76
  this.ties = makeTies(this.hops, this.preds);
35
77
 
36
- for (let [index, edge] of inputGraph.entries()) {
78
+ for (let [index, edge] of inputEdges.entries()) {
37
79
  if (!Array.isArray(edge) || edge.length !== 3) {
38
80
  throw new Error(`Edge at index ${index} must be [from, to, weight]`);
39
81
  }
@@ -56,9 +98,23 @@ class BMSSP {
56
98
  this.nodeIDs.add(edge[1]);
57
99
  }
58
100
 
101
+ // #172: fold in explicitly declared vertices (isolated nodes included).
102
+ // Same finiteness contract as edge endpoints; buildIndex/buildAdjacency
103
+ // then give every declared node an index, an empty CSR range and an
104
+ // empty neighbor list.
105
+ for (const id of declaredVertices) {
106
+ if (!Number.isFinite(id)) {
107
+ throw new Error("Declared vertex IDs must be finite numbers");
108
+ }
109
+ this.nodeIDs.add(id);
110
+ }
111
+
59
112
  // Build the adjacency map from the copied edges
60
113
  this.buildAdjacency();
61
114
 
115
+ // Build the dense-index engine state: sorted-id index + CSR + labels
116
+ this.buildIndex();
117
+
62
118
  // Initialize shortest paths map
63
119
  this.initializeShortestPaths();
64
120
 
@@ -83,20 +139,71 @@ class BMSSP {
83
139
  }
84
140
  }
85
141
 
142
+ /**
143
+ * Build the dense-index engine state (#205): assign every node ID a dense
144
+ * index, lay the graph out in CSR form over those indices, and allocate
145
+ * the typed-array labels.
146
+ *
147
+ * Indices are assigned in ASCENDING NUMERIC ID ORDER. That makes index
148
+ * order equal id order, so the composite-key id tie-break (#163) picks the
149
+ * same canonical labels the id-keyed engine did — and, because the
150
+ * assignment depends only on the node-ID set, results stay invariant
151
+ * under edge-list permutation.
152
+ *
153
+ * @internal
154
+ */
155
+ buildIndex() {
156
+ const sorted = [...this.nodeIDs].sort((a, b) => a - b);
157
+ const n = sorted.length;
158
+ const m = this.graph.length;
159
+ // ids: index -> original node id; indexOf: original node id -> index
160
+ this.ids = Float64Array.from(sorted);
161
+ this.indexOf = new Map();
162
+ for (let i = 0; i < n; i += 1) {
163
+ this.indexOf.set(sorted[i], i);
164
+ }
165
+ // CSR: offsets[u]..offsets[u+1] delimit u's outgoing edges in
166
+ // targets/weights (edge order within a node follows this.graph; the
167
+ // #163 canonical labels are iteration-order independent anyway)
168
+ const offsets = new Uint32Array(n + 1);
169
+ for (const [from] of this.graph) {
170
+ offsets[this.indexOf.get(from) + 1] += 1;
171
+ }
172
+ for (let i = 0; i < n; i += 1) {
173
+ offsets[i + 1] += offsets[i];
174
+ }
175
+ const targets = new Uint32Array(m);
176
+ const weights = new Float64Array(m);
177
+ const cursor = offsets.slice(0, n);
178
+ for (const [from, to, weight] of this.graph) {
179
+ const u = this.indexOf.get(from);
180
+ const e = cursor[u];
181
+ cursor[u] += 1;
182
+ targets[e] = this.indexOf.get(to);
183
+ weights[e] = weight;
184
+ }
185
+ this.csr = { offsets, targets, weights };
186
+ // Engine labels: d̂ / hops / canonical preds over dense indices
187
+ this.labels = makeLabels(n);
188
+ }
189
+
86
190
  // Return the outgoing edges of a node as an array of [to, weight].
87
191
  // Unknown nodes return an empty array.
88
192
  getEdges(nodeId) {
89
193
  return this.adjacency.get(nodeId) ?? [];
90
194
  }
91
195
 
92
- // Method to initialize the shortest paths map (and the #163 tie-break
93
- // labels that travel with it)
196
+ // Method to initialize the shortest paths map, its #163 tie-break mirror
197
+ // maps, and the #205 engine arrays behind them
94
198
  initializeShortestPaths() {
95
199
  for (let nodeId of this.nodeIDs) {
96
200
  this.shortestPaths.set(nodeId, Infinity);
97
201
  }
98
202
  this.hops.clear();
99
203
  this.preds.clear();
204
+ this.labels.dist.fill(Infinity);
205
+ this.labels.hops.fill(0);
206
+ this.labels.preds.fill(NO_PRED);
100
207
  }
101
208
 
102
209
  /**
@@ -108,6 +215,8 @@ class BMSSP {
108
215
  * - topLevel = max(1, ⌈log₂ n / t⌉) — level of the top BMSSP call.
109
216
  * Everything is clamped to >= 1 so tiny graphs stay out of degenerate
110
217
  * regimes: correctness never depends on the asymptotics.
218
+ *
219
+ * @internal
111
220
  */
112
221
  deriveParameters() {
113
222
  const logn = Math.log2(Math.max(2, this.nodeIDs.size));
@@ -116,26 +225,74 @@ class BMSSP {
116
225
  this.topLevel = Math.max(1, Math.ceil(logn / this.t));
117
226
  }
118
227
 
228
+ // Internal (#205): load the engine arrays from the public Maps. A direct
229
+ // multi-source caller seeds initial state by writing this.shortestPaths
230
+ // (the documented contract), so the wrapper snapshots those distances into
231
+ // the engine. Seeded sources are roots — hop 0, no predecessor — matching
232
+ // both the paper and the pre-#205 behavior (the hops/preds Maps are empty
233
+ // after initializeShortestPaths, so they contributed nothing there either).
234
+ syncLabelsIn() {
235
+ const { dist, hops, preds } = this.labels;
236
+ dist.fill(Infinity);
237
+ hops.fill(0);
238
+ preds.fill(NO_PRED);
239
+ for (const [id, d] of this.shortestPaths) {
240
+ if (Number.isFinite(d)) dist[this.indexOf.get(id)] = d;
241
+ }
242
+ }
243
+
244
+ // Internal (#205): mirror the engine arrays back into the public Maps
245
+ // (shortestPaths / hops / preds, keyed by original ids). Unreached
246
+ // vertices keep their Infinity entries; sources keep no preds entry
247
+ // (their stored pred is the NO_PRED sentinel), which reconstructPath
248
+ // relies on to terminate.
249
+ syncLabelsOut() {
250
+ const { dist, hops, preds } = this.labels;
251
+ const ids = this.ids;
252
+ for (let i = 0; i < ids.length; i += 1) {
253
+ if (dist[i] === Infinity) continue;
254
+ const id = ids[i];
255
+ this.shortestPaths.set(id, dist[i]);
256
+ this.hops.set(id, hops[i]);
257
+ if (preds[i] !== NO_PRED) this.preds.set(id, ids[preds[i]]);
258
+ }
259
+ }
260
+
261
+ // Internal (#205): translate a caller's bound into the engine's index
262
+ // space. Scalar bounds become the usual [B, -Inf, -Inf] infimum key; a
263
+ // composite bound's id component is mapped to its index (ids not in the
264
+ // graph — including the -Infinity sentinel — pass through unchanged).
265
+ boundToEngine(B) {
266
+ if (typeof B === "number") return toBound(B);
267
+ const idx = this.indexOf.get(B[2]);
268
+ return [B[0], B[1], idx === undefined ? B[2] : idx];
269
+ }
270
+
271
+ // Internal (#205): translate an engine key's index component back to the
272
+ // original node id (sentinel components pass through).
273
+ keyToPublic(key) {
274
+ const idx = key[2];
275
+ const isIndex = Number.isInteger(idx) && idx >= 0 && idx < this.ids.length;
276
+ return [key[0], key[1], isIndex ? this.ids[idx] : idx];
277
+ }
278
+
119
279
  /**
120
280
  * BMSSP(l, B, S) — Algorithm 3 of "Breaking the Sorting Barrier for
121
281
  * Directed Single-Source Shortest Paths": the main bounded multi-source
122
282
  * recursion, wiring FindPivots (Algorithm 1), the Lemma 3.3 BlockList and
123
283
  * BaseCase (Algorithm 2) together.
124
284
  *
285
+ * Public boundary (#205): S holds original node ids and the returned
286
+ * vertices/boundKey are in id space; initial multi-source state is seeded
287
+ * by writing this.shortestPaths (the pre-#205 contract). Internally the
288
+ * call runs on the dense-index engine — this wrapper snapshots the Maps
289
+ * into the typed arrays, runs bmsspIndex, and mirrors the arrays back.
290
+ *
125
291
  * Preconditions (the top-level call satisfies them trivially):
126
292
  * - Every vertex in S is complete (this.shortestPaths holds its true
127
293
  * distance), and every incomplete vertex v with d(v) < B has a shortest
128
294
  * path through some complete vertex of S.
129
295
  *
130
- * Distance estimates live in this.shortestPaths (the paper's d̂[·]) and
131
- * are relaxed in place at every level, together with the canonical hops
132
- * and preds labels (#163). All internal ordering uses the composite
133
- * [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
134
- * Assumption 2.1: pull separators are strict, no key ever ties a bound,
135
- * and the pre-#163 degenerate-tie guards (out-of-scope pivots,
136
- * boundary-tied batch members, the empty-child stall escape hatch) are
137
- * unnecessary by construction.
138
- *
139
296
  * Two outcomes (Lemma 3.1, strict under the composite order):
140
297
  * - Successful execution: the block list emptied — boundKey === B's key
141
298
  * and vertices holds every v with key(v) < B reachable through S.
@@ -145,48 +302,60 @@ class BMSSP {
145
302
  * the returned bound's length (never exceed it).
146
303
  * Every returned vertex is complete either way.
147
304
  *
305
+ * @public
148
306
  * @param {number} l - Recursion level; 0 delegates to baseCase
149
307
  * @param {number|[number, number, *]} B - Strict upper bound on the keys
150
308
  * in scope: a number (Infinity is allowed) or a composite bound
151
- * @param {Set<*>} S - Non-empty set of complete frontier sources
309
+ * @param {Set<*>} S - Non-empty set of complete frontier sources (ids)
152
310
  * @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
153
311
  * The boundary B' <= B (same kind as the B passed in: scalar callers
154
312
  * get a scalar), its composite key, and the set U of vertices
155
- * completed below it
313
+ * completed below it — all in original-id space
156
314
  */
157
315
  bmssp(l, B, S) {
158
- const dHat = this.shortestPaths;
159
- const ties = this.ties;
160
- const boundKey = toBound(B);
161
- const scalarB = typeof B === "number";
316
+ this.syncLabelsIn();
317
+ const boundKey = this.boundToEngine(B);
318
+ const SIdx = new Set();
319
+ for (const id of S) {
320
+ const idx = this.indexOf.get(id);
321
+ // Unknown sources keep their raw id: the engine then reads an
322
+ // undefined label and fails the finite-distance precondition, the
323
+ // same error the id-keyed engine raised
324
+ SIdx.add(idx === undefined ? id : idx);
325
+ }
326
+ const result = this.bmsspIndex(l, boundKey, SIdx);
327
+ this.syncLabelsOut();
328
+ const vertices = new Set();
329
+ for (const idx of result.vertices) {
330
+ vertices.add(this.ids[idx]);
331
+ }
332
+ const finalKey = this.keyToPublic(result.boundKey);
162
333
  // Project the composite result back to the caller's kind: a successful
163
334
  // execution echoes B itself, a partial one reports the separator (whose
164
335
  // length is strictly below a scalar B by construction)
165
- const finish = (finalKey, vertices) => ({
166
- bound: scalarB
167
- ? compareKeys(finalKey, boundKey) === 0
336
+ const bound =
337
+ typeof B === "number"
338
+ ? compareKeys(result.boundKey, boundKey) === 0
168
339
  ? B
169
340
  : finalKey[0]
170
- : finalKey,
171
- boundKey: finalKey,
172
- vertices,
173
- });
341
+ : finalKey;
342
+ return { bound, boundKey: finalKey, vertices };
343
+ }
344
+
345
+ // Internal (#205): the actual Algorithm 3 recursion, entirely in dense
346
+ // index space — S/vertices hold indices, keys are [length, hops, index],
347
+ // the graph is CSR and the labels are the shared typed arrays.
348
+ bmsspIndex(l, boundKey, S) {
349
+ const labels = this.labels;
174
350
 
175
351
  if (l === 0) {
176
- const result = baseCase(boundKey, S, dHat, this.adjacency, this.k, ties);
177
- return finish(result.boundKey, result.vertices);
352
+ const result = baseCase(boundKey, S, labels, this.csr, this.k);
353
+ return { boundKey: result.boundKey, vertices: result.vertices };
178
354
  }
179
355
 
180
356
  // Shrink the frontier: only the pivots are worth recursing on, and W
181
357
  // is a batch of already-completed vertices folded in at the end
182
- const { pivots, W } = findPivots(
183
- boundKey,
184
- S,
185
- dHat,
186
- this.adjacency,
187
- this.k,
188
- ties,
189
- );
358
+ const { pivots, W } = findPivots(boundKey, S, labels, this.csr, this.k);
190
359
 
191
360
  // Seed the Lemma 3.3 block list with the pivots. lastBoundKey tracks
192
361
  // the paper's Bi': B when P is empty, min key over P before the first
@@ -197,7 +366,7 @@ class BMSSP {
197
366
  const D = new BlockList(2 ** ((l - 1) * this.t), boundKey, compareKeys);
198
367
  let lastBoundKey = boundKey;
199
368
  for (const x of pivots) {
200
- const key = orderKey(x, dHat, ties);
369
+ const key = labelKey(x, labels);
201
370
  if (compareKeys(key, boundKey) < 0) {
202
371
  D.insert(x, key);
203
372
  if (compareKeys(key, lastBoundKey) < 0) lastBoundKey = key;
@@ -206,11 +375,12 @@ class BMSSP {
206
375
 
207
376
  const U = new Set();
208
377
  const workloadCap = this.k * 2 ** (l * this.t);
378
+ const { offsets, targets, weights } = this.csr;
209
379
 
210
380
  while (U.size < workloadCap && !D.isEmpty()) {
211
381
  // Bi, Si <- D.Pull(): the next-closest small batch and its separator
212
382
  const { keys: Si, bound: BiKey } = D.pull();
213
- const child = this.bmssp(l - 1, BiKey, Si);
383
+ const child = this.bmsspIndex(l - 1, BiKey, Si);
214
384
  const BiPrimeKey = child.boundKey;
215
385
  const Ui = child.vertices;
216
386
 
@@ -224,19 +394,23 @@ class BMSSP {
224
394
  // without completing (the paper's `≤` relaxation, deterministic here:
225
395
  // only the recorded label-setter triggers it); vertices already
226
396
  // completed at this level are filtered so re-enqueues stay finite.
397
+ // A non-lost relaxation leaves v's stored label equal to the
398
+ // candidate, so the band tests compare the unpacked stored components
399
+ // and a key array is built only for the enqueue itself (#168).
227
400
  const K = [];
228
401
  for (const u of Ui) {
229
- for (const [v, weight] of this.adjacency.get(u) ?? []) {
230
- const relaxed = relaxEdge(u, v, weight, dHat, ties);
231
- if (relaxed === null || U.has(v)) continue;
232
- const key = relaxed.key;
233
- if (compareKeys(key, BiKey) >= 0 && compareKeys(key, boundKey) < 0) {
234
- D.insert(v, key);
235
- } else if (
236
- compareKeys(key, BiPrimeKey) >= 0 &&
237
- compareKeys(key, BiKey) < 0
238
- ) {
239
- K.push([v, key]);
402
+ for (let e = offsets[u]; e < offsets[u + 1]; e += 1) {
403
+ const v = targets[e];
404
+ const result = relaxEdge(u, v, weights[e], labels);
405
+ if (result === RELAX_LOST || U.has(v)) continue;
406
+ const length = labels.dist[v];
407
+ const hopCount = labels.hops[v];
408
+ if (compareKeyParts(length, hopCount, v, BiKey) >= 0) {
409
+ if (compareKeyParts(length, hopCount, v, boundKey) < 0) {
410
+ D.insert(v, [length, hopCount, v]);
411
+ }
412
+ } else if (compareKeyParts(length, hopCount, v, BiPrimeKey) >= 0) {
413
+ K.push([v, [length, hopCount, v]]);
240
414
  }
241
415
  }
242
416
  }
@@ -244,9 +418,13 @@ class BMSSP {
244
418
  // go back in front of everything else
245
419
  for (const x of Si) {
246
420
  if (U.has(x)) continue;
247
- const key = orderKey(x, dHat, ties);
248
- if (compareKeys(key, BiPrimeKey) >= 0 && compareKeys(key, BiKey) < 0) {
249
- K.push([x, key]);
421
+ const length = labels.dist[x];
422
+ const hopCount = labels.hops[x];
423
+ if (
424
+ compareKeyParts(length, hopCount, x, BiKey) < 0 &&
425
+ compareKeyParts(length, hopCount, x, BiPrimeKey) >= 0
426
+ ) {
427
+ K.push([x, [length, hopCount, x]]);
250
428
  }
251
429
  }
252
430
  if (K.length > 0) D.batchPrepend(K);
@@ -256,9 +434,11 @@ class BMSSP {
256
434
  const finalKey =
257
435
  compareKeys(lastBoundKey, boundKey) < 0 ? lastBoundKey : boundKey;
258
436
  for (const x of W) {
259
- if (compareKeys(orderKey(x, dHat, ties), finalKey) < 0) U.add(x);
437
+ if (compareKeyParts(labels.dist[x], labels.hops[x], x, finalKey) < 0) {
438
+ U.add(x);
439
+ }
260
440
  }
261
- return finish(finalKey, U);
441
+ return { boundKey: finalKey, vertices: U };
262
442
  }
263
443
 
264
444
  // Method to calculate shortest paths from startNode via the BMSSP
@@ -266,7 +446,8 @@ class BMSSP {
266
446
  // is always a successful execution, so it completes every reachable
267
447
  // vertex; unreachable ones keep their Infinity estimate. Distances, hop
268
448
  // counts and predecessor pointers all end at their canonical values —
269
- // independent of edge or iteration order (#163).
449
+ // independent of edge or iteration order (#163) — and are mirrored into
450
+ // the public Maps when the run finishes (#205).
270
451
  calculateShortestPaths(startNode) {
271
452
  // To clean the state before calculation
272
453
  this.initializeShortestPaths();
@@ -278,9 +459,131 @@ class BMSSP {
278
459
 
279
460
  // The source is complete at distance 0 with zero hops and no
280
461
  // predecessor; everything else is Infinity
281
- this.shortestPaths.set(startNode, 0);
282
- this.hops.set(startNode, 0);
283
- this.bmssp(this.topLevel, Infinity, new Set([startNode]));
462
+ const s = this.indexOf.get(startNode);
463
+ this.labels.dist[s] = 0;
464
+ this.bmsspIndex(this.topLevel, toBound(Infinity), new Set([s]));
465
+ this.syncLabelsOut();
466
+ }
467
+
468
+ // Internal (#171): reduce the flexible `sources` argument of
469
+ // calculateShortestPathsFrom to a Map<id, initialDistance>. Accepts a
470
+ // Map<id, dist>, a plain object { id: dist } (numeric-string keys coerced
471
+ // like #172's adjacency objects), an array of [id, dist] pairs, or a bare
472
+ // array of ids (each seeded at distance 0). Every id must be a known node
473
+ // and every initial distance a finite non-negative number; a repeated
474
+ // source keeps its smallest initial distance.
475
+ normalizeSources(sources) {
476
+ const out = new Map();
477
+ const add = (id, dist) => {
478
+ if (!this.nodeIDs.has(id)) {
479
+ throw new Error(`Source ${id} not found in the graph`);
480
+ }
481
+ if (!Number.isFinite(dist) || dist < 0) {
482
+ throw new Error(
483
+ `Source ${id} must have a non-negative finite initial distance`,
484
+ );
485
+ }
486
+ const prev = out.get(id);
487
+ out.set(id, prev === undefined ? dist : Math.min(prev, dist));
488
+ };
489
+
490
+ if (sources instanceof Map) {
491
+ for (const [id, dist] of sources) add(id, dist);
492
+ } else if (Array.isArray(sources)) {
493
+ for (const entry of sources) {
494
+ if (Array.isArray(entry)) {
495
+ if (entry.length !== 2) {
496
+ throw new Error("Each source pair must be [id, initialDistance]");
497
+ }
498
+ add(entry[0], entry[1]);
499
+ } else {
500
+ add(entry, 0);
501
+ }
502
+ }
503
+ } else if (sources && typeof sources === "object") {
504
+ for (const key of Object.keys(sources)) {
505
+ add(Number(key), sources[key]);
506
+ }
507
+ } else {
508
+ throw new Error(
509
+ "sources must be a Map, object, array of [id, dist] pairs, or array of ids",
510
+ );
511
+ }
512
+ return out;
513
+ }
514
+
515
+ /**
516
+ * Multi-source, optionally-bounded shortest paths — the public form of the
517
+ * paper's BMSSP(l, B, S) generalization (§04). Runs from a SET of sources,
518
+ * each with an initial distance, optionally under a strict distance bound B.
519
+ * Single-source SSSP is exactly the special case sources = [start],
520
+ * bound = Infinity — what calculateShortestPaths does.
521
+ *
522
+ * Results land in this.shortestPaths (and the hops/preds mirror), like
523
+ * calculateShortestPaths: every completed vertex holds its distance;
524
+ * vertices not reached — or, in a bounded run, not completed below B — keep
525
+ * Infinity. Read the answer from this.shortestPaths after the call. Under a
526
+ * finite bound only the completed set U is exposed: BMSSP may relax vertices
527
+ * above B without completing them, so those leftover estimates are pruned
528
+ * back to Infinity, leaving exactly the vertices with distance < B.
529
+ *
530
+ * The initial distances are treated as the sources' TRUE (complete)
531
+ * distances — the paper's precondition on S. With the common all-zero
532
+ * seeding this is trivially satisfied (nearest-of-many). Passing custom
533
+ * initial distances where one source's shortest path undercuts another's
534
+ * declared distance violates the precondition; the multi-source ground
535
+ * truth is trueDist(v) = min over sources s of (d0[s] + dist_s(v)).
536
+ *
537
+ * @public
538
+ * @param {Map<number,number>|Object<string,number>|Array<[number,number]>|number[]} sources
539
+ * The source set. A Map<id, dist>, a plain object { id: dist } (numeric
540
+ * string keys coerced), an array of [id, dist] pairs, or a bare array of
541
+ * ids (each seeded at distance 0).
542
+ * @param {object} [options]
543
+ * @param {number} [options.bound=Infinity] - Strict distance upper bound B;
544
+ * only vertices with distance < B are completed. Infinity runs unbounded.
545
+ * @throws {Error} If sources is empty or an unrecognized shape, a source id
546
+ * is not in the graph, an initial distance is not a non-negative finite
547
+ * number, or bound is not a non-negative number / Infinity.
548
+ */
549
+ calculateShortestPathsFrom(sources, { bound = Infinity } = {}) {
550
+ this.initializeShortestPaths();
551
+
552
+ const seeded = this.normalizeSources(sources);
553
+ if (seeded.size === 0) {
554
+ throw new Error("At least one source is required");
555
+ }
556
+ if (typeof bound !== "number" || Number.isNaN(bound) || bound < 0) {
557
+ throw new Error("bound must be a non-negative number or Infinity");
558
+ }
559
+
560
+ // Seed each source's initial distance into the public estimate; the
561
+ // bmssp() wrapper's syncLabelsIn snapshots these finite distances into the
562
+ // engine as complete roots (hop 0, no predecessor).
563
+ for (const [id, dist] of seeded) {
564
+ this.shortestPaths.set(id, dist);
565
+ }
566
+ const { vertices } = this.bmssp(
567
+ this.topLevel,
568
+ bound,
569
+ new Set(seeded.keys()),
570
+ );
571
+
572
+ // Under a finite bound the top-level call is a successful execution whose
573
+ // returned set U is exactly the vertices completed below B. BMSSP relaxes
574
+ // (but does not complete) some vertices at or above B, leaving over-
575
+ // estimates in the mirror; prune everything outside U so the public Maps
576
+ // report only the exact in-bound distances (matching an unbounded run,
577
+ // whose U already equals the reachable set — no pruning needed there).
578
+ if (bound !== Infinity) {
579
+ for (const id of this.nodeIDs) {
580
+ if (!vertices.has(id)) {
581
+ this.shortestPaths.set(id, Infinity);
582
+ this.hops.delete(id);
583
+ this.preds.delete(id);
584
+ }
585
+ }
586
+ }
284
587
  }
285
588
 
286
589
  /**
@@ -288,6 +591,7 @@ class BMSSP {
288
591
  * target. Returns an empty array when target is unreachable or no shortest
289
592
  * path run has completed yet.
290
593
  *
594
+ * @public
291
595
  * @param {number} target - A node in the graph
292
596
  * @returns {number[]} Node IDs from the source through target
293
597
  * @throws {Error} If target is not in the graph