bmssp 1.0.0 โ†’ 1.1.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
@@ -17,8 +17,8 @@ first to beat Dijkstra's O(m + nยทlog n) bound on sparse directed graphs.
17
17
 
18
18
  **The algorithm is functional end-to-end as of `1.0.0`** ๐ŸŽ‰ โ€” `calculateShortestPaths()`
19
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,
20
+ the bounded base case), validated node-by-node against a Dijkstra oracle on thousands of
21
+ seeded graphs โ€” up to 2 million nodes. The project was built piece by piece against the paper,
22
22
  with every building block shipped, tested, and released individually:
23
23
 
24
24
  | Building block (paper) | Where | Status |
@@ -30,14 +30,24 @@ with every building block shipped, tested, and released individually:
30
30
  | `BaseCase(B, S)` โ€” Algorithm 2, bounded mini-Dijkstra ([#40](https://github.com/Sirivasv/bmssp-js/issues/40)) | `src/baseCase.mjs` | โœ… done |
31
31
  | `FindPivots(B, S)` โ€” Algorithm 1, frontier shrinking ([#44](https://github.com/Sirivasv/bmssp-js/issues/44)) | `src/findPivots.mjs` | โœ… done |
32
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** |
33
+ | Deterministic tie-breaking โ€” Assumption 2.1 realized ([#163](https://github.com/Sirivasv/bmssp-js/issues/163)) | `src/tieBreak.mjs` | โœ… done |
34
+ | Shortest-path reconstruction ([#169](https://github.com/Sirivasv/bmssp-js/issues/169)) | `BMSSP.reconstructPath()` | โœ… done |
35
+ | Constructor input validation ([#165](https://github.com/Sirivasv/bmssp-js/issues/165)) | `BMSSP` constructor | โœ… done |
36
+ | Opt-in constant-degree transform, in/out-degree โ‰ค 2 ([#164](https://github.com/Sirivasv/bmssp-js/issues/164)) | `constantDegreeTransform()` | โœ… done |
33
37
 
34
38
  > **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).
39
+ > and readability, not raw speed. Measured head-to-head (algorithm time only, graph
40
+ > loading excluded โ€” see [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)):
41
+ > Dijkstra still wins on wall-clock at every practical size, though the gap narrows as
42
+ > sparse graphs grow (2.5ร— at 50k nodes โ†’ 1.57ร— at 2M). But in the paper's own metric โ€”
43
+ > **comparisons between path lengths** โ€” BMSSP does **fewer comparisons than Dijkstra
44
+ > from about n = 1M on sparse graphs** (0.91ร— at n = 2M), and the advantage grows with
45
+ > size: the "sorting barrier" is measurably broken; what remains is JS constant factors.
46
+ > Where inputs violate the paper's distinct-path-lengths assumption (e.g. zero-weight
47
+ > edges), a principled deterministic tie-break
48
+ > ([#163](https://github.com/Sirivasv/bmssp-js/issues/163)) realizes that assumption in
49
+ > code: paths are ranked by composite `(length, hops, id)` keys, so distances, predecessor
50
+ > pointers and completed sets are identical no matter how the edge list is ordered.
41
51
 
42
52
  ## How it works (in two ideas)
43
53
 
@@ -49,7 +59,9 @@ with every building block shipped, tested, and released individually:
49
59
  ordered *between* blocks but unsorted *within* them โ€” enough to repeatedly pull the next
50
60
  closest batch without paying the ฮ˜(log n)-per-vertex "sorting barrier."
51
61
 
52
- The asymptotic win is theoretical (the crossover point is astronomically large); this repo
62
+ The wall-clock crossover point is astronomically large, but the asymptotics are real: in
63
+ measured comparison counts this implementation already beats Dijkstra past ~1M nodes on
64
+ sparse graphs ([benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)). This repo
53
65
  optimizes for a **correct, readable, well-tested** implementation, validated line-by-line
54
66
  against a Dijkstra oracle โ€” not for raw speed.
55
67
 
@@ -62,7 +74,8 @@ npm install bmssp
62
74
  ## Usage
63
75
 
64
76
  The package is ESM-only (`.mjs`). Graphs are arrays of `[from, to, weight]` edges with
65
- numeric node IDs and non-negative weights:
77
+ finite numeric node IDs and finite, non-negative weights. The constructor rejects malformed
78
+ edges with an error that identifies their array index; an empty edge array remains valid:
66
79
 
67
80
  ```javascript
68
81
  import { BMSSP } from "bmssp";
@@ -75,10 +88,41 @@ const graph = new BMSSP([
75
88
 
76
89
  graph.calculateShortestPaths(0);
77
90
  console.log(graph.shortestPaths); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
91
+ console.log(graph.reconstructPath(1)); // [0, 1]
78
92
  ```
79
93
 
94
+ `reconstructPath(target)` uses the latest `calculateShortestPaths()` run. It returns `[]`
95
+ when the target is unreachable (or before any run) and throws for a node outside the graph.
96
+
80
97
  A reference `dijkstra` implementation is also exported. See the `examples/` directory for
81
- more.
98
+ more, or the [public API reference](https://sirivasv.github.io/bmssp-js/) for the complete
99
+ documented surface.
100
+
101
+ ### Optional: constant-degree transform
102
+
103
+ The paper's bound assumes every vertex has in-degree and out-degree โ‰ค 2. That preprocessing
104
+ is **not** required for correctness here โ€” BMSSP is validated on arbitrary graphs โ€” but it is
105
+ available opt-in via `constantDegreeTransform`, which rewrites any graph into that shape by
106
+ splitting each vertex into a zero-weight cycle of "port" copies. The rewrite is
107
+ distance-preserving, so you run on the transformed graph and fold the result back onto your
108
+ original node IDs:
109
+
110
+ ```javascript
111
+ import { BMSSP, constantDegreeTransform } from "bmssp";
112
+
113
+ const t = constantDegreeTransform([
114
+ [0, 1, 50],
115
+ [0, 2, 25],
116
+ [1, 2, 75],
117
+ ]);
118
+
119
+ const g = new BMSSP(t.edges); // every node now has in/out-degree โ‰ค 2
120
+ g.calculateShortestPaths(t.sourceCopy(0)); // start from a copy of original node 0
121
+ console.log(t.collapse(g.shortestPaths)); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
122
+ ```
123
+
124
+ `collapse` maps the transformed graph's distances back to the original node IDs; `sourceCopy`
125
+ picks a valid start copy for an original node.
82
126
 
83
127
  ### Using the Docker image
84
128
 
@@ -101,26 +145,48 @@ Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp
101
145
 
102
146
  ```bash
103
147
  npm install
104
- npm test # Jest suite, incl. BMSSP-vs-Dijkstra equivalence on a real road network
148
+ npm test # Jest suite โ€” every graph seeded, every failure reproducible (~3 s)
105
149
  npm run lint # Prettier + ESLint
106
150
  npm run bench # seeded micro-benchmarks (see benchmarks/README.md)
107
151
  ```
108
152
 
153
+ The measured BMSSP-vs-Dijkstra head-to-head โ€” wall-clock and comparison counts, with
154
+ methodology โ€” lives in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)
155
+ ([#170](https://github.com/Sirivasv/bmssp-js/issues/170) tracks harness integration).
156
+
109
157
  The test suite's core contract: for every node, BMSSP's distances must equal the Dijkstra
110
- oracle's โ€” including on `test/roadNet-CA.txt`, a real road network from SNAP.
158
+ oracle's. A seeded property/fuzz suite (`test/fuzz.test.mjs`) hammers that contract across
159
+ eight graph shapes (sparse/dense random, grids, chains, stars, DAGs, disconnected forests,
160
+ multigraphs), extreme weight regimes (all-zero, mixed zero/huge, exact floats), direct
161
+ multi-source bounded calls checked against per-source oracles, and seeded scale runs up to
162
+ 150k nodes. It runs in `npm test` by default; crank the volume with the `FUZZ_ROUNDS`
163
+ multiplier (e.g. `FUZZ_ROUNDS=25 npm test -- test/fuzz.test.mjs` checks several thousand
164
+ graphs in a few seconds), and set `FUZZ_XL=1` for an additional 2-million-node round
165
+ (~30 s). Failures always report the seed that produced them.
111
166
 
112
167
  ## Roadmap
113
168
 
114
169
  | Milestone | Theme | Status |
115
170
  | --- | --- | --- |
116
171
  | [`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 |
172
+ | `1.1.0` | Correctness hardening โ€” fuzz tests, edge cases, tie-breaking, input validation, constant-degree transform, API docs | โœ… done |
173
+ | `1.2.0` | Performance & ergonomics โ€” exact Lemma 3.3 asymptotics, BMSSP-vs-Dijkstra benchmarks, performance-cliff investigation | ๐Ÿ”จ current focus |
119
174
  | `2.0.0` | API generalization โ€” public multi-source/bounded entry point, flexible inputs | planned |
120
175
 
176
+ ## Contributing (humans and AI agents welcome)
177
+
121
178
  Contributions are welcome โ€” see [CONTRIBUTING.md](CONTRIBUTING.md) and the
122
179
  [`help wanted` / `good first issue` labels](https://github.com/Sirivasv/bmssp-js/issues).
123
180
 
181
+ This repo is built to be **agent-friendly, model-agnostic**: everything a coding assistant
182
+ needs to contribute is checked in, so you can point **any AI running any model** at the
183
+ project and it can start working **without ever reading the source paper**. Have your agent
184
+ read [`.claude/CLAUDE.md`](.claude/CLAUDE.md) first โ€” it lays out the full working lifecycle
185
+ as literal, follow-along checklists โ€” backed by a self-contained knowledge base under
186
+ [`.claude/knowledge/`](.claude/knowledge/) (a verified transcription of the paper, the
187
+ current codebase map, the roadmap, and a glossary). Prefer to work by hand? The same
188
+ knowledge base reads as plain documentation.
189
+
124
190
  ## Other implementations on GitHub
125
191
 
126
192
  <https://github.com/search?q=bmssp&type=repositories>
package/docs/index.html CHANGED
@@ -2,8 +2,9 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <title>Tsinghua SSSP - BMSSP - Landing Page</title>
5
+ <title>bmssp โ€” BMSSP shortest paths for JavaScript</title>
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="JavaScript implementation of BMSSP, the O(m log^(2/3) n) single-source shortest-paths algorithm โ€” public API documentation.">
7
8
  <style>
8
9
  body {
9
10
  margin: 0;
@@ -14,35 +15,63 @@
14
15
  display: flex;
15
16
  flex-direction: column;
16
17
  align-items: center;
17
- justify-content: center;
18
18
  }
19
19
  .container {
20
20
  background: #fff;
21
21
  border-radius: 18px;
22
22
  box-shadow: 0 4px 24px rgba(0,0,0,0.08);
23
23
  padding: 2.5rem 2rem;
24
- max-width: 420px;
25
- text-align: center;
24
+ margin: 2rem 1rem;
25
+ max-width: 760px;
26
+ width: calc(100% - 2rem);
27
+ box-sizing: border-box;
26
28
  }
29
+ header { text-align: center; }
27
30
  h1 {
28
31
  font-size: 2.2rem;
29
32
  margin-bottom: 0.5rem;
30
33
  color: #3a0ca3;
31
34
  }
32
- p {
33
- font-size: 1.1rem;
34
- margin-bottom: 1.5rem;
35
+ h2 {
36
+ font-size: 1.4rem;
37
+ color: #3a0ca3;
38
+ border-bottom: 2px solid #e0e7ff;
39
+ padding-bottom: 0.3rem;
40
+ margin-top: 2.2rem;
41
+ }
42
+ h3 {
43
+ font-size: 1.05rem;
44
+ color: #22223b;
45
+ margin-bottom: 0.3rem;
46
+ }
47
+ p, li {
48
+ font-size: 1rem;
49
+ line-height: 1.55;
35
50
  color: #4a4e69;
36
51
  }
52
+ .tagline { font-size: 1.1rem; }
53
+ a { color: #4361ee; }
54
+ code {
55
+ font-family: 'Cascadia Code', Consolas, Menlo, monospace;
56
+ font-size: 0.92em;
57
+ background: #eef1ff;
58
+ border-radius: 4px;
59
+ padding: 0.08em 0.35em;
60
+ }
61
+ pre {
62
+ background: #22223b;
63
+ color: #f0fdfa;
64
+ border-radius: 10px;
65
+ padding: 1rem 1.2rem;
66
+ overflow-x: auto;
67
+ }
68
+ pre code { background: none; color: inherit; padding: 0; }
37
69
  .graphics {
38
- margin: 1.5rem 0;
70
+ margin: 1.2rem 0 0.4rem;
39
71
  display: flex;
40
72
  justify-content: center;
41
73
  }
42
- .node-graph {
43
- width: 120px;
44
- height: 120px;
45
- }
74
+ .node-graph { width: 110px; height: 110px; }
46
75
  .btn {
47
76
  display: inline-block;
48
77
  padding: 0.7em 1.5em;
@@ -54,43 +83,164 @@
54
83
  text-decoration: none;
55
84
  transition: background 0.2s;
56
85
  cursor: pointer;
86
+ margin: 0.3rem 0.35rem;
57
87
  }
58
- .btn:hover {
59
- background: #3a0ca3;
88
+ .btn:hover { background: #3a0ca3; }
89
+ .btn.secondary { background: #4cc9f0; color: #22223b; }
90
+ .btn.secondary:hover { background: #3ab8de; }
91
+ .note {
92
+ background: #f0fdfa;
93
+ border-left: 4px solid #4cc9f0;
94
+ border-radius: 0 8px 8px 0;
95
+ padding: 0.7rem 1rem;
96
+ margin: 1rem 0;
60
97
  }
61
98
  footer {
62
- margin-top: 2rem;
99
+ margin: 0 0 2rem;
63
100
  font-size: 0.95rem;
64
- color: #adb5bd;
101
+ color: #6c757d;
102
+ text-align: center;
65
103
  }
104
+ footer a { color: #6c757d; }
66
105
  </style>
67
106
  </head>
68
107
  <body>
69
108
  <div class="container">
70
- <h1>Tsinghua SSSP - BMSSP JS</h1>
109
+ <header>
110
+ <h1>bmssp</h1>
111
+ <p class="tagline">
112
+ A JavaScript (ESM) implementation of <strong>BMSSP</strong> โ€”
113
+ <strong>B</strong>ounded <strong>M</strong>ulti-<strong>S</strong>ource
114
+ <strong>S</strong>hortest <strong>P</strong>aths โ€” the deterministic
115
+ O(mยทlog<sup>2/3</sup> n) single-source shortest-paths algorithm from
116
+ <a href="https://dl.acm.org/doi/10.1145/3717823.3718179" target="_blank" rel="noopener">
117
+ โ€œBreaking the Sorting Barrier for Directed Single-Source Shortest Pathsโ€</a>
118
+ (Duan, Mao, Mao, Shu, Yin โ€” 2025), the first to beat Dijkstra on sparse directed graphs.
119
+ </p>
120
+ <div class="graphics">
121
+ <!-- SVG: stylized graph with nodes and edges -->
122
+ <svg class="node-graph" viewBox="0 0 120 120" role="img" aria-label="Stylized graph">
123
+ <line x1="60" y1="20" x2="30" y2="60" stroke="#adb5bd" stroke-width="3"/>
124
+ <line x1="60" y1="20" x2="90" y2="60" stroke="#adb5bd" stroke-width="3"/>
125
+ <line x1="30" y1="60" x2="60" y2="100" stroke="#adb5bd" stroke-width="3"/>
126
+ <line x1="90" y1="60" x2="60" y2="100" stroke="#adb5bd" stroke-width="3"/>
127
+ <circle cx="60" cy="20" r="12" fill="#4361ee" />
128
+ <circle cx="30" cy="60" r="10" fill="#4cc9f0" />
129
+ <circle cx="90" cy="60" r="10" fill="#4cc9f0" />
130
+ <circle cx="60" cy="100" r="12" fill="#3a0ca3" />
131
+ </svg>
132
+ </div>
133
+ <p>
134
+ <a class="btn" href="https://github.com/Sirivasv/bmssp-js" target="_blank" rel="noopener">GitHub</a>
135
+ <a class="btn secondary" href="https://www.npmjs.com/package/bmssp" target="_blank" rel="noopener">npm</a>
136
+ <a class="btn secondary" href="https://hub.docker.com/r/sirivasv/bmssp-js" target="_blank" rel="noopener">Docker&nbsp;Hub</a>
137
+ </p>
138
+ </header>
139
+
140
+ <h2>Install</h2>
141
+ <pre><code>npm install bmssp</code></pre>
71
142
  <p>
72
- Implementation of the Single Source Shortest Path (SSSP) algorithm - Bounded Multiple Source Shortest Paths (BMSSP), originally published by Tsinghua University's research.
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.
73
145
  </p>
146
+
147
+ <h2>Quick start</h2>
148
+ <pre><code>import { BMSSP } from "bmssp";
149
+
150
+ const graph = new BMSSP([
151
+ [0, 1, 50],
152
+ [1, 2, 75],
153
+ [0, 2, 25],
154
+ ]);
155
+
156
+ graph.calculateShortestPaths(0);
157
+ graph.shortestPaths; // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
158
+ graph.reconstructPath(1); // [0, 1]</code></pre>
159
+
160
+ <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
+
164
+ <h3><code>new BMSSP(graph)</code></h3>
74
165
  <p>
75
- Read the original paper:
76
- <a href="https://dl.acm.org/doi/10.1145/3717823.3718179" target="_blank">
77
- Tsinghua SSSP Paper
78
- </a>
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).
79
171
  </p>
80
- <div class="graphics">
81
- <!-- SVG: stylized graph with nodes and edges -->
82
- <svg class="node-graph" viewBox="0 0 120 120">
83
- <circle cx="60" cy="20" r="12" fill="#4361ee" />
84
- <circle cx="30" cy="60" r="10" fill="#4cc9f0" />
85
- <circle cx="90" cy="60" r="10" fill="#4cc9f0" />
86
- <circle cx="60" cy="100" r="12" fill="#3a0ca3" />
87
- <line x1="60" y1="20" x2="30" y2="60" stroke="#adb5bd" stroke-width="3"/>
88
- <line x1="60" y1="20" x2="90" y2="60" stroke="#adb5bd" stroke-width="3"/>
89
- <line x1="30" y1="60" x2="60" y2="100" stroke="#adb5bd" stroke-width="3"/>
90
- <line x1="90" y1="60" x2="60" y2="100" stroke="#adb5bd" stroke-width="3"/>
91
- </svg>
172
+
173
+ <h3><code>graph.calculateShortestPaths(source)</code></h3>
174
+ <p>
175
+ Computes shortest distances from <code>source</code> via the paper's method
176
+ (FindPivots โ†’ block list โ†’ recursive BMSSP with the bounded base case โ€” not by
177
+ calling Dijkstra). Throws if <code>source</code> is not a node of the graph.
178
+ Results land in <code>graph.shortestPaths</code>, a
179
+ <code>Map&lt;nodeId, distance&gt;</code> holding a value for every node โ€”
180
+ <code>Infinity</code> where unreachable. Distances and predecessor pointers are
181
+ canonical: deterministic regardless of edge-list order, even with ties and
182
+ zero-weight edges. Calling it again with a different source resets state and
183
+ recomputes.
184
+ </p>
185
+
186
+ <h3><code>graph.reconstructPath(target)</code></h3>
187
+ <p>
188
+ Returns the canonical shortest path from the most recent source to
189
+ <code>target</code> as an array of node IDs (<code>[source, โ€ฆ, target]</code>).
190
+ Returns <code>[]</code> when <code>target</code> is unreachable or no run has
191
+ completed yet; throws for a node that is not in the graph.
192
+ </p>
193
+
194
+ <h3><code>dijkstra(graph, nodeIDs, source)</code></h3>
195
+ <p>
196
+ The reference Dijkstra implementation used as the test suite's ground-truth
197
+ oracle, exported for comparison and independent checking. Takes the raw edge
198
+ array, a <code>Set</code> of node IDs, and a source; returns a
199
+ <code>Map&lt;nodeId, distance&gt;</code> (<code>Infinity</code> if unreachable).
200
+ </p>
201
+
202
+ <h3><code>constantDegreeTransform(graph)</code></h3>
203
+ <p>
204
+ Opt-in preprocessing that rewrites any graph so every vertex has in-degree and
205
+ out-degree โ‰ค 2 (the paper's preliminary assumption) by splitting each vertex into
206
+ a zero-weight cycle of โ€œportโ€ copies. Distance-preserving and never required for
207
+ correctness โ€” BMSSP is validated on arbitrary graphs. Returns
208
+ <code>{ edges, copiesOf, originalOf, sourceCopy, collapse }</code>:
209
+ </p>
210
+ <pre><code>import { BMSSP, constantDegreeTransform } from "bmssp";
211
+
212
+ const t = constantDegreeTransform([
213
+ [0, 1, 50],
214
+ [0, 2, 25],
215
+ [1, 2, 75],
216
+ ]);
217
+
218
+ const g = new BMSSP(t.edges); // in/out-degree โ‰ค 2 everywhere
219
+ g.calculateShortestPaths(t.sourceCopy(0)); // start from a copy of node 0
220
+ t.collapse(g.shortestPaths); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }</code></pre>
221
+
222
+ <div class="note">
223
+ 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.
92
227
  </div>
93
- <a class="btn" href="https://github.com/sirivasv/tsinghua-sssp" target="_blank">View on GitHub</a>
228
+
229
+ <h2>Correctness &amp; performance</h2>
230
+ <p>
231
+ Every release is validated node-by-node against the Dijkstra oracle on thousands
232
+ 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
236
+ <a href="https://github.com/Sirivasv/bmssp-js/blob/main/benchmarks/HEAD-TO-HEAD.md"
237
+ target="_blank" rel="noopener">benchmarks/HEAD-TO-HEAD.md</a> for the data and
238
+ methodology.
239
+ </p>
94
240
  </div>
241
+ <footer>
242
+ <a href="https://github.com/Sirivasv/bmssp-js" target="_blank" rel="noopener">bmssp-js</a>
243
+ ยท MPL-2.0 ยท Saul Ivan Rivas Vega
244
+ </footer>
95
245
  </body>
96
- </html></svg>
246
+ </html>
package/index.mjs CHANGED
@@ -1,2 +1,41 @@
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.
7
+
8
+ /**
9
+ * BMSSP โ€” the paper's Algorithm 3 behind a small class API.
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).
14
+ * `calculateShortestPaths(source)` computes canonical distances into the
15
+ * `shortestPaths` Map (`Infinity` for unreachable nodes), and
16
+ * `reconstructPath(target)` returns the canonical shortest path as an
17
+ * array of node IDs. Full JSDoc lives on the class in `src/bmssp.mjs`.
18
+ */
1
19
  export { BMSSP } from "./src/bmssp.mjs";
20
+
21
+ /**
22
+ * Reference Dijkstra implementation โ€” the oracle the BMSSP test suite is
23
+ * validated against, exported for comparison and independent checking.
24
+ *
25
+ * `dijkstra(graph, nodeIDs, source)` returns a `Map` from node ID to
26
+ * shortest distance (`Infinity` if unreachable). See `src/dijkstra.mjs`.
27
+ */
2
28
  export { dijkstra } from "./src/dijkstra.mjs";
29
+
30
+ /**
31
+ * Opt-in constant-degree transform โ€” rewrites any graph so every vertex
32
+ * has in-degree and out-degree <= 2 (the paper's preliminary assumption)
33
+ * by splitting each vertex into a zero-weight cycle of port copies. The
34
+ * rewrite is distance-preserving and never required for correctness.
35
+ *
36
+ * `constantDegreeTransform(graph)` returns
37
+ * `{ edges, copiesOf, originalOf, sourceCopy, collapse }`; run BMSSP on
38
+ * `edges` from `sourceCopy(source)` and `collapse()` the resulting
39
+ * distances back onto original node IDs. See `src/constantDegree.mjs`.
40
+ */
41
+ export { constantDegreeTransform } from "./src/constantDegree.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmssp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Javascript package implementation of the bmssp algorithm.",
5
5
  "main": "index.mjs",
6
6
  "keywords": [
package/src/baseCase.mjs CHANGED
@@ -1,4 +1,11 @@
1
1
  import { MinHeap } from "./heap.mjs";
2
+ import {
3
+ compareKeys,
4
+ toBound,
5
+ makeTies,
6
+ orderKey,
7
+ relaxEdge,
8
+ } from "./tieBreak.mjs";
2
9
 
3
10
  /**
4
11
  * BaseCase(B, S) โ€” Algorithm 2 of "Breaking the Sorting Barrier for Directed
@@ -12,26 +19,40 @@ import { MinHeap } from "./heap.mjs";
12
19
  *
13
20
  * Distance estimates are read from and written into dHat in place โ€” exactly
14
21
  * like the paper's global dฬ‚[ยท] labels, so improvements made here are visible
15
- * to the levels above.
22
+ * to the levels above. Since #163 the heap is ordered by the composite
23
+ * [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
24
+ * Assumption 2.1 (distinct path lengths): a settled vertex carries its
25
+ * canonical (lexicographically minimal) label, which no later relaxation can
26
+ * improve. The settled filter below only skips canonical re-enqueue signals
27
+ * (exact label equality from the recorded predecessor), which is what keeps
28
+ * zero-weight plateaus quiescent instead of looping.
16
29
  *
17
30
  * Two outcomes:
18
31
  * - Full success (fewer than k + 1 vertices exist under B): every vertex
19
- * with d(v) < B is settled and returned, with bound === B.
20
- * - Partial (the k + 1 cap was hit): bound = the largest settled distance
21
- * B' <= B, and only the strictly-closer vertices (dฬ‚ < B') are returned.
32
+ * with key < B is settled and returned, with bound === B.
33
+ * - Partial (the k + 1 cap was hit): boundKey = the largest settled key
34
+ * B' < B, and exactly the k strictly-closer vertices are returned. Under
35
+ * the composite order the strictly-closer filter is exact โ€” scalar-length
36
+ * ties with the boundary vertex are broken by (hops, id), and a returned
37
+ * vertex may therefore share the boundary's scalar length (the documented
38
+ * d(v) <= bound caveat of the scalar view).
22
39
  * Every returned vertex is complete either way.
23
40
  *
24
- * @param {number} B - Strict upper bound on the distances to settle (Infinity is allowed)
41
+ * @param {number|[number, number, *]} B - Strict upper bound on the keys to
42
+ * settle: a number (Infinity is allowed) or a composite bound
25
43
  * @param {Set<*>|Iterable<*>} S - Singleton set holding the complete source x
26
44
  * @param {Map<*, number>} dHat - Global distance estimates dฬ‚[ยท], updated in place
27
45
  * @param {Map<*, Array<[*, number]>>} adjacency - nodeId -> outgoing [to, weight] edges
28
46
  * @param {number} k - Settle cap parameter, >= 1 (floored); the paper's โŒŠlog^(1/3) nโŒ‹
29
- * @returns {{ bound: number, vertices: Set<*> }} The boundary B' <= B and the
30
- * set U of vertices settled below it
47
+ * @param {{ hops: Map<*, number>, preds: Map<*, *> }} [ties] - Canonical
48
+ * tie-break labels updated alongside dHat; fresh throwaway maps by default
49
+ * @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
50
+ * The boundary B' <= B (same kind as the B passed in), its composite key,
51
+ * and the set U of vertices settled below it
31
52
  * @throws {Error} If k is not a number >= 1, S is not a singleton, or the
32
53
  * source has no finite distance estimate
33
54
  */
34
- function baseCase(B, S, dHat, adjacency, k) {
55
+ function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
35
56
  if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
36
57
  throw new Error("k must be a number >= 1");
37
58
  }
@@ -45,50 +66,53 @@ function baseCase(B, S, dHat, adjacency, k) {
45
66
  if (typeof sourceDistance !== "number" || !Number.isFinite(sourceDistance)) {
46
67
  throw new Error("the source must have a finite distance estimate");
47
68
  }
69
+ const boundKey = toBound(B);
48
70
 
49
71
  // U0 in the paper: the vertices settled by this call, seeded with x
50
72
  const settled = new Set([x]);
51
- const heap = new MinHeap();
52
- heap.insert(x, sourceDistance);
73
+ const heap = new MinHeap(compareKeys);
74
+ heap.insert(x, orderKey(x, dHat, ties));
53
75
 
54
76
  while (!heap.isEmpty() && settled.size < cap + 1) {
55
- const { key: u, value: du } = heap.extractMin();
77
+ const { key: u } = heap.extractMin();
56
78
  settled.add(u);
57
79
  for (const [v, weight] of adjacency.get(u) ?? []) {
58
- const candidate = du + weight;
59
- // Paper relaxation: dฬ‚[u] + w(u,v) <= dฬ‚[v], and below the bound B
60
- if (candidate <= (dHat.get(v) ?? Infinity) && candidate < B) {
61
- dHat.set(v, candidate);
62
- // With non-negative weights a vertex settled in this call cannot
63
- // strictly improve, so an equal-sum relaxation (which the <= allows,
64
- // e.g. via a zero-weight cycle) must not re-enter the heap.
65
- if (settled.has(v)) continue;
66
- if (heap.has(v)) {
67
- heap.decreaseKey(v, candidate);
68
- } else {
69
- heap.insert(v, candidate);
70
- }
80
+ // Canonical relaxation, gated by the bound (the paper's `< B`). An
81
+ // exact-equality result means u is v's recorded label-setter (v was
82
+ // labeled by an earlier phase without being completed) and v must be
83
+ // (re-)enqueued โ€” unless this very call already settled it, which is
84
+ // what keeps zero-weight plateaus finite.
85
+ const relaxed = relaxEdge(u, v, weight, dHat, ties, boundKey);
86
+ if (relaxed === null || settled.has(v)) continue;
87
+ if (heap.has(v)) {
88
+ heap.decreaseKey(v, relaxed.key);
89
+ } else {
90
+ heap.insert(v, relaxed.key);
71
91
  }
72
92
  }
73
93
  }
74
94
 
75
95
  if (settled.size <= cap) {
76
96
  // Exhausted before the cap: everything under B is settled (B' = B)
77
- return { bound: B, vertices: settled };
97
+ return { bound: B, boundKey, vertices: settled };
78
98
  }
79
99
 
80
- // Hit the k + 1 cap: report B' = the largest settled distance and return
81
- // only the vertices strictly below it
82
- let boundary = -Infinity;
100
+ // Hit the k + 1 cap: report B' = the largest settled key and return the
101
+ // vertices strictly below it โ€” exactly k of them, keys being distinct
102
+ let boundary = null;
83
103
  for (const v of settled) {
84
- const distance = dHat.get(v);
85
- if (distance > boundary) boundary = distance;
104
+ const key = orderKey(v, dHat, ties);
105
+ if (boundary === null || compareKeys(key, boundary) > 0) boundary = key;
86
106
  }
87
107
  const vertices = new Set();
88
108
  for (const v of settled) {
89
- if (dHat.get(v) < boundary) vertices.add(v);
109
+ if (compareKeys(orderKey(v, dHat, ties), boundary) < 0) vertices.add(v);
90
110
  }
91
- return { bound: boundary, vertices };
111
+ return {
112
+ bound: typeof B === "number" ? boundary[0] : boundary,
113
+ boundKey: boundary,
114
+ vertices,
115
+ };
92
116
  }
93
117
 
94
118
  export { baseCase };