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/README.md +113 -22
- package/docs/index.html +82 -15
- package/index.mjs +19 -9
- package/package.json +1 -1
- package/src/baseCase.mjs +41 -33
- package/src/blockList.mjs +133 -68
- package/src/bmssp.mjs +370 -66
- package/src/boundIndex.mjs +243 -0
- package/src/findPivots.mjs +40 -26
- package/src/graph.mjs +174 -0
- package/src/select.mjs +144 -0
- package/src/tieBreak.mjs +111 -29
package/README.md
CHANGED
|
@@ -36,18 +36,30 @@ with every building block shipped, tested, and released individually:
|
|
|
36
36
|
| Opt-in constant-degree transform, in/out-degree ≤ 2 ([#164](https://github.com/Sirivasv/bmssp-js/issues/164)) | `constantDegreeTransform()` | ✅ done |
|
|
37
37
|
| BMSSP-vs-Dijkstra head-to-head in the benchmark harness ([#170](https://github.com/Sirivasv/bmssp-js/issues/170)) | `npm run bench` / `npm run bench:counts` | ✅ done |
|
|
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
|
+
| 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
|
+
| 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** |
|
|
39
45
|
|
|
40
46
|
> **Honest note:** the paper's win is asymptotic, and this repo optimizes for correctness
|
|
41
|
-
> and readability
|
|
42
|
-
> loading excluded — rerun it yourself with
|
|
43
|
-
> 1.0.0 record in
|
|
44
|
-
> Dijkstra still wins on
|
|
45
|
-
>
|
|
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 —
|
|
46
55
|
> **comparisons between path lengths** — BMSSP does **fewer comparisons than Dijkstra
|
|
47
|
-
> from
|
|
48
|
-
>
|
|
49
|
-
>
|
|
50
|
-
>
|
|
56
|
+
> from under n = 50k on sparse graphs** (`npm run bench:counts` measures 0.95× at 50k →
|
|
57
|
+
> 0.76× at 200k → **0.65× at 1M**), and the advantage grows with size: the "sorting
|
|
58
|
+
> barrier" is measurably broken. That crossover
|
|
59
|
+
> used to sit at ~n = 1M until [#167](https://github.com/Sirivasv/bmssp-js/issues/167)
|
|
60
|
+
> replaced the block structure's sort-based internals with the paper's exact machinery
|
|
61
|
+
> (balanced-BST bound index + deterministic linear-time selection).
|
|
62
|
+
> The two performance cliffs found in the 1.0.0 measurements were
|
|
51
63
|
> run down in [#182](https://github.com/Sirivasv/bmssp-js/issues/182): the star-graph
|
|
52
64
|
> blowup was a quadratic in `BatchPrepend`'s bookkeeping — fixed in `1.1.1` (500k-node
|
|
53
65
|
> star: 61 s → ~3 s) — and the recursion-level step is an inherent, measured ~+24% per
|
|
@@ -69,11 +81,13 @@ with every building block shipped, tested, and released individually:
|
|
|
69
81
|
ordered *between* blocks but unsorted *within* them — enough to repeatedly pull the next
|
|
70
82
|
closest batch without paying the Θ(log n)-per-vertex "sorting barrier."
|
|
71
83
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
sparse graphs
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
77
91
|
|
|
78
92
|
## Installation
|
|
79
93
|
|
|
@@ -106,7 +120,77 @@ when the target is unreachable (or before any run) and throws for a node outside
|
|
|
106
120
|
|
|
107
121
|
A reference `dijkstra` implementation is also exported. See the `examples/` directory for
|
|
108
122
|
more, or the [public API reference](https://sirivasv.github.io/bmssp-js/) for the complete
|
|
109
|
-
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).
|
|
110
194
|
|
|
111
195
|
### Optional: constant-degree transform
|
|
112
196
|
|
|
@@ -136,17 +220,24 @@ picks a valid start copy for an original node.
|
|
|
136
220
|
|
|
137
221
|
### Using the Docker image
|
|
138
222
|
|
|
139
|
-
|
|
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:
|
|
140
232
|
|
|
141
233
|
```bash
|
|
142
|
-
docker run
|
|
234
|
+
docker run --rm sirivasv/bmssp-js:latest node examples/02-dijkstra-oracle.mjs
|
|
143
235
|
```
|
|
144
236
|
|
|
145
|
-
Or
|
|
146
|
-
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):
|
|
147
238
|
|
|
148
239
|
```bash
|
|
149
|
-
docker run
|
|
240
|
+
docker run --rm -v "$PWD/mine.mjs:/bmssp-js/mine.mjs" sirivasv/bmssp-js:latest node mine.mjs
|
|
150
241
|
```
|
|
151
242
|
|
|
152
243
|
Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp-js/tags).
|
|
@@ -183,8 +274,8 @@ graphs in a few seconds), and set `FUZZ_XL=1` for an additional 2-million-node r
|
|
|
183
274
|
| --- | --- | --- |
|
|
184
275
|
| [`1.0.0`](https://github.com/Sirivasv/bmssp-js/milestones) | First end-to-end functional BMSSP (issues #40–#45) | ✅ done |
|
|
185
276
|
| `1.1.0` | Correctness hardening — fuzz tests, edge cases, tie-breaking, input validation, constant-degree transform, API docs | ✅ done |
|
|
186
|
-
| `1.2.0` | Performance & ergonomics — exact Lemma 3.3 asymptotics, BMSSP-vs-Dijkstra benchmarks,
|
|
187
|
-
| `2.0.0` | API generalization — public multi-source/bounded entry point,
|
|
277
|
+
| `1.2.0` | Performance & ergonomics — exact Lemma 3.3 asymptotics, BMSSP-vs-Dijkstra benchmarks, cliff investigation, relaxation micro-optimizations | ✅ done |
|
|
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 |
|
|
188
279
|
|
|
189
280
|
## Contributing (humans and AI agents welcome)
|
|
190
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
|
|
162
|
-
and <code>constantDegreeTransform</code
|
|
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 → 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(
|
|
174
|
+
<h3><code>new BMSSP(input)</code></h3>
|
|
165
175
|
<p>
|
|
166
|
-
|
|
167
|
-
<code>[from, to, weight]</code>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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<from, [[to, weight], …]></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<id, dist></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>< 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>,
|
|
225
|
-
|
|
226
|
-
|
|
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 & 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
|
|
234
|
-
|
|
235
|
-
|
|
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 = 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).
|
|
2
|
-
//
|
|
3
|
-
// `
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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)`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
package/src/baseCase.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { MinHeap } from "./heap.mjs";
|
|
|
2
2
|
import {
|
|
3
3
|
compareKeys,
|
|
4
4
|
toBound,
|
|
5
|
-
|
|
6
|
-
orderKey,
|
|
5
|
+
labelKey,
|
|
7
6
|
relaxEdge,
|
|
7
|
+
RELAX_LOST,
|
|
8
8
|
} from "./tieBreak.mjs";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -14,18 +14,21 @@ import {
|
|
|
14
14
|
* that stops after settling k + 1 vertices.
|
|
15
15
|
*
|
|
16
16
|
* Preconditions (guaranteed by the caller, Algorithm 3):
|
|
17
|
-
* - S is a singleton {x} and x is complete (
|
|
17
|
+
* - S is a singleton {x} and x is complete (labels.dist holds its true
|
|
18
|
+
* distance).
|
|
18
19
|
* - Every incomplete vertex v with d(v) < B has a shortest path through x.
|
|
19
20
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* [
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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.
|
|
29
32
|
*
|
|
30
33
|
* Two outcomes:
|
|
31
34
|
* - Full success (fewer than k + 1 vertices exist under B): every vertex
|
|
@@ -33,26 +36,27 @@ import {
|
|
|
33
36
|
* - Partial (the k + 1 cap was hit): boundKey = the largest settled key
|
|
34
37
|
* B' < B, and exactly the k strictly-closer vertices are returned. Under
|
|
35
38
|
* the composite order the strictly-closer filter is exact — scalar-length
|
|
36
|
-
* ties with the boundary vertex are broken by (hops,
|
|
37
|
-
* vertex may therefore share the boundary's scalar length (the
|
|
38
|
-
* 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).
|
|
39
42
|
* Every returned vertex is complete either way.
|
|
40
43
|
*
|
|
41
44
|
* @param {number|[number, number, *]} B - Strict upper bound on the keys to
|
|
42
45
|
* settle: a number (Infinity is allowed) or a composite bound
|
|
43
|
-
* @param {Set
|
|
44
|
-
*
|
|
45
|
-
* @param {
|
|
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
|
|
46
52
|
* @param {number} k - Settle cap parameter, >= 1 (floored); the paper's ⌊log^(1/3) n⌋
|
|
47
|
-
* @
|
|
48
|
-
* tie-break labels updated alongside dHat; fresh throwaway maps by default
|
|
49
|
-
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
|
|
53
|
+
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<number> }}
|
|
50
54
|
* The boundary B' <= B (same kind as the B passed in), its composite key,
|
|
51
|
-
* and the set U of
|
|
55
|
+
* and the set U of vertex indices settled below it
|
|
52
56
|
* @throws {Error} If k is not a number >= 1, S is not a singleton, or the
|
|
53
57
|
* source has no finite distance estimate
|
|
54
58
|
*/
|
|
55
|
-
function baseCase(B, S,
|
|
59
|
+
function baseCase(B, S, labels, csr, k) {
|
|
56
60
|
if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
|
|
57
61
|
throw new Error("k must be a number >= 1");
|
|
58
62
|
}
|
|
@@ -62,32 +66,36 @@ function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
|
|
|
62
66
|
throw new Error("S must contain exactly one source node");
|
|
63
67
|
}
|
|
64
68
|
const [x] = sources;
|
|
65
|
-
|
|
66
|
-
if (typeof sourceDistance !== "number" || !Number.isFinite(sourceDistance)) {
|
|
69
|
+
if (!Number.isFinite(labels.dist[x])) {
|
|
67
70
|
throw new Error("the source must have a finite distance estimate");
|
|
68
71
|
}
|
|
69
72
|
const boundKey = toBound(B);
|
|
73
|
+
const { offsets, targets, weights } = csr;
|
|
70
74
|
|
|
71
75
|
// U0 in the paper: the vertices settled by this call, seeded with x
|
|
72
76
|
const settled = new Set([x]);
|
|
73
77
|
const heap = new MinHeap(compareKeys);
|
|
74
|
-
heap.insert(x,
|
|
78
|
+
heap.insert(x, labelKey(x, labels));
|
|
75
79
|
|
|
76
80
|
while (!heap.isEmpty() && settled.size < cap + 1) {
|
|
77
81
|
const { key: u } = heap.extractMin();
|
|
78
82
|
settled.add(u);
|
|
79
|
-
for (
|
|
83
|
+
for (let e = offsets[u]; e < offsets[u + 1]; e += 1) {
|
|
84
|
+
const v = targets[e];
|
|
80
85
|
// Canonical relaxation, gated by the bound (the paper's `< B`). An
|
|
81
86
|
// exact-equality result means u is v's recorded label-setter (v was
|
|
82
87
|
// labeled by an earlier phase without being completed) and v must be
|
|
83
88
|
// (re-)enqueued — unless this very call already settled it, which is
|
|
84
89
|
// what keeps zero-weight plateaus finite.
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
90
|
+
const result = relaxEdge(u, v, weights[e], labels, boundKey);
|
|
91
|
+
if (result === RELAX_LOST || settled.has(v)) continue;
|
|
92
|
+
// Non-lost ⇒ v's stored label IS the candidate; materialize its key
|
|
93
|
+
// only here, on the enqueue path (#168)
|
|
94
|
+
const key = labelKey(v, labels);
|
|
87
95
|
if (heap.has(v)) {
|
|
88
|
-
heap.decreaseKey(v,
|
|
96
|
+
heap.decreaseKey(v, key);
|
|
89
97
|
} else {
|
|
90
|
-
heap.insert(v,
|
|
98
|
+
heap.insert(v, key);
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
}
|
|
@@ -101,12 +109,12 @@ function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
|
|
|
101
109
|
// vertices strictly below it — exactly k of them, keys being distinct
|
|
102
110
|
let boundary = null;
|
|
103
111
|
for (const v of settled) {
|
|
104
|
-
const key =
|
|
112
|
+
const key = labelKey(v, labels);
|
|
105
113
|
if (boundary === null || compareKeys(key, boundary) > 0) boundary = key;
|
|
106
114
|
}
|
|
107
115
|
const vertices = new Set();
|
|
108
116
|
for (const v of settled) {
|
|
109
|
-
if (compareKeys(
|
|
117
|
+
if (compareKeys(labelKey(v, labels), boundary) < 0) vertices.add(v);
|
|
110
118
|
}
|
|
111
119
|
return {
|
|
112
120
|
bound: typeof B === "number" ? boundary[0] : boundary,
|