bmssp 0.19.0 → 1.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 +19 -14
- package/package.json +1 -1
- package/src/bmssp.mjs +165 -6
package/README.md
CHANGED
|
@@ -15,9 +15,11 @@ first to beat Dijkstra's O(m + n·log n) bound on sparse directed graphs.
|
|
|
15
15
|
|
|
16
16
|
## Project status
|
|
17
17
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
**The algorithm is functional end-to-end as of `1.0.0`** 🎉 — `calculateShortestPaths()`
|
|
19
|
+
computes distances via the paper's method (FindPivots → block list → recursive BMSSP with
|
|
20
|
+
the bounded base case), validated node-by-node against a Dijkstra oracle, including on a
|
|
21
|
+
real 2-million-node road network. The project was built piece by piece against the paper,
|
|
22
|
+
with every building block shipped, tested, and released individually:
|
|
21
23
|
|
|
22
24
|
| Building block (paper) | Where | Status |
|
|
23
25
|
| --- | --- | --- |
|
|
@@ -27,12 +29,15 @@ tested, and released individually. Current focus: milestone
|
|
|
27
29
|
| Indexed binary min-heap ([#41](https://github.com/Sirivasv/bmssp-js/issues/41)) | `src/heap.mjs` | ✅ done |
|
|
28
30
|
| `BaseCase(B, S)` — Algorithm 2, bounded mini-Dijkstra ([#40](https://github.com/Sirivasv/bmssp-js/issues/40)) | `src/baseCase.mjs` | ✅ done |
|
|
29
31
|
| `FindPivots(B, S)` — Algorithm 1, frontier shrinking ([#44](https://github.com/Sirivasv/bmssp-js/issues/44)) | `src/findPivots.mjs` | ✅ done |
|
|
30
|
-
| `BMSSP(l, B, S)` — Algorithm 3, the main recursion ([#43](https://github.com/Sirivasv/bmssp-js/issues/43)) |
|
|
32
|
+
| `BMSSP(l, B, S)` — Algorithm 3, the main recursion ([#43](https://github.com/Sirivasv/bmssp-js/issues/43)) | `src/bmssp.mjs` | ✅ done — **1.0.0** |
|
|
31
33
|
|
|
32
|
-
> **Honest note:**
|
|
33
|
-
>
|
|
34
|
-
>
|
|
35
|
-
>
|
|
34
|
+
> **Honest note:** the paper's win is asymptotic, and this repo optimizes for correctness
|
|
35
|
+
> and readability, not raw speed. On the full California road network (~2 M nodes, ~5.5 M
|
|
36
|
+
> edges) this implementation currently runs about 2× slower than its own reference
|
|
37
|
+
> Dijkstra — consistent with published experimental studies of the algorithm. Where inputs
|
|
38
|
+
> violate the paper's distinct-path-lengths assumption (e.g. zero-weight edges), documented
|
|
39
|
+
> tie guards keep the results correct
|
|
40
|
+
> ([#163](https://github.com/Sirivasv/bmssp-js/issues/163) tracks a principled tie-break).
|
|
36
41
|
|
|
37
42
|
## How it works (in two ideas)
|
|
38
43
|
|
|
@@ -106,12 +111,12 @@ oracle's — including on `test/roadNet-CA.txt`, a real road network from SNAP.
|
|
|
106
111
|
|
|
107
112
|
## Roadmap
|
|
108
113
|
|
|
109
|
-
| Milestone | Theme |
|
|
110
|
-
| --- | --- |
|
|
111
|
-
| [`1.0.0`](https://github.com/Sirivasv/bmssp-js/milestones) | First end-to-end functional BMSSP (issues #40–#45) |
|
|
112
|
-
| `1.1.0` | Correctness hardening — fuzz tests, edge cases, tie-breaking, input validation |
|
|
113
|
-
| `1.2.0` | Performance & ergonomics — exact Lemma 3.3 asymptotics, path reconstruction, BMSSP-vs-Dijkstra benchmarks |
|
|
114
|
-
| `2.0.0` | API generalization — public multi-source/bounded entry point, flexible inputs |
|
|
114
|
+
| Milestone | Theme | Status |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| [`1.0.0`](https://github.com/Sirivasv/bmssp-js/milestones) | First end-to-end functional BMSSP (issues #40–#45) | ✅ done |
|
|
117
|
+
| `1.1.0` | Correctness hardening — fuzz tests, edge cases, tie-breaking, input validation | 🔨 current focus |
|
|
118
|
+
| `1.2.0` | Performance & ergonomics — exact Lemma 3.3 asymptotics, path reconstruction, BMSSP-vs-Dijkstra benchmarks | planned |
|
|
119
|
+
| `2.0.0` | API generalization — public multi-source/bounded entry point, flexible inputs | planned |
|
|
115
120
|
|
|
116
121
|
Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
117
122
|
[`help wanted` / `good first issue` labels](https://github.com/Sirivasv/bmssp-js/issues).
|
package/package.json
CHANGED
package/src/bmssp.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { baseCase } from "./baseCase.mjs";
|
|
2
|
+
import { findPivots } from "./findPivots.mjs";
|
|
3
|
+
import { BlockList } from "./blockList.mjs";
|
|
2
4
|
|
|
3
5
|
class BMSSP {
|
|
4
6
|
constructor(inputGraph) {
|
|
@@ -27,6 +29,9 @@ class BMSSP {
|
|
|
27
29
|
|
|
28
30
|
// Initialize shortest paths map
|
|
29
31
|
this.initializeShortestPaths();
|
|
32
|
+
|
|
33
|
+
// Derive the paper's k / t parameters and the top recursion level
|
|
34
|
+
this.deriveParameters();
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
// Method to (re)build the adjacency map from this.graph.
|
|
@@ -59,7 +64,162 @@ class BMSSP {
|
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Derive the paper's parameters from n = |V| and store them on the
|
|
69
|
+
* instance:
|
|
70
|
+
* - k = max(1, ⌊(log₂ n)^(1/3)⌋) — Bellman-Ford rounds in FindPivots and
|
|
71
|
+
* the BaseCase settle cap,
|
|
72
|
+
* - t = max(1, ⌊(log₂ n)^(2/3)⌋) — branching / level sizing,
|
|
73
|
+
* - topLevel = max(1, ⌈log₂ n / t⌉) — level of the top BMSSP call.
|
|
74
|
+
* Everything is clamped to >= 1 so tiny graphs stay out of degenerate
|
|
75
|
+
* regimes: correctness never depends on the asymptotics.
|
|
76
|
+
*/
|
|
77
|
+
deriveParameters() {
|
|
78
|
+
const logn = Math.log2(Math.max(2, this.nodeIDs.size));
|
|
79
|
+
this.k = Math.max(1, Math.floor(logn ** (1 / 3)));
|
|
80
|
+
this.t = Math.max(1, Math.floor(logn ** (2 / 3)));
|
|
81
|
+
this.topLevel = Math.max(1, Math.ceil(logn / this.t));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* BMSSP(l, B, S) — Algorithm 3 of "Breaking the Sorting Barrier for
|
|
86
|
+
* Directed Single-Source Shortest Paths": the main bounded multi-source
|
|
87
|
+
* recursion, wiring FindPivots (Algorithm 1), the Lemma 3.3 BlockList and
|
|
88
|
+
* BaseCase (Algorithm 2) together.
|
|
89
|
+
*
|
|
90
|
+
* Preconditions (the top-level call satisfies them trivially):
|
|
91
|
+
* - Every vertex in S is complete (this.shortestPaths holds its true
|
|
92
|
+
* distance), and every incomplete vertex v with d(v) < B has a shortest
|
|
93
|
+
* path through some complete vertex of S.
|
|
94
|
+
*
|
|
95
|
+
* Distance estimates live in this.shortestPaths (the paper's d̂[·]) and
|
|
96
|
+
* are relaxed in place at every level.
|
|
97
|
+
*
|
|
98
|
+
* Two outcomes (Lemma 3.1):
|
|
99
|
+
* - Successful execution: the block list emptied — bound === B and
|
|
100
|
+
* vertices holds every v with d(v) < B reachable through S.
|
|
101
|
+
* - Partial execution: the k·2^(l·t) workload guard tripped — bound < B
|
|
102
|
+
* and vertices holds exactly the v with d(v) < bound.
|
|
103
|
+
* Every returned vertex is complete either way.
|
|
104
|
+
*
|
|
105
|
+
* @param {number} l - Recursion level; 0 delegates to baseCase
|
|
106
|
+
* @param {number} B - Strict upper bound on the distances in scope (Infinity is allowed)
|
|
107
|
+
* @param {Set<*>} S - Non-empty set of complete frontier sources
|
|
108
|
+
* @returns {{ bound: number, vertices: Set<*> }} The boundary B' <= B and
|
|
109
|
+
* the set U of vertices completed below it
|
|
110
|
+
*/
|
|
111
|
+
bmssp(l, B, S) {
|
|
112
|
+
const dHat = this.shortestPaths;
|
|
113
|
+
if (l === 0) {
|
|
114
|
+
return baseCase(B, S, dHat, this.adjacency, this.k);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Shrink the frontier: only the pivots are worth recursing on, and W
|
|
118
|
+
// is a batch of already-completed vertices folded in at the end
|
|
119
|
+
const { pivots, W } = findPivots(B, S, dHat, this.adjacency, this.k);
|
|
120
|
+
|
|
121
|
+
// Seed the Lemma 3.3 block list with the pivots. lastBound tracks the
|
|
122
|
+
// paper's Bi': B when P is empty, min d̂ over P before the first pull,
|
|
123
|
+
// then the boundary returned by the latest recursive call.
|
|
124
|
+
// A pivot with d̂ >= B is out of scope at this level (only possible
|
|
125
|
+
// when equal path lengths violate Assumption 2.1 and a pull returned
|
|
126
|
+
// a key tied with its separator): skip it — the ancestor whose band
|
|
127
|
+
// covers its distance is responsible for it.
|
|
128
|
+
const D = new BlockList(2 ** ((l - 1) * this.t), B);
|
|
129
|
+
let lastBound = B;
|
|
130
|
+
for (const x of pivots) {
|
|
131
|
+
const dx = dHat.get(x);
|
|
132
|
+
if (dx < B) {
|
|
133
|
+
D.insert(x, dx);
|
|
134
|
+
if (dx < lastBound) lastBound = dx;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const U = new Set();
|
|
139
|
+
const workloadCap = this.k * 2 ** (l * this.t);
|
|
140
|
+
|
|
141
|
+
while (U.size < workloadCap && !D.isEmpty()) {
|
|
142
|
+
// Bi, Si <- D.Pull(): the next-closest small batch and its separator
|
|
143
|
+
const { keys: Si, bound: Bi } = D.pull();
|
|
144
|
+
let { bound: BiPrime, vertices: Ui } = this.bmssp(l - 1, Bi, Si);
|
|
145
|
+
|
|
146
|
+
if (Ui.size === 0) {
|
|
147
|
+
// Degenerate tie stall: the child settled only vertices tied
|
|
148
|
+
// exactly at its boundary (possible only through zero-weight
|
|
149
|
+
// paths, which violate the paper's Assumption 2.1 — see #163),
|
|
150
|
+
// so its strict d̂ < B' filter returned nothing and this batch
|
|
151
|
+
// would be re-pulled forever. Escape hatch: settle everything
|
|
152
|
+
// below Bi reachable through the batch with an uncapped bounded
|
|
153
|
+
// Dijkstra per member — correct, just not sublinear.
|
|
154
|
+
Ui = new Set();
|
|
155
|
+
for (const x of Si) {
|
|
156
|
+
const { vertices } = baseCase(
|
|
157
|
+
Bi,
|
|
158
|
+
new Set([x]),
|
|
159
|
+
dHat,
|
|
160
|
+
this.adjacency,
|
|
161
|
+
Math.max(1, this.nodeIDs.size),
|
|
162
|
+
);
|
|
163
|
+
for (const v of vertices) Ui.add(v);
|
|
164
|
+
}
|
|
165
|
+
BiPrime = Bi;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lastBound = BiPrime;
|
|
169
|
+
for (const v of Ui) U.add(v);
|
|
170
|
+
|
|
171
|
+
// Relax out of the newly-completed Ui, routing improved neighbors
|
|
172
|
+
// by distance band: [Bi, B) re-enters the block list, [Bi', Bi) is
|
|
173
|
+
// staged for a batch prepend (closer than the current batch's floor)
|
|
174
|
+
const K = [];
|
|
175
|
+
for (const u of Ui) {
|
|
176
|
+
const du = dHat.get(u);
|
|
177
|
+
for (const [v, weight] of this.adjacency.get(u) ?? []) {
|
|
178
|
+
const candidate = du + weight;
|
|
179
|
+
// Paper relaxation: d̂[u] + w(u,v) <= d̂[v] always updates d̂
|
|
180
|
+
if (candidate <= (dHat.get(v) ?? Infinity)) {
|
|
181
|
+
dHat.set(v, candidate);
|
|
182
|
+
// A vertex already completed at this level cannot strictly
|
|
183
|
+
// improve (non-negative weights), so an equal-sum relaxation
|
|
184
|
+
// — which the <= allows, e.g. via a zero-weight cycle — must
|
|
185
|
+
// not be re-queued (mirrors the baseCase settled guard)
|
|
186
|
+
if (U.has(v)) continue;
|
|
187
|
+
if (candidate >= Bi && candidate < B) {
|
|
188
|
+
D.insert(v, candidate);
|
|
189
|
+
} else if (candidate >= BiPrime && candidate < Bi) {
|
|
190
|
+
K.push([v, candidate]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Batch members the child did not complete (d̂ still in [Bi', Bi))
|
|
196
|
+
// go back in front of everything else. A member tied exactly at the
|
|
197
|
+
// separator (d̂ == Bi < B, an Assumption 2.1 violation) is still in
|
|
198
|
+
// scope at this level and re-enters through a regular insert.
|
|
199
|
+
for (const x of Si) {
|
|
200
|
+
if (U.has(x)) continue;
|
|
201
|
+
const dx = dHat.get(x);
|
|
202
|
+
if (dx >= BiPrime && dx < Bi) {
|
|
203
|
+
K.push([x, dx]);
|
|
204
|
+
} else if (dx === Bi && Bi < B) {
|
|
205
|
+
D.insert(x, dx);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (K.length > 0) D.batchPrepend(K);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// B' <- min(last Bi', B); fold in the FindPivots batch below it
|
|
212
|
+
const bound = Math.min(lastBound, B);
|
|
213
|
+
for (const x of W) {
|
|
214
|
+
if (dHat.get(x) < bound) U.add(x);
|
|
215
|
+
}
|
|
216
|
+
return { bound, vertices: U };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Method to calculate shortest paths from startNode via the BMSSP
|
|
220
|
+
// recursion (Algorithm 3). The top-level call BMSSP(topLevel, ∞, {start})
|
|
221
|
+
// is always a successful execution, so it completes every reachable
|
|
222
|
+
// vertex; unreachable ones keep their Infinity estimate.
|
|
63
223
|
calculateShortestPaths(startNode) {
|
|
64
224
|
// To clean the state before calculation
|
|
65
225
|
this.initializeShortestPaths();
|
|
@@ -69,10 +229,9 @@ class BMSSP {
|
|
|
69
229
|
throw new Error("Start node not found in the graph");
|
|
70
230
|
}
|
|
71
231
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
232
|
+
// The source is complete at distance 0; everything else is Infinity
|
|
233
|
+
this.shortestPaths.set(startNode, 0);
|
|
234
|
+
this.bmssp(this.topLevel, Infinity, new Set([startNode]));
|
|
76
235
|
}
|
|
77
236
|
}
|
|
78
237
|
|