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/README.md CHANGED
@@ -38,17 +38,24 @@ with every building block shipped, tested, and released individually:
38
38
  | Performance-cliff investigation; quadratic `BatchPrepend` fixed ([#182](https://github.com/Sirivasv/bmssp-js/issues/182)) | `src/blockList.mjs` | ✅ done — **1.1.1** |
39
39
  | Exact Lemma 3.3 asymptotics: balanced-BST bound index + linear-time selection ([#167](https://github.com/Sirivasv/bmssp-js/issues/167)) | `src/boundIndex.mjs` + `src/select.mjs` | ✅ done |
40
40
  | Relaxation micro-optimizations: allocation-free hot loops, −13–23% wall-clock ([#168](https://github.com/Sirivasv/bmssp-js/issues/168)) | `src/tieBreak.mjs` + the algorithm modules | ✅ done — **1.2.0** |
41
+ | Dense-index core: typed-array labels + CSR adjacency, ~½ the wall-clock ([#205](https://github.com/Sirivasv/bmssp-js/issues/205)) | `src/bmssp.mjs` (CSR) + `src/tieBreak.mjs` (typed labels) | ✅ done |
42
+ | Typed / flexible graph inputs: `Graph` builder + adjacency Map/object + explicit vertex universe ([#172](https://github.com/Sirivasv/bmssp-js/issues/172)) | `src/graph.mjs` + `BMSSP` constructor | ✅ done |
43
+ | Public multi-source / bounded entrypoint ([#171](https://github.com/Sirivasv/bmssp-js/issues/171)) | `BMSSP.calculateShortestPathsFrom()` | ✅ done |
44
+ | Public API stabilization + 1.0→2.0 migration note ([#173](https://github.com/Sirivasv/bmssp-js/issues/173)) | `MIGRATION.md` + `docs/index.html` + contract test | ✅ done — **2.0.0** |
41
45
 
42
46
  > **Honest note:** the paper's win is asymptotic, and this repo optimizes for correctness
43
- > and readability, not raw speed. Measured head-to-head (algorithm time only, graph
44
- > loading excluded — rerun it yourself with `npm run bench`; methodology and the deep
45
- > 1.0.0 record in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)):
46
- > Dijkstra still wins on wall-clock at every practical size, though the gap narrows as
47
- > sparse graphs grow (2.5× at 50k nodes → 1.57× at 2M). But in the paper's own metric —
47
+ > and readability first but the constant factors have come down a lot. Measured
48
+ > head-to-head (algorithm time only, graph loading excluded — rerun it yourself with
49
+ > `npm run bench`; methodology and the deep 1.0.0 record in
50
+ > [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)): Dijkstra still wins on
51
+ > wall-clock, but after the dense-index engine
52
+ > ([#205](https://github.com/Sirivasv/bmssp-js/issues/205): typed-array labels + CSR
53
+ > adjacency) **BMSSP is within ~1.1–1.4× on sparse graphs** (sparse-random 1.38×,
54
+ > sparse-l4 1.07×, dense 1.16×) — down from ~2.5× before. And in the paper's own metric —
48
55
  > **comparisons between path lengths** — BMSSP does **fewer comparisons than Dijkstra
49
56
  > from under n = 50k on sparse graphs** (`npm run bench:counts` measures 0.95× at 50k →
50
57
  > 0.76× at 200k → **0.65× at 1M**), and the advantage grows with size: the "sorting
51
- > barrier" is measurably broken; what remains is JS constant factors. That crossover
58
+ > barrier" is measurably broken. That crossover
52
59
  > used to sit at ~n = 1M until [#167](https://github.com/Sirivasv/bmssp-js/issues/167)
53
60
  > replaced the block structure's sort-based internals with the paper's exact machinery
54
61
  > (balanced-BST bound index + deterministic linear-time selection).
@@ -74,12 +81,13 @@ with every building block shipped, tested, and released individually:
74
81
  ordered *between* blocks but unsorted *within* them — enough to repeatedly pull the next
75
82
  closest batch without paying the Θ(log n)-per-vertex "sorting barrier."
76
83
 
77
- The wall-clock crossover point is astronomically large, but the asymptotics are real: in
78
- measured comparison counts this implementation already beats Dijkstra from under 50k
79
- nodes on sparse graphs, by a third at 1M
80
- ([benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)). This repo
81
- optimizes for a **correct, readable, well-tested** implementation, validated line-by-line
82
- against a Dijkstra oracle — not for raw speed.
84
+ Dijkstra still wins the wall-clock race, but only by a small margin on sparse graphs now
85
+ (~1.1–1.4× since the dense-index engine); in measured comparison counts this
86
+ implementation already beats Dijkstra from under 50k nodes on sparse graphs, by a third
87
+ at 1M ([benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)). This repo optimizes for
88
+ a **correct, readable, well-tested** implementation, validated line-by-line against a
89
+ Dijkstra oracle first with the constant factors brought down where it doesn't cost
90
+ clarity.
83
91
 
84
92
  ## Installation
85
93
 
@@ -112,7 +120,77 @@ when the target is unreachable (or before any run) and throws for a node outside
112
120
 
113
121
  A reference `dijkstra` implementation is also exported. See the `examples/` directory for
114
122
  more, or the [public API reference](https://sirivasv.github.io/bmssp-js/) for the complete
115
- documented surface.
123
+ documented surface. The public API is **stable as of 2.0.0** — see
124
+ [MIGRATION.md](MIGRATION.md) for the 1.0 → 2.0 note (no breaking changes) and the locked
125
+ surface.
126
+
127
+ ### Flexible graph inputs
128
+
129
+ The constructor also accepts an **adjacency map/object** or a small **`Graph` builder** — all
130
+ equivalent to the edge-array form (node IDs stay finite numbers). The builder is also the way
131
+ to declare an **isolated vertex** (one with no incident edges), which the plain edge list
132
+ can't express:
133
+
134
+ ```javascript
135
+ import { BMSSP, Graph } from "bmssp";
136
+
137
+ // Adjacency object ({ from: [[to, weight], ...] }) or a Map with the same shape:
138
+ new BMSSP({ 0: [[1, 50], [2, 25]], 1: [[2, 75]] });
139
+
140
+ // Or the chainable builder — addVertex declares nodes (incl. isolated ones):
141
+ const g = new Graph()
142
+ .addEdge(0, 1, 50)
143
+ .addEdge(0, 2, 25)
144
+ .addEdge(1, 2, 75)
145
+ .addVertex(9); // an isolated vertex: present, but unreachable (Infinity)
146
+
147
+ const graph = new BMSSP(g);
148
+ graph.calculateShortestPaths(0);
149
+ console.log(graph.shortestPaths.get(2)); // 25
150
+ console.log(graph.shortestPaths.get(9)); // Infinity
151
+ ```
152
+
153
+ (Plain-object keys are strings in JavaScript, so the object form coerces them to numbers;
154
+ use a `Map` or the `Graph` builder if you want to keep numeric keys explicit.)
155
+
156
+ ### Multi-source and bounded runs
157
+
158
+ `calculateShortestPathsFrom(sources, { bound })` runs the paper's `BMSSP(l, B, S)`
159
+ generalization directly: from a **set** of sources, each with an initial distance, optionally
160
+ under a strict distance **bound `B`**. Results land in `shortestPaths` just like
161
+ `calculateShortestPaths` — single-source SSSP is exactly the special case
162
+ `calculateShortestPathsFrom([start])`.
163
+
164
+ ```javascript
165
+ import { BMSSP } from "bmssp";
166
+
167
+ const g = new BMSSP([
168
+ [0, 1, 2],
169
+ [1, 2, 3],
170
+ [5, 2, 1],
171
+ ]);
172
+
173
+ // Nearest of several sources (each seeded at distance 0):
174
+ g.calculateShortestPathsFrom([0, 5]);
175
+ console.log(g.shortestPaths.get(2)); // 1 (via 5 -> 2, beating 0 -> 1 -> 2 = 5)
176
+
177
+ // Sources with explicit initial distances, as pairs / a Map / an object:
178
+ g.calculateShortestPathsFrom([
179
+ [0, 0],
180
+ [5, 10],
181
+ ]);
182
+ console.log(g.shortestPaths.get(2)); // 5 (via 0 -> 1 -> 2; 5's head start no longer wins)
183
+
184
+ // Bounded: only vertices with distance < B are completed; the rest stay Infinity.
185
+ g.calculateShortestPathsFrom([0], { bound: 4 });
186
+ console.log(g.shortestPaths.get(1)); // 2
187
+ console.log(g.shortestPaths.get(2)); // Infinity (distance 5 is not < 4)
188
+ ```
189
+
190
+ Sources accept a `Map<id, dist>`, an object `{ id: dist }`, an array of `[id, dist]` pairs, or
191
+ a bare array of ids (each seeded at distance 0). Initial distances are treated as the sources'
192
+ true (complete) distances — with the common all-zero seeding this is automatic. `bound`
193
+ defaults to `Infinity` (unbounded).
116
194
 
117
195
  ### Optional: constant-degree transform
118
196
 
@@ -142,17 +220,24 @@ picks a valid start copy for an original node.
142
220
 
143
221
  ### Using the Docker image
144
222
 
145
- Run the bundled example:
223
+ The published image is a pre-configured Node environment with `bmssp` installed and the
224
+ [`examples/`](examples/) gallery bundled. Run the whole gallery — basic shortest paths,
225
+ Dijkstra-oracle validation, the constant-degree transform, and a larger generated grid:
226
+
227
+ ```bash
228
+ docker run --rm sirivasv/bmssp-js:latest
229
+ ```
230
+
231
+ Run a single bundled example instead:
146
232
 
147
233
  ```bash
148
- docker run -it sirivasv/bmssp-js:latest
234
+ docker run --rm sirivasv/bmssp-js:latest node examples/02-dijkstra-oracle.mjs
149
235
  ```
150
236
 
151
- Or run your own tests in a pre-configured environment (replace `folder-mytest/` with your
152
- tests folder and `index.mjs` with your test file):
237
+ Or mount your own script and run it in the same environment (no local Node install needed):
153
238
 
154
239
  ```bash
155
- docker run -it -v ./folder-mytest/:/bmssp-js/folder-mytest/ sirivasv/bmssp-js:latest node /bmssp-js/folder-mytest/index.mjs
240
+ docker run --rm -v "$PWD/mine.mjs:/bmssp-js/mine.mjs" sirivasv/bmssp-js:latest node mine.mjs
156
241
  ```
157
242
 
158
243
  Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp-js/tags).
@@ -190,7 +275,7 @@ graphs in a few seconds), and set `FUZZ_XL=1` for an additional 2-million-node r
190
275
  | [`1.0.0`](https://github.com/Sirivasv/bmssp-js/milestones) | First end-to-end functional BMSSP (issues #40–#45) | ✅ done |
191
276
  | `1.1.0` | Correctness hardening — fuzz tests, edge cases, tie-breaking, input validation, constant-degree transform, API docs | ✅ done |
192
277
  | `1.2.0` | Performance & ergonomics — exact Lemma 3.3 asymptotics, BMSSP-vs-Dijkstra benchmarks, cliff investigation, relaxation micro-optimizations | ✅ done |
193
- | `2.0.0` | API generalization — public multi-source/bounded entry point, flexible inputs | 🔨 next |
278
+ | `2.0.0` | API generalization — dense-index engine, typed/flexible inputs, public multi-source/bounded entry point, and public-API stabilization (migration note + locked surface) | done |
194
279
 
195
280
  ## Contributing (humans and AI agents welcome)
196
281
 
package/docs/index.html CHANGED
@@ -141,7 +141,9 @@
141
141
  <pre><code>npm install bmssp</code></pre>
142
142
  <p>
143
143
  The package is ESM-only. Graphs are arrays of <code>[from, to, weight]</code>
144
- edges with finite numeric node IDs and finite, non-negative weights.
144
+ edges with finite numeric node IDs and finite, non-negative weights — or,
145
+ equivalently, an adjacency <code>Map</code>/object or a <code>Graph</code> builder
146
+ (see below).
145
147
  </p>
146
148
 
147
149
  <h2>Quick start</h2>
@@ -158,18 +160,48 @@ graph.shortestPaths; // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
158
160
  graph.reconstructPath(1); // [0, 1]</code></pre>
159
161
 
160
162
  <h2>Public API</h2>
161
- <p>The package has exactly three exports: <code>BMSSP</code>, <code>dijkstra</code>,
162
- and <code>constantDegreeTransform</code>.</p>
163
+ <p>The package has exactly four exports: <code>BMSSP</code>, <code>Graph</code>,
164
+ <code>dijkstra</code>, and <code>constantDegreeTransform</code>. This surface is
165
+ <strong>stable as of 2.0.0</strong> — see the
166
+ <a href="https://github.com/Sirivasv/bmssp-js/blob/main/MIGRATION.md"
167
+ target="_blank" rel="noopener">1.0&nbsp;→&nbsp;2.0 migration note</a> and the
168
+ runnable <a href="https://github.com/Sirivasv/bmssp-js/tree/main/examples"
169
+ target="_blank" rel="noopener">examples gallery</a>. Anything not listed here
170
+ (the block list, the indexed heap, <code>BaseCase</code>/<code>FindPivots</code>,
171
+ and the <code>BMSSP</code> class's dense-index engine members) is internal and may
172
+ change in a minor release.</p>
163
173
 
164
- <h3><code>new BMSSP(graph)</code></h3>
174
+ <h3><code>new BMSSP(input)</code></h3>
165
175
  <p>
166
- Validates and stores the edge list. Every entry must be an exact three-element
167
- <code>[from, to, weight]</code> array; node IDs must be finite numbers and weights
168
- finite and non-negative. Malformed input throws an <code>Error</code> identifying
169
- the offending edge index. An empty edge array is valid (the graph then has no
170
- nodes, so any start node is rejected later).
176
+ Accepts any of four input shapes and reduces them to a canonical graph: an array of
177
+ <code>[from, to, weight]</code> edges, an adjacency <code>Map</code>
178
+ (<code>Map&lt;from, [[to, weight], …]&gt;</code>), a plain adjacency object
179
+ (<code>{ from: [[to, weight], …] }</code>, numeric-string keys coerced to numbers),
180
+ or a <code>Graph</code> builder instance. Node IDs must be finite numbers and
181
+ weights finite and non-negative; malformed edges throw an <code>Error</code>
182
+ identifying the offending index. An empty graph in any shape is valid (it then has
183
+ no nodes, so any start node is rejected later).
171
184
  </p>
172
185
 
186
+ <h3><code>new Graph()</code></h3>
187
+ <p>
188
+ A small mutable input builder. <code>addVertex(id)</code> declares a vertex —
189
+ including an <strong>isolated</strong> one (present but with no incident edges),
190
+ which a bare edge list can't express — and <code>addEdge(from, to, weight)</code>
191
+ adds a directed weighted edge, auto-declaring its endpoints. Both validate eagerly
192
+ and chain; pass the instance to <code>new BMSSP(graph)</code>.
193
+ </p>
194
+ <pre><code>import { BMSSP, Graph } from "bmssp";
195
+
196
+ const g = new Graph()
197
+ .addEdge(0, 1, 50)
198
+ .addEdge(0, 2, 25)
199
+ .addVertex(9); // an isolated vertex — present, but unreachable
200
+
201
+ const graph = new BMSSP(g);
202
+ graph.calculateShortestPaths(0);
203
+ graph.shortestPaths.get(9); // Infinity</code></pre>
204
+
173
205
  <h3><code>graph.calculateShortestPaths(source)</code></h3>
174
206
  <p>
175
207
  Computes shortest distances from <code>source</code> via the paper's method
@@ -183,6 +215,37 @@ graph.reconstructPath(1); // [0, 1]</code></pre>
183
215
  recomputes.
184
216
  </p>
185
217
 
218
+ <h3><code>graph.calculateShortestPathsFrom(sources, { bound })</code></h3>
219
+ <p>
220
+ The paper's multi-source, bounded generalization as public API: runs from a
221
+ <strong>set</strong> of sources, each with an initial distance, optionally under a
222
+ strict distance <code>bound</code> <em>B</em>. <code>sources</code> accepts a
223
+ <code>Map&lt;id, dist&gt;</code>, an object <code>{ id: dist }</code>, an array of
224
+ <code>[id, dist]</code> pairs, or a bare array of ids (each seeded at distance 0).
225
+ Single-source SSSP is the special case
226
+ <code>calculateShortestPathsFrom([start])</code>. Results land in
227
+ <code>shortestPaths</code> exactly like <code>calculateShortestPaths</code>. Under a
228
+ <strong>finite</strong> <code>bound</code>, only the completed set — vertices with
229
+ distance <em>&lt; B</em> — is exposed; BMSSP's above-<em>B</em> over-estimates are
230
+ pruned to <code>Infinity</code>. <code>bound</code> defaults to <code>Infinity</code>.
231
+ </p>
232
+ <pre><code>const g = new BMSSP([[0, 1, 2], [1, 2, 3], [5, 2, 1]]);
233
+
234
+ g.calculateShortestPathsFrom([0, 5]); // nearest of two sources
235
+ g.shortestPaths.get(2); // 1 (via 5 -> 2)
236
+
237
+ g.calculateShortestPathsFrom([0], { bound: 4 }); // bounded
238
+ g.shortestPaths.get(2); // Infinity (distance 5 ≥ 4)</code></pre>
239
+
240
+ <div class="note">
241
+ <strong>Advanced:</strong> <code>graph.bmssp(l, B, S)</code> is the low-level
242
+ bounded multi-source primitive that <code>calculateShortestPathsFrom</code> wraps.
243
+ It works in composite <code>[length, hops, id]</code> key space and returns
244
+ <code>{ bound, boundKey, vertices }</code> (the paper's <em>B′</em>, its key, and
245
+ the completed set <em>U</em>). Most callers want
246
+ <code>calculateShortestPathsFrom</code> instead.
247
+ </div>
248
+
186
249
  <h3><code>graph.reconstructPath(target)</code></h3>
187
250
  <p>
188
251
  Returns the canonical shortest path from the most recent source to
@@ -221,18 +284,22 @@ t.collapse(g.shortestPaths); // Map(3) { 0 => 0, 1 => 50, 2 => 25
221
284
 
222
285
  <div class="note">
223
286
  Algorithm internals — the Lemma 3.3 block list, the indexed min-heap,
224
- <code>BaseCase</code>, <code>FindPivots</code>, and the tie-break helpers — are
225
- deliberately <em>not</em> exported. They are implementation details, documented
226
- in-source for contributors.
287
+ <code>BaseCase</code>, <code>FindPivots</code>, the linear-time selection and
288
+ balanced-BST bound index, the tie-break helpers, and the <code>BMSSP</code> class's
289
+ dense-index engine members (<code>csr</code>, <code>labels</code>,
290
+ <code>bmsspIndex</code>, the <code>syncLabels…</code> bridge, …) — are deliberately
291
+ <em>not</em> part of the public surface. They are implementation details, documented
292
+ in-source for contributors, and may change in a minor release.
227
293
  </div>
228
294
 
229
295
  <h2>Correctness &amp; performance</h2>
230
296
  <p>
231
297
  Every release is validated node-by-node against the Dijkstra oracle on thousands
232
298
  of seeded graphs (up to 2 million nodes). The paper's win is asymptotic: measured
233
- head-to-head, Dijkstra still wins wall-clock at practical sizes, but in the
234
- paper's own metric comparisons between path lengths this implementation does
235
- fewer comparisons than Dijkstra from about n&nbsp;=&nbsp;1M on sparse graphs. See
299
+ head-to-head, Dijkstra still wins wall-clock at practical sizes (the dense-index
300
+ engine narrowed the sparse gap to ~1.4×), but in the paper's own metric —
301
+ comparisons between path lengths this implementation does fewer comparisons than
302
+ Dijkstra from well under n&nbsp;=&nbsp;50k on sparse graphs. See
236
303
  <a href="https://github.com/Sirivasv/bmssp-js/blob/main/benchmarks/HEAD-TO-HEAD.md"
237
304
  target="_blank" rel="noopener">benchmarks/HEAD-TO-HEAD.md</a> for the data and
238
305
  methodology.
package/index.mjs CHANGED
@@ -1,16 +1,18 @@
1
- // Public API of the `bmssp` package (ESM-only). Exactly three exports:
2
- // the BMSSP class, the reference `dijkstra`, and the opt-in
3
- // `constantDegreeTransform`. Algorithm-internal modules (the block list,
4
- // the indexed heap, BaseCase, FindPivots, the tie-break helpers) are
5
- // deliberately NOT re-exported — they are implementation details of the
6
- // recursion, not supported public API.
1
+ // Public API of the `bmssp` package (ESM-only). Four exports: the BMSSP
2
+ // class, the reference `dijkstra`, the opt-in `constantDegreeTransform`, and
3
+ // the `Graph` input builder. Algorithm-internal modules (the block list, the
4
+ // indexed heap, BaseCase, FindPivots, the tie-break helpers) are deliberately
5
+ // NOT re-exported — they are implementation details of the recursion, not
6
+ // supported public API.
7
7
 
8
8
  /**
9
9
  * BMSSP — the paper's Algorithm 3 behind a small class API.
10
10
  *
11
- * `new BMSSP(graph)` takes an array of `[from, to, weight]` edges with
12
- * finite numeric node IDs and finite, non-negative weights (an empty array
13
- * is valid; malformed edges throw with the offending index).
11
+ * `new BMSSP(graph)` accepts any of the #172 input shapes: an array of
12
+ * `[from, to, weight]` edges, an adjacency `Map`/object
13
+ * (`{ from: [[to, weight], ...] }`), or a `Graph` builder instance. Node IDs
14
+ * must be finite numbers and weights finite and non-negative; an empty graph
15
+ * is valid and malformed edges throw with the offending index.
14
16
  * `calculateShortestPaths(source)` computes canonical distances into the
15
17
  * `shortestPaths` Map (`Infinity` for unreachable nodes), and
16
18
  * `reconstructPath(target)` returns the canonical shortest path as an
@@ -18,6 +20,14 @@
18
20
  */
19
21
  export { BMSSP } from "./src/bmssp.mjs";
20
22
 
23
+ /**
24
+ * Graph — a small mutable input builder (#172). Declare vertices (including
25
+ * isolated ones via `addVertex`) and directed weighted edges (`addEdge`),
26
+ * then pass the instance to `new BMSSP(graph)`. Mutators chain
27
+ * (`new Graph().addEdge(0, 1, 50).addVertex(9)`). See `src/graph.mjs`.
28
+ */
29
+ export { Graph } from "./src/graph.mjs";
30
+
21
31
  /**
22
32
  * Reference Dijkstra implementation — the oracle the BMSSP test suite is
23
33
  * validated against, exported for comparison and independent checking.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmssp",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Javascript package implementation of the bmssp algorithm.",
5
5
  "main": "index.mjs",
6
6
  "keywords": [
package/src/baseCase.mjs CHANGED
@@ -2,8 +2,7 @@ import { MinHeap } from "./heap.mjs";
2
2
  import {
3
3
  compareKeys,
4
4
  toBound,
5
- makeTies,
6
- orderKey,
5
+ labelKey,
7
6
  relaxEdge,
8
7
  RELAX_LOST,
9
8
  } from "./tieBreak.mjs";
@@ -15,18 +14,21 @@ import {
15
14
  * that stops after settling k + 1 vertices.
16
15
  *
17
16
  * Preconditions (guaranteed by the caller, Algorithm 3):
18
- * - S is a singleton {x} and x is complete (dHat holds its true distance).
17
+ * - S is a singleton {x} and x is complete (labels.dist holds its true
18
+ * distance).
19
19
  * - Every incomplete vertex v with d(v) < B has a shortest path through x.
20
20
  *
21
- * Distance estimates are read from and written into dHat in place exactly
22
- * like the paper's global d̂[·] labels, so improvements made here are visible
23
- * to the levels above. Since #163 the heap is ordered by the composite
24
- * [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
25
- * Assumption 2.1 (distinct path lengths): a settled vertex carries its
26
- * canonical (lexicographically minimal) label, which no later relaxation can
27
- * improve. The settled filter below only skips canonical re-enqueue signals
28
- * (exact label equality from the recorded predecessor), which is what keeps
29
- * zero-weight plateaus quiescent instead of looping.
21
+ * Since #205 the engine works on dense vertex indices: S holds indices, the
22
+ * graph is the class's CSR bundle, and the labels are the shared typed
23
+ * arrays (see tieBreak's makeLabels) updated in place, exactly like the
24
+ * paper's global d̂[·], so improvements made here are visible to the levels
25
+ * above. The heap is ordered by the composite [length, hops, index] keys,
26
+ * which realize the paper's Assumption 2.1 (distinct path lengths): a
27
+ * settled vertex carries its canonical (lexicographically minimal) label,
28
+ * which no later relaxation can improve. The settled filter below only
29
+ * skips canonical re-enqueue signals (exact label equality from the
30
+ * recorded predecessor), which is what keeps zero-weight plateaus quiescent
31
+ * instead of looping.
30
32
  *
31
33
  * Two outcomes:
32
34
  * - Full success (fewer than k + 1 vertices exist under B): every vertex
@@ -34,26 +36,27 @@ import {
34
36
  * - Partial (the k + 1 cap was hit): boundKey = the largest settled key
35
37
  * B' < B, and exactly the k strictly-closer vertices are returned. Under
36
38
  * the composite order the strictly-closer filter is exact — scalar-length
37
- * ties with the boundary vertex are broken by (hops, id), and a returned
38
- * vertex may therefore share the boundary's scalar length (the documented
39
- * d(v) <= bound caveat of the scalar view).
39
+ * ties with the boundary vertex are broken by (hops, index), and a
40
+ * returned vertex may therefore share the boundary's scalar length (the
41
+ * documented d(v) <= bound caveat of the scalar view).
40
42
  * Every returned vertex is complete either way.
41
43
  *
42
44
  * @param {number|[number, number, *]} B - Strict upper bound on the keys to
43
45
  * settle: a number (Infinity is allowed) or a composite bound
44
- * @param {Set<*>|Iterable<*>} S - Singleton set holding the complete source x
45
- * @param {Map<*, number>} dHat - Global distance estimates d̂[·], updated in place
46
- * @param {Map<*, Array<[*, number]>>} adjacency - nodeId -> outgoing [to, weight] edges
46
+ * @param {Set<number>|Iterable<number>} S - Singleton set holding the
47
+ * complete source index x
48
+ * @param {{ dist: Float64Array, hops: Uint32Array, preds: Int32Array }} labels
49
+ * - Engine label state (d̂[·] and tie-break labels), updated in place
50
+ * @param {{ offsets: Uint32Array, targets: Uint32Array, weights: Float64Array }} csr
51
+ * - The graph in CSR layout over dense indices
47
52
  * @param {number} k - Settle cap parameter, >= 1 (floored); the paper's ⌊log^(1/3) n⌋
48
- * @param {{ hops: Map<*, number>, preds: Map<*, *> }} [ties] - Canonical
49
- * tie-break labels updated alongside dHat; fresh throwaway maps by default
50
- * @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
53
+ * @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<number> }}
51
54
  * The boundary B' <= B (same kind as the B passed in), its composite key,
52
- * and the set U of vertices settled below it
55
+ * and the set U of vertex indices settled below it
53
56
  * @throws {Error} If k is not a number >= 1, S is not a singleton, or the
54
57
  * source has no finite distance estimate
55
58
  */
56
- function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
59
+ function baseCase(B, S, labels, csr, k) {
57
60
  if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
58
61
  throw new Error("k must be a number >= 1");
59
62
  }
@@ -63,35 +66,32 @@ function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
63
66
  throw new Error("S must contain exactly one source node");
64
67
  }
65
68
  const [x] = sources;
66
- const sourceDistance = dHat.get(x);
67
- if (typeof sourceDistance !== "number" || !Number.isFinite(sourceDistance)) {
69
+ if (!Number.isFinite(labels.dist[x])) {
68
70
  throw new Error("the source must have a finite distance estimate");
69
71
  }
70
72
  const boundKey = toBound(B);
73
+ const { offsets, targets, weights } = csr;
71
74
 
72
75
  // U0 in the paper: the vertices settled by this call, seeded with x
73
76
  const settled = new Set([x]);
74
77
  const heap = new MinHeap(compareKeys);
75
- heap.insert(x, orderKey(x, dHat, ties));
78
+ heap.insert(x, labelKey(x, labels));
76
79
 
77
80
  while (!heap.isEmpty() && settled.size < cap + 1) {
78
81
  const { key: u } = heap.extractMin();
79
82
  settled.add(u);
80
- const edges = adjacency.get(u);
81
- if (edges === undefined) continue;
82
- for (let i = 0; i < edges.length; i += 1) {
83
- const edge = edges[i];
84
- const v = edge[0];
83
+ for (let e = offsets[u]; e < offsets[u + 1]; e += 1) {
84
+ const v = targets[e];
85
85
  // Canonical relaxation, gated by the bound (the paper's `< B`). An
86
86
  // exact-equality result means u is v's recorded label-setter (v was
87
87
  // labeled by an earlier phase without being completed) and v must be
88
88
  // (re-)enqueued — unless this very call already settled it, which is
89
89
  // what keeps zero-weight plateaus finite.
90
- const result = relaxEdge(u, v, edge[1], dHat, ties, boundKey);
90
+ const result = relaxEdge(u, v, weights[e], labels, boundKey);
91
91
  if (result === RELAX_LOST || settled.has(v)) continue;
92
92
  // Non-lost ⇒ v's stored label IS the candidate; materialize its key
93
93
  // only here, on the enqueue path (#168)
94
- const key = orderKey(v, dHat, ties);
94
+ const key = labelKey(v, labels);
95
95
  if (heap.has(v)) {
96
96
  heap.decreaseKey(v, key);
97
97
  } else {
@@ -109,12 +109,12 @@ function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
109
109
  // vertices strictly below it — exactly k of them, keys being distinct
110
110
  let boundary = null;
111
111
  for (const v of settled) {
112
- const key = orderKey(v, dHat, ties);
112
+ const key = labelKey(v, labels);
113
113
  if (boundary === null || compareKeys(key, boundary) > 0) boundary = key;
114
114
  }
115
115
  const vertices = new Set();
116
116
  for (const v of settled) {
117
- if (compareKeys(orderKey(v, dHat, ties), boundary) < 0) vertices.add(v);
117
+ if (compareKeys(labelKey(v, labels), boundary) < 0) vertices.add(v);
118
118
  }
119
119
  return {
120
120
  bound: typeof B === "number" ? boundary[0] : boundary,