bmssp 1.2.0 → 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,21 +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,
6
7
  compareKeyParts,
7
8
  toBound,
8
9
  makeTies,
9
- orderKey,
10
+ makeLabels,
11
+ labelKey,
10
12
  relaxEdge,
13
+ NO_PRED,
11
14
  RELAX_LOST,
12
15
  } from "./tieBreak.mjs";
13
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
+ */
14
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
+ */
15
49
  constructor(inputGraph) {
16
- if (!Array.isArray(inputGraph)) {
17
- throw new Error("Input graph must be an array of edges");
18
- }
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);
19
58
 
20
59
  // Main graph represented as an array of edges
21
60
  this.graph = [];
@@ -24,18 +63,19 @@ class BMSSP {
24
63
  // Map to store shortest paths
25
64
  this.shortestPaths = new Map();
26
65
  // Adjacency map: nodeId -> array of [to, weight] outgoing edges.
27
- // Lets the algorithm fetch a node's edges in O(1) instead of scanning
28
- // 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.
29
68
  this.adjacency = new Map();
30
69
  // Canonical tie-break labels (#163): hops = edge count of the canonical
31
- // shortest path, preds = its predecessor pointer. Updated in lockstep
32
- // with shortestPaths by relaxEdge; together they realize the paper's
33
- // 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.
34
74
  this.hops = new Map();
35
75
  this.preds = new Map();
36
76
  this.ties = makeTies(this.hops, this.preds);
37
77
 
38
- for (let [index, edge] of inputGraph.entries()) {
78
+ for (let [index, edge] of inputEdges.entries()) {
39
79
  if (!Array.isArray(edge) || edge.length !== 3) {
40
80
  throw new Error(`Edge at index ${index} must be [from, to, weight]`);
41
81
  }
@@ -58,9 +98,23 @@ class BMSSP {
58
98
  this.nodeIDs.add(edge[1]);
59
99
  }
60
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
+
61
112
  // Build the adjacency map from the copied edges
62
113
  this.buildAdjacency();
63
114
 
115
+ // Build the dense-index engine state: sorted-id index + CSR + labels
116
+ this.buildIndex();
117
+
64
118
  // Initialize shortest paths map
65
119
  this.initializeShortestPaths();
66
120
 
@@ -85,20 +139,71 @@ class BMSSP {
85
139
  }
86
140
  }
87
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
+
88
190
  // Return the outgoing edges of a node as an array of [to, weight].
89
191
  // Unknown nodes return an empty array.
90
192
  getEdges(nodeId) {
91
193
  return this.adjacency.get(nodeId) ?? [];
92
194
  }
93
195
 
94
- // Method to initialize the shortest paths map (and the #163 tie-break
95
- // 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
96
198
  initializeShortestPaths() {
97
199
  for (let nodeId of this.nodeIDs) {
98
200
  this.shortestPaths.set(nodeId, Infinity);
99
201
  }
100
202
  this.hops.clear();
101
203
  this.preds.clear();
204
+ this.labels.dist.fill(Infinity);
205
+ this.labels.hops.fill(0);
206
+ this.labels.preds.fill(NO_PRED);
102
207
  }
103
208
 
104
209
  /**
@@ -110,6 +215,8 @@ class BMSSP {
110
215
  * - topLevel = max(1, ⌈log₂ n / t⌉) — level of the top BMSSP call.
111
216
  * Everything is clamped to >= 1 so tiny graphs stay out of degenerate
112
217
  * regimes: correctness never depends on the asymptotics.
218
+ *
219
+ * @internal
113
220
  */
114
221
  deriveParameters() {
115
222
  const logn = Math.log2(Math.max(2, this.nodeIDs.size));
@@ -118,26 +225,74 @@ class BMSSP {
118
225
  this.topLevel = Math.max(1, Math.ceil(logn / this.t));
119
226
  }
120
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
+
121
279
  /**
122
280
  * BMSSP(l, B, S) — Algorithm 3 of "Breaking the Sorting Barrier for
123
281
  * Directed Single-Source Shortest Paths": the main bounded multi-source
124
282
  * recursion, wiring FindPivots (Algorithm 1), the Lemma 3.3 BlockList and
125
283
  * BaseCase (Algorithm 2) together.
126
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
+ *
127
291
  * Preconditions (the top-level call satisfies them trivially):
128
292
  * - Every vertex in S is complete (this.shortestPaths holds its true
129
293
  * distance), and every incomplete vertex v with d(v) < B has a shortest
130
294
  * path through some complete vertex of S.
131
295
  *
132
- * Distance estimates live in this.shortestPaths (the paper's d̂[·]) and
133
- * are relaxed in place at every level, together with the canonical hops
134
- * and preds labels (#163). All internal ordering uses the composite
135
- * [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
136
- * Assumption 2.1: pull separators are strict, no key ever ties a bound,
137
- * and the pre-#163 degenerate-tie guards (out-of-scope pivots,
138
- * boundary-tied batch members, the empty-child stall escape hatch) are
139
- * unnecessary by construction.
140
- *
141
296
  * Two outcomes (Lemma 3.1, strict under the composite order):
142
297
  * - Successful execution: the block list emptied — boundKey === B's key
143
298
  * and vertices holds every v with key(v) < B reachable through S.
@@ -147,48 +302,60 @@ class BMSSP {
147
302
  * the returned bound's length (never exceed it).
148
303
  * Every returned vertex is complete either way.
149
304
  *
305
+ * @public
150
306
  * @param {number} l - Recursion level; 0 delegates to baseCase
151
307
  * @param {number|[number, number, *]} B - Strict upper bound on the keys
152
308
  * in scope: a number (Infinity is allowed) or a composite bound
153
- * @param {Set<*>} S - Non-empty set of complete frontier sources
309
+ * @param {Set<*>} S - Non-empty set of complete frontier sources (ids)
154
310
  * @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
155
311
  * The boundary B' <= B (same kind as the B passed in: scalar callers
156
312
  * get a scalar), its composite key, and the set U of vertices
157
- * completed below it
313
+ * completed below it — all in original-id space
158
314
  */
159
315
  bmssp(l, B, S) {
160
- const dHat = this.shortestPaths;
161
- const ties = this.ties;
162
- const boundKey = toBound(B);
163
- 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);
164
333
  // Project the composite result back to the caller's kind: a successful
165
334
  // execution echoes B itself, a partial one reports the separator (whose
166
335
  // length is strictly below a scalar B by construction)
167
- const finish = (finalKey, vertices) => ({
168
- bound: scalarB
169
- ? compareKeys(finalKey, boundKey) === 0
336
+ const bound =
337
+ typeof B === "number"
338
+ ? compareKeys(result.boundKey, boundKey) === 0
170
339
  ? B
171
340
  : finalKey[0]
172
- : finalKey,
173
- boundKey: finalKey,
174
- vertices,
175
- });
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;
176
350
 
177
351
  if (l === 0) {
178
- const result = baseCase(boundKey, S, dHat, this.adjacency, this.k, ties);
179
- 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 };
180
354
  }
181
355
 
182
356
  // Shrink the frontier: only the pivots are worth recursing on, and W
183
357
  // is a batch of already-completed vertices folded in at the end
184
- const { pivots, W } = findPivots(
185
- boundKey,
186
- S,
187
- dHat,
188
- this.adjacency,
189
- this.k,
190
- ties,
191
- );
358
+ const { pivots, W } = findPivots(boundKey, S, labels, this.csr, this.k);
192
359
 
193
360
  // Seed the Lemma 3.3 block list with the pivots. lastBoundKey tracks
194
361
  // the paper's Bi': B when P is empty, min key over P before the first
@@ -199,7 +366,7 @@ class BMSSP {
199
366
  const D = new BlockList(2 ** ((l - 1) * this.t), boundKey, compareKeys);
200
367
  let lastBoundKey = boundKey;
201
368
  for (const x of pivots) {
202
- const key = orderKey(x, dHat, ties);
369
+ const key = labelKey(x, labels);
203
370
  if (compareKeys(key, boundKey) < 0) {
204
371
  D.insert(x, key);
205
372
  if (compareKeys(key, lastBoundKey) < 0) lastBoundKey = key;
@@ -208,11 +375,12 @@ class BMSSP {
208
375
 
209
376
  const U = new Set();
210
377
  const workloadCap = this.k * 2 ** (l * this.t);
378
+ const { offsets, targets, weights } = this.csr;
211
379
 
212
380
  while (U.size < workloadCap && !D.isEmpty()) {
213
381
  // Bi, Si <- D.Pull(): the next-closest small batch and its separator
214
382
  const { keys: Si, bound: BiKey } = D.pull();
215
- const child = this.bmssp(l - 1, BiKey, Si);
383
+ const child = this.bmsspIndex(l - 1, BiKey, Si);
216
384
  const BiPrimeKey = child.boundKey;
217
385
  const Ui = child.vertices;
218
386
 
@@ -231,15 +399,12 @@ class BMSSP {
231
399
  // and a key array is built only for the enqueue itself (#168).
232
400
  const K = [];
233
401
  for (const u of Ui) {
234
- const edges = this.adjacency.get(u);
235
- if (edges === undefined) continue;
236
- for (let i = 0; i < edges.length; i += 1) {
237
- const edge = edges[i];
238
- const v = edge[0];
239
- const result = relaxEdge(u, v, edge[1], dHat, ties);
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);
240
405
  if (result === RELAX_LOST || U.has(v)) continue;
241
- const length = dHat.get(v);
242
- const hopCount = ties.hops.get(v) ?? 0;
406
+ const length = labels.dist[v];
407
+ const hopCount = labels.hops[v];
243
408
  if (compareKeyParts(length, hopCount, v, BiKey) >= 0) {
244
409
  if (compareKeyParts(length, hopCount, v, boundKey) < 0) {
245
410
  D.insert(v, [length, hopCount, v]);
@@ -253,8 +418,8 @@ class BMSSP {
253
418
  // go back in front of everything else
254
419
  for (const x of Si) {
255
420
  if (U.has(x)) continue;
256
- const length = dHat.get(x) ?? Infinity;
257
- const hopCount = ties.hops.get(x) ?? 0;
421
+ const length = labels.dist[x];
422
+ const hopCount = labels.hops[x];
258
423
  if (
259
424
  compareKeyParts(length, hopCount, x, BiKey) < 0 &&
260
425
  compareKeyParts(length, hopCount, x, BiPrimeKey) >= 0
@@ -269,12 +434,11 @@ class BMSSP {
269
434
  const finalKey =
270
435
  compareKeys(lastBoundKey, boundKey) < 0 ? lastBoundKey : boundKey;
271
436
  for (const x of W) {
272
- const length = dHat.get(x) ?? Infinity;
273
- if (compareKeyParts(length, ties.hops.get(x) ?? 0, x, finalKey) < 0) {
437
+ if (compareKeyParts(labels.dist[x], labels.hops[x], x, finalKey) < 0) {
274
438
  U.add(x);
275
439
  }
276
440
  }
277
- return finish(finalKey, U);
441
+ return { boundKey: finalKey, vertices: U };
278
442
  }
279
443
 
280
444
  // Method to calculate shortest paths from startNode via the BMSSP
@@ -282,7 +446,8 @@ class BMSSP {
282
446
  // is always a successful execution, so it completes every reachable
283
447
  // vertex; unreachable ones keep their Infinity estimate. Distances, hop
284
448
  // counts and predecessor pointers all end at their canonical values —
285
- // 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).
286
451
  calculateShortestPaths(startNode) {
287
452
  // To clean the state before calculation
288
453
  this.initializeShortestPaths();
@@ -294,9 +459,131 @@ class BMSSP {
294
459
 
295
460
  // The source is complete at distance 0 with zero hops and no
296
461
  // predecessor; everything else is Infinity
297
- this.shortestPaths.set(startNode, 0);
298
- this.hops.set(startNode, 0);
299
- 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
+ }
300
587
  }
301
588
 
302
589
  /**
@@ -304,6 +591,7 @@ class BMSSP {
304
591
  * target. Returns an empty array when target is unreachable or no shortest
305
592
  * path run has completed yet.
306
593
  *
594
+ * @public
307
595
  * @param {number} target - A node in the graph
308
596
  * @returns {number[]} Node IDs from the source through target
309
597
  * @throws {Error} If target is not in the graph