bmssp 1.0.1 โ†’ 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,6 +30,10 @@ 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
39
  > and readability, not raw speed. Measured head-to-head (algorithm time only, graph
@@ -40,8 +44,10 @@ with every building block shipped, tested, and released individually:
40
44
  > from about n = 1M on sparse graphs** (0.91ร— at n = 2M), and the advantage grows with
41
45
  > size: the "sorting barrier" is measurably broken; what remains is JS constant factors.
42
46
  > Where inputs violate the paper's distinct-path-lengths assumption (e.g. zero-weight
43
- > edges), documented tie guards keep the results correct
44
- > ([#163](https://github.com/Sirivasv/bmssp-js/issues/163) tracks a principled tie-break).
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.
45
51
 
46
52
  ## How it works (in two ideas)
47
53
 
@@ -68,7 +74,8 @@ npm install bmssp
68
74
  ## Usage
69
75
 
70
76
  The package is ESM-only (`.mjs`). Graphs are arrays of `[from, to, weight]` edges with
71
- 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:
72
79
 
73
80
  ```javascript
74
81
  import { BMSSP } from "bmssp";
@@ -81,10 +88,41 @@ const graph = new BMSSP([
81
88
 
82
89
  graph.calculateShortestPaths(0);
83
90
  console.log(graph.shortestPaths); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
91
+ console.log(graph.reconstructPath(1)); // [0, 1]
84
92
  ```
85
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
+
86
97
  A reference `dijkstra` implementation is also exported. See the `examples/` directory for
87
- 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.
88
126
 
89
127
  ### Using the Docker image
90
128
 
@@ -107,7 +145,7 @@ Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp
107
145
 
108
146
  ```bash
109
147
  npm install
110
- 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)
111
149
  npm run lint # Prettier + ESLint
112
150
  npm run bench # seeded micro-benchmarks (see benchmarks/README.md)
113
151
  ```
@@ -117,27 +155,38 @@ methodology โ€” lives in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md
117
155
  ([#170](https://github.com/Sirivasv/bmssp-js/issues/170) tracks harness integration).
118
156
 
119
157
  The test suite's core contract: for every node, BMSSP's distances must equal the Dijkstra
120
- oracle's โ€” including on `test/roadNet-CA.txt`, a real road network from SNAP. A seeded
121
- property/fuzz suite (`test/fuzz.test.mjs`) hammers that contract across eight graph shapes
122
- (sparse/dense random, grids, chains, stars, DAGs, disconnected forests, multigraphs),
123
- extreme weight regimes (all-zero, mixed zero/huge, exact floats), and direct multi-source
124
- bounded calls checked against per-source oracles. It runs in `npm test` by default; crank
125
- the volume with the `FUZZ_ROUNDS` multiplier (e.g. `FUZZ_ROUNDS=25 npm test -- test/fuzz.test.mjs`
126
- checks several thousand graphs in a few seconds). Failures always report the seed that
127
- produced them.
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.
128
166
 
129
167
  ## Roadmap
130
168
 
131
169
  | Milestone | Theme | Status |
132
170
  | --- | --- | --- |
133
171
  | [`1.0.0`](https://github.com/Sirivasv/bmssp-js/milestones) | First end-to-end functional BMSSP (issues #40โ€“#45) | โœ… done |
134
- | `1.1.0` | Correctness hardening โ€” fuzz tests, edge cases, tie-breaking, input validation | ๐Ÿ”จ current focus |
135
- | `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 |
136
174
  | `2.0.0` | API generalization โ€” public multi-source/bounded entry point, flexible inputs | planned |
137
175
 
176
+ ## Contributing (humans and AI agents welcome)
177
+
138
178
  Contributions are welcome โ€” see [CONTRIBUTING.md](CONTRIBUTING.md) and the
139
179
  [`help wanted` / `good first issue` labels](https://github.com/Sirivasv/bmssp-js/issues).
140
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
+
141
190
  ## Other implementations on GitHub
142
191
 
143
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.1",
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 };