@graphty/algorithms 2.1.2 → 2.2.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 +139 -50
- package/dist/algorithms.js +15260 -10390
- package/dist/algorithms.js.map +1 -1
- package/dist/algorithms.standalone.js +26675 -21542
- package/dist/algorithms.standalone.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +18 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +40 -1
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +20 -0
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +93 -8
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.d.ts +9 -0
- package/dist/src/algorithms/centrality/degree.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +18 -0
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +10 -0
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.js +29 -0
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
- package/dist/src/algorithms/centrality/hits.d.ts +9 -0
- package/dist/src/algorithms/centrality/hits.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/hits.js +23 -0
- package/dist/src/algorithms/centrality/hits.js.map +1 -1
- package/dist/src/algorithms/centrality/katz.d.ts +9 -0
- package/dist/src/algorithms/centrality/katz.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/katz.js +31 -0
- package/dist/src/algorithms/centrality/katz.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +13 -13
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +47 -10
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman-legacy.d.ts +27 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.d.ts.map +1 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.js +335 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.js.map +1 -0
- package/dist/src/algorithms/community/girvan-newman.d.ts +5 -3
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +34 -308
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +9 -6
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +24 -110
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/components/connected.d.ts.map +1 -1
- package/dist/src/algorithms/components/connected.js +28 -77
- package/dist/src/algorithms/components/connected.js.map +1 -1
- package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -1
- package/dist/src/algorithms/mst/kruskal.js +59 -3
- package/dist/src/algorithms/mst/kruskal.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +46 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +27 -0
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +15 -5
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.js +97 -107
- package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +2 -12
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.js +47 -308
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs-variants.js +5 -25
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs.d.ts +0 -3
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +0 -4
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/dfs.js +33 -154
- package/dist/src/algorithms/traversal/dfs.js.map +1 -1
- package/dist/src/clustering/hierarchical-legacy.d.ts +56 -0
- package/dist/src/clustering/hierarchical-legacy.d.ts.map +1 -0
- package/dist/src/clustering/hierarchical-legacy.js +441 -0
- package/dist/src/clustering/hierarchical-legacy.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +7 -39
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +56 -417
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/k-core-legacy.d.ts +30 -0
- package/dist/src/clustering/k-core-legacy.d.ts.map +1 -0
- package/dist/src/clustering/k-core-legacy.js +191 -0
- package/dist/src/clustering/k-core-legacy.js.map +1 -0
- package/dist/src/clustering/k-core.d.ts +8 -7
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +33 -174
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/mcl-legacy.d.ts +42 -0
- package/dist/src/clustering/mcl-legacy.d.ts.map +1 -0
- package/dist/src/clustering/mcl-legacy.js +386 -0
- package/dist/src/clustering/mcl-legacy.js.map +1 -0
- package/dist/src/clustering/mcl.d.ts +9 -32
- package/dist/src/clustering/mcl.d.ts.map +1 -1
- package/dist/src/clustering/mcl.js +48 -374
- package/dist/src/clustering/mcl.js.map +1 -1
- package/dist/src/index.d.ts +29 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/indexed/accelerator.d.ts +141 -11
- package/dist/src/indexed/accelerator.d.ts.map +1 -1
- package/dist/src/indexed/accelerator.js +319 -5
- package/dist/src/indexed/accelerator.js.map +1 -1
- package/dist/src/indexed/all-pairs.d.ts +59 -0
- package/dist/src/indexed/all-pairs.d.ts.map +1 -0
- package/dist/src/indexed/all-pairs.js +274 -0
- package/dist/src/indexed/all-pairs.js.map +1 -0
- package/dist/src/indexed/bellman-ford.d.ts +25 -0
- package/dist/src/indexed/bellman-ford.d.ts.map +1 -0
- package/dist/src/indexed/bellman-ford.js +59 -0
- package/dist/src/indexed/bellman-ford.js.map +1 -0
- package/dist/src/indexed/betweenness.d.ts +85 -0
- package/dist/src/indexed/betweenness.d.ts.map +1 -0
- package/dist/src/indexed/betweenness.js +188 -0
- package/dist/src/indexed/betweenness.js.map +1 -0
- package/dist/src/indexed/bfs.d.ts +69 -2
- package/dist/src/indexed/bfs.d.ts.map +1 -1
- package/dist/src/indexed/bfs.js +148 -1
- package/dist/src/indexed/bfs.js.map +1 -1
- package/dist/src/indexed/bipartite.d.ts +37 -0
- package/dist/src/indexed/bipartite.d.ts.map +1 -0
- package/dist/src/indexed/bipartite.js +54 -0
- package/dist/src/indexed/bipartite.js.map +1 -0
- package/dist/src/indexed/closeness.d.ts +49 -0
- package/dist/src/indexed/closeness.d.ts.map +1 -0
- package/dist/src/indexed/closeness.js +127 -0
- package/dist/src/indexed/closeness.js.map +1 -0
- package/dist/src/indexed/common-neighbors.d.ts +14 -1
- package/dist/src/indexed/common-neighbors.d.ts.map +1 -1
- package/dist/src/indexed/common-neighbors.js +24 -14
- package/dist/src/indexed/common-neighbors.js.map +1 -1
- package/dist/src/indexed/degree.d.ts +19 -0
- package/dist/src/indexed/degree.d.ts.map +1 -0
- package/dist/src/indexed/degree.js +45 -0
- package/dist/src/indexed/degree.js.map +1 -0
- package/dist/src/indexed/delta-pagerank.d.ts +145 -0
- package/dist/src/indexed/delta-pagerank.d.ts.map +1 -0
- package/dist/src/indexed/delta-pagerank.js +428 -0
- package/dist/src/indexed/delta-pagerank.js.map +1 -0
- package/dist/src/indexed/dfs.d.ts +55 -0
- package/dist/src/indexed/dfs.d.ts.map +1 -0
- package/dist/src/indexed/dfs.js +146 -0
- package/dist/src/indexed/dfs.js.map +1 -0
- package/dist/src/indexed/eigenvector.d.ts +48 -0
- package/dist/src/indexed/eigenvector.d.ts.map +1 -0
- package/dist/src/indexed/eigenvector.js +162 -0
- package/dist/src/indexed/eigenvector.js.map +1 -0
- package/dist/src/indexed/facade.d.ts +109 -0
- package/dist/src/indexed/facade.d.ts.map +1 -0
- package/dist/src/indexed/facade.js +204 -0
- package/dist/src/indexed/facade.js.map +1 -0
- package/dist/src/indexed/flow.d.ts +120 -0
- package/dist/src/indexed/flow.d.ts.map +1 -0
- package/dist/src/indexed/flow.js +377 -0
- package/dist/src/indexed/flow.js.map +1 -0
- package/dist/src/indexed/girvan-newman.d.ts +42 -0
- package/dist/src/indexed/girvan-newman.d.ts.map +1 -0
- package/dist/src/indexed/girvan-newman.js +90 -0
- package/dist/src/indexed/girvan-newman.js.map +1 -0
- package/dist/src/indexed/grsbm.d.ts +85 -0
- package/dist/src/indexed/grsbm.d.ts.map +1 -0
- package/dist/src/indexed/grsbm.js +287 -0
- package/dist/src/indexed/grsbm.js.map +1 -0
- package/dist/src/indexed/hierarchical.d.ts +69 -0
- package/dist/src/indexed/hierarchical.d.ts.map +1 -0
- package/dist/src/indexed/hierarchical.js +245 -0
- package/dist/src/indexed/hierarchical.js.map +1 -0
- package/dist/src/indexed/index.d.ts +29 -3
- package/dist/src/indexed/index.d.ts.map +1 -1
- package/dist/src/indexed/index.js +29 -3
- package/dist/src/indexed/index.js.map +1 -1
- package/dist/src/indexed/isomorphism.d.ts +42 -0
- package/dist/src/indexed/isomorphism.d.ts.map +1 -0
- package/dist/src/indexed/isomorphism.js +269 -0
- package/dist/src/indexed/isomorphism.js.map +1 -0
- package/dist/src/indexed/label-propagation.d.ts +134 -0
- package/dist/src/indexed/label-propagation.d.ts.map +1 -0
- package/dist/src/indexed/label-propagation.js +486 -0
- package/dist/src/indexed/label-propagation.js.map +1 -0
- package/dist/src/indexed/leiden.d.ts +52 -0
- package/dist/src/indexed/leiden.d.ts.map +1 -0
- package/dist/src/indexed/leiden.js +384 -0
- package/dist/src/indexed/leiden.js.map +1 -0
- package/dist/src/indexed/link-prediction.d.ts +147 -0
- package/dist/src/indexed/link-prediction.d.ts.map +1 -0
- package/dist/src/indexed/link-prediction.js +317 -0
- package/dist/src/indexed/link-prediction.js.map +1 -0
- package/dist/src/indexed/markov.d.ts +54 -0
- package/dist/src/indexed/markov.d.ts.map +1 -0
- package/dist/src/indexed/markov.js +284 -0
- package/dist/src/indexed/markov.js.map +1 -0
- package/dist/src/indexed/matching.d.ts +53 -0
- package/dist/src/indexed/matching.d.ts.map +1 -0
- package/dist/src/indexed/matching.js +139 -0
- package/dist/src/indexed/matching.js.map +1 -0
- package/dist/src/indexed/min-cut.d.ts +48 -0
- package/dist/src/indexed/min-cut.d.ts.map +1 -0
- package/dist/src/indexed/min-cut.js +175 -0
- package/dist/src/indexed/min-cut.js.map +1 -0
- package/dist/src/indexed/modularity.d.ts +34 -0
- package/dist/src/indexed/modularity.d.ts.map +1 -0
- package/dist/src/indexed/modularity.js +74 -0
- package/dist/src/indexed/modularity.js.map +1 -0
- package/dist/src/indexed/mst.d.ts +29 -1
- package/dist/src/indexed/mst.d.ts.map +1 -1
- package/dist/src/indexed/mst.js +62 -0
- package/dist/src/indexed/mst.js.map +1 -1
- package/dist/src/indexed/pagerank.d.ts +29 -6
- package/dist/src/indexed/pagerank.d.ts.map +1 -1
- package/dist/src/indexed/pagerank.js +107 -11
- package/dist/src/indexed/pagerank.js.map +1 -1
- package/dist/src/indexed/point-to-point.d.ts +53 -0
- package/dist/src/indexed/point-to-point.d.ts.map +1 -0
- package/dist/src/indexed/point-to-point.js +173 -0
- package/dist/src/indexed/point-to-point.js.map +1 -0
- package/dist/src/indexed/scc.d.ts +53 -0
- package/dist/src/indexed/scc.d.ts.map +1 -0
- package/dist/src/indexed/scc.js +110 -0
- package/dist/src/indexed/scc.js.map +1 -0
- package/dist/src/indexed/spectral.d.ts +61 -0
- package/dist/src/indexed/spectral.d.ts.map +1 -0
- package/dist/src/indexed/spectral.js +476 -0
- package/dist/src/indexed/spectral.js.map +1 -0
- package/dist/src/indexed/structures/bit-set.d.ts +45 -0
- package/dist/src/indexed/structures/bit-set.d.ts.map +1 -0
- package/dist/src/indexed/structures/bit-set.js +57 -0
- package/dist/src/indexed/structures/bit-set.js.map +1 -0
- package/dist/src/indexed/structures/max-heap.d.ts +52 -0
- package/dist/src/indexed/structures/max-heap.d.ts.map +1 -0
- package/dist/src/indexed/structures/max-heap.js +66 -0
- package/dist/src/indexed/structures/max-heap.js.map +1 -0
- package/dist/src/indexed/structures/min-heap.d.ts +30 -1
- package/dist/src/indexed/structures/min-heap.d.ts.map +1 -1
- package/dist/src/indexed/structures/min-heap.js +46 -6
- package/dist/src/indexed/structures/min-heap.js.map +1 -1
- package/dist/src/indexed/structures/ring-queue.d.ts +41 -0
- package/dist/src/indexed/structures/ring-queue.d.ts.map +1 -0
- package/dist/src/indexed/structures/ring-queue.js +64 -0
- package/dist/src/indexed/structures/ring-queue.js.map +1 -0
- package/dist/src/indexed/sync.d.ts +50 -0
- package/dist/src/indexed/sync.d.ts.map +1 -0
- package/dist/src/indexed/sync.js +175 -0
- package/dist/src/indexed/sync.js.map +1 -0
- package/dist/src/indexed/terahac.d.ts +58 -0
- package/dist/src/indexed/terahac.d.ts.map +1 -0
- package/dist/src/indexed/terahac.js +244 -0
- package/dist/src/indexed/terahac.js.map +1 -0
- package/dist/src/indexed/to-snapshot.d.ts +45 -2
- package/dist/src/indexed/to-snapshot.d.ts.map +1 -1
- package/dist/src/indexed/to-snapshot.js +140 -6
- package/dist/src/indexed/to-snapshot.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors-legacy.d.ts +71 -0
- package/dist/src/link-prediction/common-neighbors-legacy.d.ts.map +1 -0
- package/dist/src/link-prediction/common-neighbors-legacy.js +172 -0
- package/dist/src/link-prediction/common-neighbors-legacy.js.map +1 -0
- package/dist/src/link-prediction/common-neighbors.d.ts +11 -19
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +56 -123
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/optimized/direction-optimized-bfs.d.ts +6 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -1
- package/dist/src/optimized/direction-optimized-bfs.js +6 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -1
- package/dist/src/research/grsbm-legacy.d.ts +82 -0
- package/dist/src/research/grsbm-legacy.d.ts.map +1 -0
- package/dist/src/research/grsbm-legacy.js +416 -0
- package/dist/src/research/grsbm-legacy.js.map +1 -0
- package/dist/src/research/grsbm.d.ts +9 -66
- package/dist/src/research/grsbm.d.ts.map +1 -1
- package/dist/src/research/grsbm.js +67 -391
- package/dist/src/research/grsbm.js.map +1 -1
- package/dist/src/research/sync-legacy.d.ts +48 -0
- package/dist/src/research/sync-legacy.d.ts.map +1 -0
- package/dist/src/research/sync-legacy.js +337 -0
- package/dist/src/research/sync-legacy.js.map +1 -0
- package/dist/src/research/sync.d.ts +10 -32
- package/dist/src/research/sync.d.ts.map +1 -1
- package/dist/src/research/sync.js +28 -316
- package/dist/src/research/sync.js.map +1 -1
- package/package.json +4 -3
- package/src/algorithms/centrality/betweenness.ts +51 -1
- package/src/algorithms/centrality/closeness.ts +112 -12
- package/src/algorithms/centrality/degree.ts +19 -0
- package/src/algorithms/centrality/eigenvector.ts +34 -0
- package/src/algorithms/centrality/hits.ts +24 -0
- package/src/algorithms/centrality/katz.ts +32 -0
- package/src/algorithms/centrality/pagerank.ts +53 -13
- package/src/algorithms/community/girvan-newman-legacy.ts +414 -0
- package/src/algorithms/community/girvan-newman.ts +36 -386
- package/src/algorithms/community/label-propagation.ts +24 -133
- package/src/algorithms/components/connected.ts +28 -95
- package/src/algorithms/mst/kruskal.ts +65 -4
- package/src/algorithms/shortest-path/bellman-ford.ts +49 -0
- package/src/algorithms/shortest-path/dijkstra.ts +29 -0
- package/src/algorithms/shortest-path/floyd-warshall.ts +106 -136
- package/src/algorithms/traversal/bfs-unified.ts +52 -366
- package/src/algorithms/traversal/bfs-variants.ts +5 -28
- package/src/algorithms/traversal/bfs.ts +0 -4
- package/src/algorithms/traversal/dfs.ts +39 -182
- package/src/clustering/hierarchical-legacy.ts +551 -0
- package/src/clustering/hierarchical.ts +64 -520
- package/src/clustering/k-core-legacy.ts +229 -0
- package/src/clustering/k-core.ts +38 -209
- package/src/clustering/mcl-legacy.ts +498 -0
- package/src/clustering/mcl.ts +46 -479
- package/src/index.ts +90 -3
- package/src/indexed/accelerator.ts +502 -16
- package/src/indexed/all-pairs.ts +342 -0
- package/src/indexed/bellman-ford.ts +72 -0
- package/src/indexed/betweenness.ts +258 -0
- package/src/indexed/bfs.ts +189 -3
- package/src/indexed/bipartite.ts +77 -0
- package/src/indexed/closeness.ts +160 -0
- package/src/indexed/common-neighbors.ts +31 -14
- package/src/indexed/degree.ts +55 -0
- package/src/indexed/delta-pagerank.ts +513 -0
- package/src/indexed/dfs.ts +189 -0
- package/src/indexed/eigenvector.ts +202 -0
- package/src/indexed/facade.ts +229 -0
- package/src/indexed/flow.ts +500 -0
- package/src/indexed/girvan-newman.ts +120 -0
- package/src/indexed/grsbm.ts +390 -0
- package/src/indexed/hierarchical.ts +295 -0
- package/src/indexed/index.ts +94 -3
- package/src/indexed/isomorphism.ts +305 -0
- package/src/indexed/label-propagation.ts +582 -0
- package/src/indexed/leiden.ts +436 -0
- package/src/indexed/link-prediction.ts +399 -0
- package/src/indexed/markov.ts +324 -0
- package/src/indexed/matching.ts +192 -0
- package/src/indexed/min-cut.ts +201 -0
- package/src/indexed/modularity.ts +81 -0
- package/src/indexed/mst.ts +79 -1
- package/src/indexed/pagerank.ts +128 -15
- package/src/indexed/point-to-point.ts +220 -0
- package/src/indexed/scc.ts +132 -0
- package/src/indexed/spectral.ts +550 -0
- package/src/indexed/structures/bit-set.ts +65 -0
- package/src/indexed/structures/max-heap.ts +74 -0
- package/src/indexed/structures/min-heap.ts +51 -6
- package/src/indexed/structures/ring-queue.ts +70 -0
- package/src/indexed/sync.ts +229 -0
- package/src/indexed/terahac.ts +290 -0
- package/src/indexed/to-snapshot.ts +156 -6
- package/src/link-prediction/common-neighbors-legacy.ts +252 -0
- package/src/link-prediction/common-neighbors.ts +65 -168
- package/src/optimized/direction-optimized-bfs.ts +6 -1
- package/src/research/grsbm-legacy.ts +586 -0
- package/src/research/grsbm.ts +76 -555
- package/src/research/sync-legacy.ts +456 -0
- package/src/research/sync.ts +30 -431
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { type F64, type GraphSnapshot, INVALID_INDEX, type NumericVector, type U32 } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
import { PathWalkError } from "../errors.js";
|
|
4
|
+
import { walkPredArcs, walkPredEdges } from "./dijkstra.js";
|
|
5
|
+
import { IndexedMinHeap } from "./structures/min-heap.js";
|
|
6
|
+
|
|
7
|
+
/** The default `maxNodes`: the GPU kernel's ceiling at WebGPU's 128 MiB storage binding, floor(sqrt(2^27 / 4)). */
|
|
8
|
+
export const APSP_DEFAULT_MAX_NODES = 5792;
|
|
9
|
+
|
|
10
|
+
/** Options of the index-based all-pairs shortest paths. @public */
|
|
11
|
+
export interface ApspOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Per-arc weight override, arcCount long; defaults to `s.weights` (null = 1 per arc). The
|
|
14
|
+
* snapshot's weights are f32, whose rounding can change which path is shortest; pass the exact
|
|
15
|
+
* f64 weights here to match the shipped `floydWarshall`.
|
|
16
|
+
*/
|
|
17
|
+
readonly weights?: NumericVector | undefined;
|
|
18
|
+
/** `false` counts hops and ignores every weight. Default true. */
|
|
19
|
+
readonly weighted?: boolean | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Strategy override. `"auto"` (the default): BFS rows on unit weights, Floyd-Warshall on any
|
|
22
|
+
* negative weight, Dijkstra rows below arcCount n^2 / 3, Floyd-Warshall otherwise.
|
|
23
|
+
* `"floyd-warshall"` always sweeps; `"per-source"` runs BFS or Dijkstra rows and throws on a
|
|
24
|
+
* negative weight.
|
|
25
|
+
*/
|
|
26
|
+
readonly method?: "auto" | "floyd-warshall" | "per-source" | undefined;
|
|
27
|
+
/** Record `predArc` so `pathTo` / `pathEdges` work; adds 4 n^2 bytes. Default false. */
|
|
28
|
+
readonly paths?: boolean | undefined;
|
|
29
|
+
/** Refuse larger graphs before allocating. Default 5,792. */
|
|
30
|
+
readonly maxNodes?: number | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** All-pairs shortest paths as a dense row-major matrix. @public */
|
|
34
|
+
export interface ApspResult {
|
|
35
|
+
/** dist[i * n + j] = distance from i to j; +Infinity unreachable, 0 on the diagonal, NaN under a negative cycle. */
|
|
36
|
+
readonly dist: F64;
|
|
37
|
+
/** The side of the matrix, s.nodeCount. */
|
|
38
|
+
readonly n: number;
|
|
39
|
+
/** True when a negative cycle exists; dist is then all NaN and the path accessors throw. */
|
|
40
|
+
readonly hasNegativeCycle: boolean;
|
|
41
|
+
/** The strategy that ran. */
|
|
42
|
+
readonly method: "bfs" | "dijkstra" | "floyd-warshall";
|
|
43
|
+
/** The arc ending each shortest path, row-major; INVALID_INDEX on the diagonal and when unreachable. Null unless `paths: true`. */
|
|
44
|
+
readonly predArc: U32 | null;
|
|
45
|
+
/**
|
|
46
|
+
* Node indices from `source` to `target` inclusive; empty when unreachable.
|
|
47
|
+
* @param source - The row's node index
|
|
48
|
+
* @param target - The column's node index
|
|
49
|
+
*/
|
|
50
|
+
pathTo(source: number, target: number): U32;
|
|
51
|
+
/**
|
|
52
|
+
* Logical edge indices along that path; empty when unreachable or when source === target.
|
|
53
|
+
* @param source - The row's node index
|
|
54
|
+
* @param target - The column's node index
|
|
55
|
+
*/
|
|
56
|
+
pathEdges(source: number, target: number): U32;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The weights in use, after the checks of design section 5.2.
|
|
61
|
+
* @param s - The snapshot
|
|
62
|
+
* @param options - The caller's options
|
|
63
|
+
* @returns The weight vector, or `null` when every weight is 1 (or weights are ignored), and whether one is negative
|
|
64
|
+
*/
|
|
65
|
+
function weightsInUse(s: GraphSnapshot, options: ApspOptions): { w: NumericVector | null; negative: boolean } {
|
|
66
|
+
if (options.weighted === false) {
|
|
67
|
+
return { w: null, negative: false };
|
|
68
|
+
}
|
|
69
|
+
if (options.weights !== undefined && options.weights.length !== s.arcCount) {
|
|
70
|
+
throw new RangeError(
|
|
71
|
+
`allPairsShortestPath: the weights override has ${String(options.weights.length)} entries; the snapshot has ${String(s.arcCount)} arcs`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const w = options.weights ?? s.weights;
|
|
75
|
+
if (w === null) {
|
|
76
|
+
return { w: null, negative: false };
|
|
77
|
+
}
|
|
78
|
+
let unit = true;
|
|
79
|
+
let negative = false;
|
|
80
|
+
for (let a = 0; a < s.arcCount; a++) {
|
|
81
|
+
const x = w[a];
|
|
82
|
+
if (!Number.isFinite(x)) {
|
|
83
|
+
// A finite weight above 3.4e38 reaches the f32 arc column as Infinity.
|
|
84
|
+
const hint =
|
|
85
|
+
options.weights === undefined && !Number.isNaN(x)
|
|
86
|
+
? "; a finite weight above the f32 range (3.4e38) becomes Infinity in the snapshot's f32 arc weights -- pass the exact f64 weights through the weights override"
|
|
87
|
+
: "";
|
|
88
|
+
throw new RangeError(
|
|
89
|
+
`allPairsShortestPath: arc ${String(a)} has weight ${String(x)}; weights must be finite${hint}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
unit &&= x === 1;
|
|
93
|
+
negative ||= x < 0;
|
|
94
|
+
}
|
|
95
|
+
return { w: unit ? null : w, negative };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Floyd-Warshall in place (design section 3.1): k-i-j over the row-major matrix, row offsets
|
|
100
|
+
* hoisted, a row skipped when `d[i][k]` is +Infinity, strict `<` so the smallest pivot wins ties.
|
|
101
|
+
* With a negative weight in play the diagonal is scanned after every round and the sweep stops at
|
|
102
|
+
* the first negative entry (design section 3.3), before any value can run away.
|
|
103
|
+
* @param s - The snapshot
|
|
104
|
+
* @param w - The weights in use, or `null` for 1 per arc
|
|
105
|
+
* @param negative - Whether some weight is negative, so a cycle is possible
|
|
106
|
+
* @param d - The n x n output, overwritten
|
|
107
|
+
* @param p - The n x n predecessor arcs, filled with INVALID_INDEX, or `null` when paths are off
|
|
108
|
+
* @returns True when a negative cycle was found; `d` is then partial
|
|
109
|
+
*/
|
|
110
|
+
function floydWarshall(s: GraphSnapshot, w: NumericVector | null, negative: boolean, d: F64, p: U32 | null): boolean {
|
|
111
|
+
const { nodeCount: n, rowPtr, colIdx } = s;
|
|
112
|
+
d.fill(Infinity);
|
|
113
|
+
for (let u = 0; u < n; u++) {
|
|
114
|
+
const ur = u * n;
|
|
115
|
+
for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
|
|
116
|
+
const v = colIdx[a];
|
|
117
|
+
const x = w === null ? 1 : w[a];
|
|
118
|
+
if (v !== u && x < d[ur + v]) {
|
|
119
|
+
d[ur + v] = x;
|
|
120
|
+
if (p !== null) {
|
|
121
|
+
p[ur + v] = a;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
d[ur + u] = 0;
|
|
126
|
+
}
|
|
127
|
+
for (let k = 0; k < n; k++) {
|
|
128
|
+
const kr = k * n;
|
|
129
|
+
// Row views, not d[kr + j]: 12 to 19 percent faster at every measured size (design section 12).
|
|
130
|
+
const dk = d.subarray(kr, kr + n);
|
|
131
|
+
for (let i = 0; i < n; i++) {
|
|
132
|
+
const ir = i * n;
|
|
133
|
+
const dik = d[ir + k];
|
|
134
|
+
if (dik === Infinity) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const di = d.subarray(ir, ir + n);
|
|
138
|
+
// two copies of the j loop so the common no-paths sweep carries no per-cell branch
|
|
139
|
+
if (p === null) {
|
|
140
|
+
for (let j = 0; j < di.length; j++) {
|
|
141
|
+
const via = dik + dk[j];
|
|
142
|
+
if (via < di[j]) {
|
|
143
|
+
di[j] = via;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const pk = p.subarray(kr, kr + n);
|
|
148
|
+
const pi = p.subarray(ir, ir + n);
|
|
149
|
+
for (let j = 0; j < di.length; j++) {
|
|
150
|
+
const via = dik + dk[j];
|
|
151
|
+
if (via < di[j]) {
|
|
152
|
+
di[j] = via;
|
|
153
|
+
pi[j] = pk[j];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (negative) {
|
|
159
|
+
for (let i = 0; i < n; i++) {
|
|
160
|
+
if (d[i * n + i] < 0) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The negative cycles the weights alone prove (design section 3.3): a negative self-loop, or any
|
|
171
|
+
* negative weight on an undirected snapshot (u-v-u).
|
|
172
|
+
* @param s - The snapshot
|
|
173
|
+
* @param w - The weights in use
|
|
174
|
+
* @returns True when such a cycle exists
|
|
175
|
+
*/
|
|
176
|
+
function negativeCycleFromWeights(s: GraphSnapshot, w: NumericVector): boolean {
|
|
177
|
+
for (let u = 0; u < s.nodeCount; u++) {
|
|
178
|
+
for (let a = s.rowPtr[u]; a < s.rowPtr[u + 1]; a++) {
|
|
179
|
+
if (w[a] < 0 && (!s.directed || s.colIdx[a] === u)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* One breadth-first search per source, hop counts written straight into row `src` of `d`. One
|
|
189
|
+
* queue of n entries serves every source.
|
|
190
|
+
* @param s - The snapshot
|
|
191
|
+
* @param d - The n x n output, overwritten
|
|
192
|
+
* @param p - The n x n predecessor arcs, filled with INVALID_INDEX, or `null`: the discovering arc
|
|
193
|
+
*/
|
|
194
|
+
function bfsRows(s: GraphSnapshot, d: F64, p: U32 | null): void {
|
|
195
|
+
const { nodeCount: n, rowPtr, colIdx } = s;
|
|
196
|
+
const queue = new Uint32Array(n);
|
|
197
|
+
d.fill(Infinity);
|
|
198
|
+
for (let src = 0; src < n; src++) {
|
|
199
|
+
const row = d.subarray(src * n, src * n + n);
|
|
200
|
+
const prow = p?.subarray(src * n, src * n + n);
|
|
201
|
+
row[src] = 0;
|
|
202
|
+
queue[0] = src;
|
|
203
|
+
let tail = 1;
|
|
204
|
+
for (let head = 0; head < tail; head++) {
|
|
205
|
+
const u = queue[head];
|
|
206
|
+
const next = row[u] + 1;
|
|
207
|
+
for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
|
|
208
|
+
const v = colIdx[a];
|
|
209
|
+
if (row[v] === Infinity) {
|
|
210
|
+
row[v] = next;
|
|
211
|
+
if (prow !== undefined) {
|
|
212
|
+
prow[v] = a;
|
|
213
|
+
}
|
|
214
|
+
queue[tail++] = v;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* One Dijkstra per source, relaxing straight into row `src` of `d`. One heap serves every source:
|
|
223
|
+
* a drained heap is empty again. Not `indexed.dijkstra` per source, which would allocate two O(n)
|
|
224
|
+
* arrays, a heap and two closures per source and then copy the row (design section 4).
|
|
225
|
+
* @param s - The snapshot
|
|
226
|
+
* @param w - The weights in use, all non-negative
|
|
227
|
+
* @param d - The n x n output, overwritten
|
|
228
|
+
* @param p - The n x n predecessor arcs, filled with INVALID_INDEX, or `null`: the relaxing arc
|
|
229
|
+
*/
|
|
230
|
+
function dijkstraRows(s: GraphSnapshot, w: NumericVector, d: F64, p: U32 | null): void {
|
|
231
|
+
const { nodeCount: n, rowPtr, colIdx } = s;
|
|
232
|
+
const heap = new IndexedMinHeap(n);
|
|
233
|
+
d.fill(Infinity);
|
|
234
|
+
for (let src = 0; src < n; src++) {
|
|
235
|
+
const row = d.subarray(src * n, src * n + n);
|
|
236
|
+
const prow = p?.subarray(src * n, src * n + n);
|
|
237
|
+
row[src] = 0;
|
|
238
|
+
heap.push(src, 0);
|
|
239
|
+
while (!heap.isEmpty()) {
|
|
240
|
+
const u = heap.pop();
|
|
241
|
+
const du = row[u];
|
|
242
|
+
for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
|
|
243
|
+
const v = colIdx[a];
|
|
244
|
+
const dv = du + w[a];
|
|
245
|
+
if (dv < row[v]) {
|
|
246
|
+
row[v] = dv;
|
|
247
|
+
if (prow !== undefined) {
|
|
248
|
+
prow[v] = a;
|
|
249
|
+
}
|
|
250
|
+
heap.pushOrDecrease(v, dv);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The strategy rule of design section 4.
|
|
259
|
+
* @param s - The snapshot
|
|
260
|
+
* @param w - The weights in use, or `null` for 1 per arc
|
|
261
|
+
* @param negative - Whether some weight is negative
|
|
262
|
+
* @param method - The caller's override
|
|
263
|
+
* @returns The strategy to run
|
|
264
|
+
*/
|
|
265
|
+
function pickStrategy(
|
|
266
|
+
s: GraphSnapshot,
|
|
267
|
+
w: NumericVector | null,
|
|
268
|
+
negative: boolean,
|
|
269
|
+
method: ApspOptions["method"],
|
|
270
|
+
): ApspResult["method"] {
|
|
271
|
+
if (method === "floyd-warshall") {
|
|
272
|
+
return "floyd-warshall";
|
|
273
|
+
}
|
|
274
|
+
if (w === null) {
|
|
275
|
+
return "bfs";
|
|
276
|
+
}
|
|
277
|
+
if (negative) {
|
|
278
|
+
if (method === "per-source") {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'allPairsShortestPath: method "per-source" runs Dijkstra, which is incorrect with a negative weight; use "auto" or "floyd-warshall"',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return "floyd-warshall";
|
|
284
|
+
}
|
|
285
|
+
// The measured crossover (design section 12): n^2 / 3 arcs, above SciPy's n^2 / 4. Integer form, no rounding.
|
|
286
|
+
const n = s.nodeCount;
|
|
287
|
+
return method === "per-source" || 3 * s.arcCount < n * n ? "dijkstra" : "floyd-warshall";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* All-pairs shortest paths over a snapshot.
|
|
292
|
+
* @param s - The snapshot
|
|
293
|
+
* @param options - Weights, strategy, paths and the size bound
|
|
294
|
+
* @returns The distance matrix and the path accessors
|
|
295
|
+
* @public
|
|
296
|
+
*/
|
|
297
|
+
export function allPairsShortestPath(s: GraphSnapshot, options: ApspOptions = {}): ApspResult {
|
|
298
|
+
const n = s.nodeCount;
|
|
299
|
+
const maxNodes = options.maxNodes ?? APSP_DEFAULT_MAX_NODES;
|
|
300
|
+
// Written negated so a NaN maxNodes refuses rather than switching the bound off.
|
|
301
|
+
if (!(n <= maxNodes)) {
|
|
302
|
+
const bytes = (options.paths === true ? 12 : 8) * n * n;
|
|
303
|
+
throw new RangeError(
|
|
304
|
+
`allPairsShortestPath: ${String(n)} nodes exceeds maxNodes ${String(maxNodes)}; the result would allocate ${String(bytes)} bytes. Pass a larger maxNodes to allow it.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const { w, negative } = weightsInUse(s, options);
|
|
308
|
+
const method = pickStrategy(s, w, negative, options.method);
|
|
309
|
+
const dist = new Float64Array(n * n);
|
|
310
|
+
const predArc = options.paths === true ? new Uint32Array(n * n).fill(INVALID_INDEX) : null;
|
|
311
|
+
let hasNegativeCycle = false;
|
|
312
|
+
if (method === "bfs") {
|
|
313
|
+
bfsRows(s, dist, predArc);
|
|
314
|
+
} else if (method === "dijkstra" && w !== null) {
|
|
315
|
+
dijkstraRows(s, w, dist, predArc);
|
|
316
|
+
} else {
|
|
317
|
+
hasNegativeCycle =
|
|
318
|
+
(negative && w !== null && negativeCycleFromWeights(s, w)) || floydWarshall(s, w, negative, dist, predArc);
|
|
319
|
+
}
|
|
320
|
+
if (hasNegativeCycle) {
|
|
321
|
+
dist.fill(NaN);
|
|
322
|
+
}
|
|
323
|
+
// Row i of predArc is a single-source predecessor-arc array, so the SSSP walkers apply to it.
|
|
324
|
+
const row = (source: number, target: number): U32 => {
|
|
325
|
+
if (predArc === null) {
|
|
326
|
+
throw new Error("allPairsShortestPath: pass paths: true to walk shortest paths");
|
|
327
|
+
}
|
|
328
|
+
if (hasNegativeCycle) {
|
|
329
|
+
throw new PathWalkError(source, target, "cycle");
|
|
330
|
+
}
|
|
331
|
+
return predArc.subarray(source * n, source * n + n);
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
dist,
|
|
335
|
+
n,
|
|
336
|
+
hasNegativeCycle,
|
|
337
|
+
method,
|
|
338
|
+
predArc,
|
|
339
|
+
pathTo: (source, target) => walkPredArcs(s, row(source, target), source, target),
|
|
340
|
+
pathEdges: (source, target) => walkPredEdges(s, row(source, target), source, target),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type AdjacencyView, INVALID_INDEX, type NumericVector, type U32 } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
import { type SsspOptions, type SsspResult, walkPredArcs, walkPredEdges } from "./dijkstra.js";
|
|
4
|
+
|
|
5
|
+
/** Result of the index-based Bellman-Ford: the SSSP result plus the negative-cycle flag. @public */
|
|
6
|
+
export interface BellmanFordResult extends SsspResult {
|
|
7
|
+
/**
|
|
8
|
+
* True when a negative cycle is reachable from the source. `dist` and `predArc` then hold the
|
|
9
|
+
* state after `nodeCount - 1` rounds, which are not shortest paths, and a path accessor may throw
|
|
10
|
+
* `PathWalkError` on a predecessor chain that loops.
|
|
11
|
+
*/
|
|
12
|
+
readonly hasNegativeCycle: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Bellman-Ford single-source shortest paths, index-based, for weights that may be negative. It
|
|
17
|
+
* relaxes every arc, so an undirected edge is relaxed both ways (it has an arc in each direction),
|
|
18
|
+
* and one negative undirected edge is therefore a negative cycle. Rounds stop early once one changes
|
|
19
|
+
* nothing; after `nodeCount - 1` rounds one more round decides `hasNegativeCycle`. The predecessor
|
|
20
|
+
* is the relaxing ARC, as in `dijkstra`, so a parallel edge on a path is identified exactly.
|
|
21
|
+
* @param g - The adjacency to search
|
|
22
|
+
* @param source - The node index to start from
|
|
23
|
+
* @param options - Cutoff (a relaxation beyond it is skipped) and per-arc weight override
|
|
24
|
+
* @returns The distances, the predecessor arcs, the path accessors and the negative-cycle flag
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
export function bellmanFord(g: AdjacencyView, source: number, options: SsspOptions = {}): BellmanFordResult {
|
|
28
|
+
const { nodeCount, rowPtr, colIdx } = g;
|
|
29
|
+
const weights: NumericVector | null = options.weights ?? g.weights;
|
|
30
|
+
const cutoff = options.cutoff ?? Infinity;
|
|
31
|
+
const dist = new Float64Array(nodeCount).fill(Infinity);
|
|
32
|
+
const predArc = new Uint32Array(nodeCount).fill(INVALID_INDEX);
|
|
33
|
+
dist[source] = 0;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One pass over every arc leaving a reached node.
|
|
37
|
+
* @returns True when any distance dropped
|
|
38
|
+
*/
|
|
39
|
+
const round = (): boolean => {
|
|
40
|
+
let changed = false;
|
|
41
|
+
for (let u = 0; u < nodeCount; u++) {
|
|
42
|
+
const du = dist[u];
|
|
43
|
+
if (du === Infinity) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const end = rowPtr[u + 1];
|
|
47
|
+
for (let a = rowPtr[u]; a < end; a++) {
|
|
48
|
+
const v = colIdx[a];
|
|
49
|
+
const dv = du + (weights === null ? 1 : weights[a]);
|
|
50
|
+
if (dv < dist[v] && dv <= cutoff) {
|
|
51
|
+
dist[v] = dv;
|
|
52
|
+
predArc[v] = a;
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return changed;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let settled = false;
|
|
61
|
+
for (let i = 0; i < nodeCount - 1 && !settled; i++) {
|
|
62
|
+
settled = !round();
|
|
63
|
+
}
|
|
64
|
+
const hasNegativeCycle = !settled && round();
|
|
65
|
+
return {
|
|
66
|
+
dist,
|
|
67
|
+
predArc,
|
|
68
|
+
hasNegativeCycle,
|
|
69
|
+
pathTo: (target: number): U32 => walkPredArcs(g, predArc, source, target),
|
|
70
|
+
pathEdges: (target: number): U32 => walkPredEdges(g, predArc, source, target),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { type EdgeMask, type F64, type GraphSnapshot, maskTest } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
/** Options of the index-based node betweenness. @public */
|
|
4
|
+
export interface BetweennessOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Divide by the number of ordered pairs a node can sit between: `(n - 1)(n - 2)` directed, half that
|
|
7
|
+
* undirected; with `endpoints`, `n (n - 1)` and half that. Default false.
|
|
8
|
+
*/
|
|
9
|
+
readonly normalized?: boolean | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Count a path's two ends as lying on it, as NetworkX's `endpoints=True` does. Default false. The legacy
|
|
12
|
+
* `betweennessCentrality` accepts this option and ignores it.
|
|
13
|
+
*/
|
|
14
|
+
readonly endpoints?: boolean | undefined;
|
|
15
|
+
/** Sampled betweenness: the source node indices to run from. Duplicates run twice. */
|
|
16
|
+
readonly sources?: readonly number[] | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Sampled betweenness: how many distinct sources to draw when `sources` is not given. The draw is
|
|
19
|
+
* deterministic -- the same `(n, k)` draws the same sources every time, and the dispatcher hands an
|
|
20
|
+
* accelerator the drawn sources, so both paths run the same ones. With `sources` it must equal `sources.length`.
|
|
21
|
+
*/
|
|
22
|
+
readonly k?: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Options of the index-based edge betweenness. @public */
|
|
26
|
+
export interface EdgeBetweennessOptions {
|
|
27
|
+
/** Divide by `(n - 1)(n - 2)` directed, half that undirected, as the node scores are. Default false. */
|
|
28
|
+
readonly normalized?: boolean | undefined;
|
|
29
|
+
/** Sampled edge betweenness: the source node indices to run from. */
|
|
30
|
+
readonly sources?: readonly number[] | undefined;
|
|
31
|
+
/** Sampled edge betweenness: how many sources to draw; see {@link BetweennessOptions.k}. */
|
|
32
|
+
readonly k?: number | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Logical edges to keep, as a packed mask over `edgeCount`; a cleared edge is treated as deleted and scores
|
|
35
|
+
* 0. Girvan-Newman removes edges this way without building a new snapshot.
|
|
36
|
+
*/
|
|
37
|
+
readonly alive?: EdgeMask | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Scores per node index from an exact, non-iterative computation. @public */
|
|
41
|
+
export interface ScoresResult {
|
|
42
|
+
/** One score per node index. */
|
|
43
|
+
readonly scores: F64;
|
|
44
|
+
/** The number of breadth-first searches or shortest-path searches run: one per source. */
|
|
45
|
+
readonly iterations: number;
|
|
46
|
+
/** Always true: nothing here iterates to a tolerance. */
|
|
47
|
+
readonly converged: true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Scores per logical edge index. @public */
|
|
51
|
+
export interface EdgeScoresResult {
|
|
52
|
+
/** One score per logical edge. */
|
|
53
|
+
readonly scores: F64;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The seed of the `k` draw. Any fixed value works: the draw only has to repeat. */
|
|
57
|
+
const SAMPLE_SEED = 0x9e3779b9;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* `k` distinct node indices by a partial Fisher-Yates shuffle over a fixed-seed mulberry32 stream, so the same
|
|
61
|
+
* `(n, k)` draws the same sources every time.
|
|
62
|
+
* @param n - The node count
|
|
63
|
+
* @param k - How many to draw, at most n
|
|
64
|
+
* @returns The sources
|
|
65
|
+
*/
|
|
66
|
+
function drawSources(n: number, k: number): number[] {
|
|
67
|
+
const pool = Array.from({ length: n }, (_, i) => i);
|
|
68
|
+
let state = SAMPLE_SEED;
|
|
69
|
+
for (let i = 0; i < k; i++) {
|
|
70
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
71
|
+
let t = Math.imul(state ^ (state >>> 15), state | 1);
|
|
72
|
+
t = (t + Math.imul(t ^ (t >>> 7), t | 61)) ^ t;
|
|
73
|
+
const unit = ((t ^ (t >>> 14)) >>> 0) / 2 ** 32;
|
|
74
|
+
const j = i + Math.floor(unit * (n - i));
|
|
75
|
+
[pool[i], pool[j]] = [pool[j], pool[i]];
|
|
76
|
+
}
|
|
77
|
+
return pool.slice(0, k);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The sources a call runs: `sources` as given, else `k` drawn, else every node.
|
|
82
|
+
* @param n - The node count
|
|
83
|
+
* @param sources - The caller's list
|
|
84
|
+
* @param k - The caller's count
|
|
85
|
+
* @returns The sources
|
|
86
|
+
* @throws RangeError for a source outside `[0, n)`, a `k` outside `[0, n]`, or a `k` that disagrees with the list
|
|
87
|
+
*/
|
|
88
|
+
export function resolveSources(
|
|
89
|
+
n: number,
|
|
90
|
+
sources: readonly number[] | undefined,
|
|
91
|
+
k: number | undefined,
|
|
92
|
+
): readonly number[] {
|
|
93
|
+
if (k !== undefined && (!Number.isInteger(k) || k < 0 || k > n)) {
|
|
94
|
+
throw new RangeError(`betweenness: k must be an integer in [0, ${n}], got ${k}`);
|
|
95
|
+
}
|
|
96
|
+
if (sources !== undefined) {
|
|
97
|
+
for (const v of sources) {
|
|
98
|
+
if (!Number.isInteger(v) || v < 0 || v >= n) {
|
|
99
|
+
throw new RangeError(`betweenness: sources must be node indices in [0, ${n}), got ${v}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (k !== undefined && k !== sources.length) {
|
|
103
|
+
throw new RangeError(`betweenness: k (${k}) must be absent or equal sources.length (${sources.length})`);
|
|
104
|
+
}
|
|
105
|
+
return sources;
|
|
106
|
+
}
|
|
107
|
+
return k === undefined ? Array.from({ length: n }, (_, i) => i) : drawSources(n, k);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Brandes' accumulation from every source, into `node` and/or `edge` (raw sums, before halving or
|
|
112
|
+
* normalising).
|
|
113
|
+
*
|
|
114
|
+
* A pair joined by parallel edges is ONE neighbour relation (graph-format design 3.5, the adjacent-skip idiom),
|
|
115
|
+
* so path counts are the simple graph's and equal the legacy function's. The pair's share of an edge score goes
|
|
116
|
+
* to the first alive parallel edge in row order; the others score 0. Removing that edge hands the whole share to
|
|
117
|
+
* the next parallel, so the scores of a pair's parallels always sum to the simple graph's edge score.
|
|
118
|
+
* Dependencies are pulled over OUT-arcs from the deepest level up, so no reverse adjacency is built.
|
|
119
|
+
* @param s - The snapshot
|
|
120
|
+
* @param sources - Source node indices
|
|
121
|
+
* @param node - Node sums, or null
|
|
122
|
+
* @param edge - Edge sums, or null
|
|
123
|
+
* @param alive - Kept edges, or null for all
|
|
124
|
+
* @param endpoints - Count path ends on the path (node sums only)
|
|
125
|
+
*/
|
|
126
|
+
function accumulate(
|
|
127
|
+
s: GraphSnapshot,
|
|
128
|
+
sources: readonly number[],
|
|
129
|
+
node: F64 | null,
|
|
130
|
+
edge: F64 | null,
|
|
131
|
+
alive: EdgeMask | null,
|
|
132
|
+
endpoints: boolean,
|
|
133
|
+
): void {
|
|
134
|
+
const { nodeCount: n, rowPtr, colIdx, arcToEdge } = s;
|
|
135
|
+
const depth = new Int32Array(n).fill(-1);
|
|
136
|
+
const sigma = new Float64Array(n);
|
|
137
|
+
const delta = new Float64Array(n);
|
|
138
|
+
const order = new Uint32Array(n);
|
|
139
|
+
for (const source of sources) {
|
|
140
|
+
let head = 0;
|
|
141
|
+
let tail = 0;
|
|
142
|
+
depth[source] = 0;
|
|
143
|
+
sigma[source] = 1;
|
|
144
|
+
order[tail++] = source;
|
|
145
|
+
while (head < tail) {
|
|
146
|
+
const v = order[head++];
|
|
147
|
+
const next = depth[v] + 1;
|
|
148
|
+
let prev = -1;
|
|
149
|
+
for (let a = rowPtr[v], end = rowPtr[v + 1]; a < end; a++) {
|
|
150
|
+
const w = colIdx[a];
|
|
151
|
+
if (w === prev || (alive !== null && !maskTest(alive, arcToEdge[a]))) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
prev = w;
|
|
155
|
+
if (depth[w] < 0) {
|
|
156
|
+
depth[w] = next;
|
|
157
|
+
order[tail++] = w;
|
|
158
|
+
}
|
|
159
|
+
if (depth[w] === next) {
|
|
160
|
+
sigma[w] += sigma[v];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (let i = tail - 1; i >= 0; i--) {
|
|
165
|
+
const v = order[i];
|
|
166
|
+
const next = depth[v] + 1;
|
|
167
|
+
let prev = -1;
|
|
168
|
+
for (let a = rowPtr[v], end = rowPtr[v + 1]; a < end; a++) {
|
|
169
|
+
const w = colIdx[a];
|
|
170
|
+
if (w === prev || (alive !== null && !maskTest(alive, arcToEdge[a]))) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
prev = w;
|
|
174
|
+
if (depth[w] === next) {
|
|
175
|
+
const c = (sigma[v] / sigma[w]) * (1 + delta[w]);
|
|
176
|
+
delta[v] += c;
|
|
177
|
+
if (edge !== null) {
|
|
178
|
+
edge[arcToEdge[a]] += c;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (node !== null && v !== source) {
|
|
183
|
+
node[v] += endpoints ? delta[v] + 1 : delta[v];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (node !== null && endpoints) {
|
|
187
|
+
node[source] += tail - 1;
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < tail; i++) {
|
|
190
|
+
const v = order[i];
|
|
191
|
+
depth[v] = -1;
|
|
192
|
+
sigma[v] = 0;
|
|
193
|
+
delta[v] = 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Halve on an undirected snapshot (each unordered pair was counted from both ends) and divide by `factor` when
|
|
200
|
+
* normalising and it is positive.
|
|
201
|
+
* @param s - The snapshot
|
|
202
|
+
* @param scores - The raw sums, scaled in place
|
|
203
|
+
* @param normalized - Whether to normalise
|
|
204
|
+
* @param factor - The directed pair count to normalise by
|
|
205
|
+
*/
|
|
206
|
+
function scale(s: GraphSnapshot, scores: F64, normalized: boolean | undefined, factor: number): void {
|
|
207
|
+
// Undirected: halving and then dividing by half the directed pair count is dividing by the whole count.
|
|
208
|
+
const halve = s.directed ? 1 : 2;
|
|
209
|
+
const divisor = normalized === true && factor > 0 ? factor : halve;
|
|
210
|
+
for (let i = 0; i < scores.length; i++) {
|
|
211
|
+
scores[i] /= divisor;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Node betweenness centrality by Brandes' algorithm: breadth-first, so weights are ignored, as in the legacy
|
|
217
|
+
* `betweennessCentrality`, whose scores this equals on every graph it can hold. A sampled run (`sources` or `k`)
|
|
218
|
+
* is the UNSCALED sum over the sources run, as the WebGPU accelerator reports it.
|
|
219
|
+
* @param s - The snapshot
|
|
220
|
+
* @param options - Normalisation, endpoints and sampling
|
|
221
|
+
* @returns One score per node index; `iterations` is the number of sources run
|
|
222
|
+
* @throws RangeError for a bad `sources` or `k`
|
|
223
|
+
* @public
|
|
224
|
+
*/
|
|
225
|
+
export function betweennessCentrality(s: GraphSnapshot, options: BetweennessOptions = {}): ScoresResult {
|
|
226
|
+
const n = s.nodeCount;
|
|
227
|
+
const sources = resolveSources(n, options.sources, options.k);
|
|
228
|
+
const scores = new Float64Array(n);
|
|
229
|
+
const endpoints = options.endpoints === true;
|
|
230
|
+
accumulate(s, sources, scores, null, null, endpoints);
|
|
231
|
+
scale(s, scores, options.normalized, endpoints ? n * (n - 1) : (n - 1) * (n - 2));
|
|
232
|
+
return { scores, iterations: sources.length, converged: true };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Edge betweenness centrality by Brandes' algorithm, one score per logical edge.
|
|
237
|
+
*
|
|
238
|
+
* On a directed snapshot an edge's score equals the legacy `edgeBetweennessCentrality` value keyed
|
|
239
|
+
* `"source-target"`. On an undirected one it is the number of pairs whose shortest paths cross the edge in
|
|
240
|
+
* either direction, which is the SUM of the two values the legacy function reports under `"u-v"` and `"v-u"`
|
|
241
|
+
* (each of those holds half), and is the score the WebGPU accelerator returns.
|
|
242
|
+
* @param s - The snapshot
|
|
243
|
+
* @param options - Normalisation, sampling and the alive-edge mask
|
|
244
|
+
* @returns One score per logical edge
|
|
245
|
+
* @throws RangeError for a bad `sources` or `k`, or an `alive` mask shorter than the edge count
|
|
246
|
+
* @public
|
|
247
|
+
*/
|
|
248
|
+
export function edgeBetweennessCentrality(s: GraphSnapshot, options: EdgeBetweennessOptions = {}): EdgeScoresResult {
|
|
249
|
+
const n = s.nodeCount;
|
|
250
|
+
const alive = options.alive ?? null;
|
|
251
|
+
if (alive !== null && alive.length < Math.ceil(s.edgeCount / 32)) {
|
|
252
|
+
throw new RangeError(`edgeBetweennessCentrality: alive covers fewer than ${s.edgeCount} edges`);
|
|
253
|
+
}
|
|
254
|
+
const scores = new Float64Array(s.edgeCount);
|
|
255
|
+
accumulate(s, resolveSources(n, options.sources, options.k), null, scores, alive, false);
|
|
256
|
+
scale(s, scores, options.normalized, (n - 1) * (n - 2));
|
|
257
|
+
return { scores };
|
|
258
|
+
}
|