bmssp 1.1.0 → 1.1.1
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 +22 -9
- package/package.json +2 -1
- package/src/blockList.mjs +9 -4
- package/src/tieBreak.mjs +33 -1
package/README.md
CHANGED
|
@@ -34,15 +34,25 @@ with every building block shipped, tested, and released individually:
|
|
|
34
34
|
| Shortest-path reconstruction ([#169](https://github.com/Sirivasv/bmssp-js/issues/169)) | `BMSSP.reconstructPath()` | ✅ done |
|
|
35
35
|
| Constructor input validation ([#165](https://github.com/Sirivasv/bmssp-js/issues/165)) | `BMSSP` constructor | ✅ done |
|
|
36
36
|
| Opt-in constant-degree transform, in/out-degree ≤ 2 ([#164](https://github.com/Sirivasv/bmssp-js/issues/164)) | `constantDegreeTransform()` | ✅ done |
|
|
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
|
+
| Performance-cliff investigation; quadratic `BatchPrepend` fixed ([#182](https://github.com/Sirivasv/bmssp-js/issues/182)) | `src/blockList.mjs` | ✅ done — **1.1.1** |
|
|
37
39
|
|
|
38
40
|
> **Honest note:** the paper's win is asymptotic, and this repo optimizes for correctness
|
|
39
41
|
> and readability, not raw speed. Measured head-to-head (algorithm time only, graph
|
|
40
|
-
> loading excluded —
|
|
42
|
+
> loading excluded — rerun it yourself with `npm run bench`; methodology and the deep
|
|
43
|
+
> 1.0.0 record in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)):
|
|
41
44
|
> Dijkstra still wins on wall-clock at every practical size, though the gap narrows as
|
|
42
45
|
> sparse graphs grow (2.5× at 50k nodes → 1.57× at 2M). But in the paper's own metric —
|
|
43
46
|
> **comparisons between path lengths** — BMSSP does **fewer comparisons than Dijkstra
|
|
44
|
-
> from about n = 1M on sparse graphs** (
|
|
45
|
-
>
|
|
47
|
+
> from about n = 1M on sparse graphs** (`npm run bench:counts` measures the ratio falling
|
|
48
|
+
> 1.20× at 50k → 1.03× at 200k → 0.98× at 1M; 0.91× at 2M in the record), and the
|
|
49
|
+
> advantage grows with size: the "sorting barrier" is measurably broken; what remains is
|
|
50
|
+
> JS constant factors. The two performance cliffs found in the 1.0.0 measurements were
|
|
51
|
+
> run down in [#182](https://github.com/Sirivasv/bmssp-js/issues/182): the star-graph
|
|
52
|
+
> blowup was a quadratic in `BatchPrepend`'s bookkeeping — fixed in `1.1.1` (500k-node
|
|
53
|
+
> star: 61 s → ~3 s) — and the recursion-level step is an inherent, measured ~+24% per
|
|
54
|
+
> extra level (see the addendum in
|
|
55
|
+
> [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)).
|
|
46
56
|
> Where inputs violate the paper's distinct-path-lengths assumption (e.g. zero-weight
|
|
47
57
|
> edges), a principled deterministic tie-break
|
|
48
58
|
> ([#163](https://github.com/Sirivasv/bmssp-js/issues/163)) realizes that assumption in
|
|
@@ -145,14 +155,17 @@ Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp
|
|
|
145
155
|
|
|
146
156
|
```bash
|
|
147
157
|
npm install
|
|
148
|
-
npm test
|
|
149
|
-
npm run lint
|
|
150
|
-
npm run bench
|
|
158
|
+
npm test # Jest suite — every graph seeded, every failure reproducible (~7 s)
|
|
159
|
+
npm run lint # Prettier + ESLint
|
|
160
|
+
npm run bench # BMSSP-vs-Dijkstra head-to-head per graph shape, outputs verified
|
|
161
|
+
npm run bench:counts # …plus comparison-count tables (the paper's own cost metric)
|
|
151
162
|
```
|
|
152
163
|
|
|
153
|
-
The measured BMSSP-vs-Dijkstra head-to-head
|
|
154
|
-
|
|
155
|
-
(
|
|
164
|
+
The benchmark harness runs the measured BMSSP-vs-Dijkstra head-to-head on every
|
|
165
|
+
`npm run bench` ([#170](https://github.com/Sirivasv/bmssp-js/issues/170)); methodology,
|
|
166
|
+
the deep 1.0.0 record (up to n = 4M) and the "when to use which" guidance live in
|
|
167
|
+
[benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md) and
|
|
168
|
+
[benchmarks/README.md](benchmarks/README.md).
|
|
156
169
|
|
|
157
170
|
The test suite's core contract: for every node, BMSSP's distances must equal the Dijkstra
|
|
158
171
|
oracle's. A seeded property/fuzz suite (`test/fuzz.test.mjs`) hammers that contract across
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bmssp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Javascript package implementation of the bmssp algorithm.",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"keywords": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" jest --coverage",
|
|
37
37
|
"bench": "node benchmarks/run.mjs",
|
|
38
|
+
"bench:counts": "node benchmarks/run.mjs --counts",
|
|
38
39
|
"lint": "npm run prettier && npm run eslint",
|
|
39
40
|
"format": "npm run prettier:fix && npm run eslint:fix",
|
|
40
41
|
"eslint": "eslint --max-warnings=0 .",
|
package/src/blockList.mjs
CHANGED
|
@@ -156,9 +156,13 @@ class BlockList {
|
|
|
156
156
|
chunks.push(fresh.slice(i, i + chunkSize));
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
// Materialize the chunks as blocks (ascending, so chunk 0 — the smallest
|
|
160
|
+
// values — ends up frontmost) and prepend them all in one concat. A
|
|
161
|
+
// per-chunk unshift here re-shifts the whole d0 array every time: with a
|
|
162
|
+
// small M the chunk count approaches |L| and the loop turns quadratic —
|
|
163
|
+
// the #182 star-graph blowup (~n single-entry chunks at M = 1).
|
|
164
|
+
const blocks = [];
|
|
165
|
+
for (const chunk of chunks) {
|
|
162
166
|
// Seed the block bound with the first value, then max-update — avoids
|
|
163
167
|
// needing a -Infinity sentinel that a custom comparator can't order
|
|
164
168
|
const block = this.makeBlock(chunk[0][1]);
|
|
@@ -168,8 +172,9 @@ class BlockList {
|
|
|
168
172
|
if (this.compare(value, block.bound) > 0) block.bound = value;
|
|
169
173
|
this.count += 1;
|
|
170
174
|
}
|
|
171
|
-
|
|
175
|
+
blocks.push(block);
|
|
172
176
|
}
|
|
177
|
+
this.d0 = blocks.concat(this.d0);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
/**
|
package/src/tieBreak.mjs
CHANGED
|
@@ -42,6 +42,28 @@
|
|
|
42
42
|
// source's own label never loses an equal-(length, hops) comparison.
|
|
43
43
|
const NO_PRED = -Infinity;
|
|
44
44
|
|
|
45
|
+
// Running count of compareKeys calls — the paper's cost metric ("comparisons
|
|
46
|
+
// between path lengths"). Every BMSSP-side comparison funnels through
|
|
47
|
+
// compareKeys (the heap and BlockList receive it as their comparator, and
|
|
48
|
+
// baseCase/findPivots/bmssp call it directly), so this one counter covers the
|
|
49
|
+
// whole algorithm. The benchmark harness (#170) reads it to measure the
|
|
50
|
+
// sorting barrier; one unconditional increment keeps the hot path branch-free.
|
|
51
|
+
let comparisonCount = 0;
|
|
52
|
+
|
|
53
|
+
/** Reset the compareKeys call counter to zero (benchmark instrumentation). */
|
|
54
|
+
function resetComparisonCount() {
|
|
55
|
+
comparisonCount = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Number of compareKeys calls since the last reset (benchmark
|
|
60
|
+
* instrumentation).
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
function getComparisonCount() {
|
|
64
|
+
return comparisonCount;
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
/**
|
|
46
68
|
* Lexicographic comparison of two composite keys.
|
|
47
69
|
* @param {[number, number, *]} a - [length, hops, id]
|
|
@@ -49,6 +71,7 @@ const NO_PRED = -Infinity;
|
|
|
49
71
|
* @returns {number} Negative when a < b, positive when a > b, 0 when equal
|
|
50
72
|
*/
|
|
51
73
|
function compareKeys(a, b) {
|
|
74
|
+
comparisonCount += 1;
|
|
52
75
|
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
53
76
|
if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
|
|
54
77
|
if (a[2] !== b[2]) return a[2] < b[2] ? -1 : 1;
|
|
@@ -139,4 +162,13 @@ function relaxEdge(u, v, weight, dHat, ties, bound) {
|
|
|
139
162
|
return { key: [length, hopCount, v], improved: true };
|
|
140
163
|
}
|
|
141
164
|
|
|
142
|
-
export {
|
|
165
|
+
export {
|
|
166
|
+
compareKeys,
|
|
167
|
+
toBound,
|
|
168
|
+
makeTies,
|
|
169
|
+
orderKey,
|
|
170
|
+
relaxEdge,
|
|
171
|
+
NO_PRED,
|
|
172
|
+
resetComparisonCount,
|
|
173
|
+
getComparisonCount,
|
|
174
|
+
};
|