@graphty/algorithms 2.1.1 → 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,390 @@
|
|
|
1
|
+
import { type F64, type GraphSnapshot, INVALID_INDEX, renumberPartition, type U32 } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
import { SeededRandom } from "../utils/math-utilities.js";
|
|
4
|
+
import { type LabelResult, withGroups } from "./components.js";
|
|
5
|
+
|
|
6
|
+
/** Options of the index-based GRSBM, matching the legacy `grsbm`. @public */
|
|
7
|
+
export interface GrsbmOptions {
|
|
8
|
+
/** Clusters at this depth are not split; default 10. */
|
|
9
|
+
readonly maxDepth?: number | undefined;
|
|
10
|
+
/** A cluster is split only when it has at least twice this many members; default 2. */
|
|
11
|
+
readonly minClusterSize?: number | undefined;
|
|
12
|
+
/** Convergence tolerance of the Fiedler-vector iteration; default 1e-6. */
|
|
13
|
+
readonly tolerance?: number | undefined;
|
|
14
|
+
/** Iteration cap of the Fiedler-vector iteration; default 100. */
|
|
15
|
+
readonly maxIterations?: number | undefined;
|
|
16
|
+
/** Seed of the iteration's start vectors; default 42. */
|
|
17
|
+
readonly seed?: number | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Use arc weights in the Laplacian and the modularity (true, the default), or count arcs
|
|
20
|
+
* (false, which is what the legacy `grsbm` does). A parallel arc counts once per arc.
|
|
21
|
+
*/
|
|
22
|
+
readonly weighted?: boolean | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Why a cluster was split: the legacy `ClusterExplanation`, over node indices. @public */
|
|
26
|
+
export interface GrsbmSplit {
|
|
27
|
+
/** The split's modularity minus the parent's. */
|
|
28
|
+
readonly improvement: number;
|
|
29
|
+
/** Modularity of the chosen split point (the legacy explanation text prints it to 3 places). */
|
|
30
|
+
readonly bisectionModularity: number;
|
|
31
|
+
/** Up to five members with the largest-magnitude Fiedler values, in member order. */
|
|
32
|
+
readonly keyNodes: U32;
|
|
33
|
+
/** The Fiedler vector, one value per member in member order. */
|
|
34
|
+
readonly spectralValues: F64;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One cluster of the hierarchy. @public */
|
|
38
|
+
export interface GrsbmCluster {
|
|
39
|
+
/**
|
|
40
|
+
* 0 for the root, k for the k-th cluster a bisection proposed, counting the proposals that were
|
|
41
|
+
* rejected: the number in the legacy id `cluster_k`.
|
|
42
|
+
*/
|
|
43
|
+
readonly serial: number;
|
|
44
|
+
/** Member node indices, in the legacy member order. */
|
|
45
|
+
readonly members: U32;
|
|
46
|
+
/** Depth below the root. */
|
|
47
|
+
readonly depth: number;
|
|
48
|
+
/** Modularity of the split that made this cluster (the root: of the whole graph as one cluster). */
|
|
49
|
+
readonly modularity: number;
|
|
50
|
+
/** Spread of the Fiedler values of the split that made this cluster; 0 for the root. */
|
|
51
|
+
readonly spectralScore: number;
|
|
52
|
+
/** Index in `clusters` of the left child, or `INVALID_INDEX` for a leaf. */
|
|
53
|
+
readonly left: number;
|
|
54
|
+
/** Index in `clusters` of the right child, or `INVALID_INDEX` for a leaf. */
|
|
55
|
+
readonly right: number;
|
|
56
|
+
/** Why this cluster was split; null for a leaf. */
|
|
57
|
+
readonly split: GrsbmSplit | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Result of the index-based GRSBM: the leaves as a partition, plus the hierarchy. `clusters[0]` is
|
|
62
|
+
* the root; the others follow in the order they were made, children in pairs.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export interface GrsbmResult extends LabelResult {
|
|
66
|
+
/** Every cluster of the hierarchy. */
|
|
67
|
+
readonly clusters: readonly GrsbmCluster[];
|
|
68
|
+
/** The root's modularity, then each accepted split's, in split order. */
|
|
69
|
+
readonly modularityScores: F64;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface MutableCluster {
|
|
73
|
+
serial: number;
|
|
74
|
+
members: U32;
|
|
75
|
+
depth: number;
|
|
76
|
+
modularity: number;
|
|
77
|
+
spectralScore: number;
|
|
78
|
+
left: number;
|
|
79
|
+
right: number;
|
|
80
|
+
split: GrsbmSplit | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* GRSBM over a snapshot: recursive spectral bisection. Each cluster's Laplacian (over the arcs
|
|
85
|
+
* between its members, self-loops left out) gives an approximate Fiedler vector; members are sorted
|
|
86
|
+
* by it and cut at the point between 20% and 80% that maximises modularity; the split is kept when
|
|
87
|
+
* modularity drops by no more than 0.01. The arithmetic and the random draws are the legacy
|
|
88
|
+
* `grsbm`'s, so with `weighted: false` the result equals legacy's exactly on a graph without
|
|
89
|
+
* self-loops; the port draws from its own generator instead of replacing `Math.random`.
|
|
90
|
+
*
|
|
91
|
+
* Modularity uses `weightedDegree()` (a self-loop counts twice) against `totalWeight()`, or
|
|
92
|
+
* `degree()` against the edge count when unweighted. Like legacy's, a split's modularity sums only
|
|
93
|
+
* the two halves of the cluster being split. A self-loop follows the standard convention (left out
|
|
94
|
+
* of the Laplacian, twice in the degree), so on a graph with self-loops the result differs from
|
|
95
|
+
* legacy's, which scores the whole graph as one community above 0. On a directed graph the
|
|
96
|
+
* unweighted path keeps legacy's halving of internal arcs; the weighted path does not, so there the
|
|
97
|
+
* whole graph as one community scores 0. Weights must be finite and non-negative (RangeError).
|
|
98
|
+
* @param s - The snapshot
|
|
99
|
+
* @param options - {@link GrsbmOptions}
|
|
100
|
+
* @returns The leaf partition and the hierarchy
|
|
101
|
+
*/
|
|
102
|
+
export function grsbm(s: GraphSnapshot, options: GrsbmOptions = {}): GrsbmResult {
|
|
103
|
+
const {
|
|
104
|
+
maxDepth = 10,
|
|
105
|
+
minClusterSize = 2,
|
|
106
|
+
tolerance = 1e-6,
|
|
107
|
+
maxIterations = 100,
|
|
108
|
+
seed = 42,
|
|
109
|
+
weighted = true,
|
|
110
|
+
} = options;
|
|
111
|
+
const n = s.nodeCount;
|
|
112
|
+
if (n === 0) {
|
|
113
|
+
throw new Error("Cannot cluster empty graph");
|
|
114
|
+
}
|
|
115
|
+
const random = SeededRandom.createGenerator(seed);
|
|
116
|
+
const arcWeights = weighted ? s.weights : null;
|
|
117
|
+
if (arcWeights !== null) {
|
|
118
|
+
for (let a = 0; a < arcWeights.length; a++) {
|
|
119
|
+
const w = arcWeights[a];
|
|
120
|
+
if (!(w >= 0) || w === Infinity) {
|
|
121
|
+
throw new RangeError(
|
|
122
|
+
`arc ${String(a)} has weight ${String(w)}; grsbm needs finite, non-negative weights`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const degree = weighted ? s.weightedDegree() : s.degree();
|
|
128
|
+
const m = weighted ? s.totalWeight() : s.edgeCount;
|
|
129
|
+
const loopFactor = s.directed ? 1 : 2;
|
|
130
|
+
// An undirected edge is stored as two arcs, so the internal sum counts it twice. A directed arc is
|
|
131
|
+
// stored once; legacy halves it anyway (the whole graph as one community scores -0.5), and the
|
|
132
|
+
// unweighted path keeps that for parity.
|
|
133
|
+
const halveInternal = !s.directed || !weighted;
|
|
134
|
+
const { rowPtr, colIdx } = s;
|
|
135
|
+
|
|
136
|
+
// side[u]: the community of u in the split being scored, -1 outside the cluster.
|
|
137
|
+
const side = new Int32Array(n).fill(-1);
|
|
138
|
+
const communityTerm = (group: ArrayLike<number>): number => {
|
|
139
|
+
let internal = 0;
|
|
140
|
+
let total = 0;
|
|
141
|
+
for (let p = 0; p < group.length; p++) {
|
|
142
|
+
const u = group[p];
|
|
143
|
+
total += degree[u];
|
|
144
|
+
for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
|
|
145
|
+
const v = colIdx[a];
|
|
146
|
+
if (side[v] === side[u]) {
|
|
147
|
+
const w = arcWeights === null ? 1 : arcWeights[a];
|
|
148
|
+
internal += v === u ? loopFactor * w : w;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (halveInternal) {
|
|
153
|
+
internal /= 2;
|
|
154
|
+
}
|
|
155
|
+
return (internal - (total * total) / (4 * m)) / m;
|
|
156
|
+
};
|
|
157
|
+
const modularity = (groups: readonly ArrayLike<number>[]): number => {
|
|
158
|
+
if (m === 0) {
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
let q = 0;
|
|
162
|
+
groups.forEach((group, c) => {
|
|
163
|
+
for (let p = 0; p < group.length; p++) {
|
|
164
|
+
side[group[p]] = c;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
for (const group of groups) {
|
|
168
|
+
if (group.length > 0) {
|
|
169
|
+
q += communityTerm(group);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const group of groups) {
|
|
173
|
+
for (let p = 0; p < group.length; p++) {
|
|
174
|
+
side[group[p]] = -1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return q;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const everyone = new Uint32Array(n);
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
everyone[i] = i;
|
|
183
|
+
}
|
|
184
|
+
const initial = modularity([everyone]);
|
|
185
|
+
const clusters: MutableCluster[] = [
|
|
186
|
+
{
|
|
187
|
+
serial: 0,
|
|
188
|
+
members: everyone,
|
|
189
|
+
depth: 0,
|
|
190
|
+
modularity: initial,
|
|
191
|
+
spectralScore: 0,
|
|
192
|
+
left: INVALID_INDEX,
|
|
193
|
+
right: INVALID_INDEX,
|
|
194
|
+
split: null,
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
const scores = [initial];
|
|
198
|
+
const position = new Int32Array(n).fill(-1);
|
|
199
|
+
let nextSerial = 1;
|
|
200
|
+
|
|
201
|
+
// The clusters array doubles as the breadth-first work queue: children are appended as made.
|
|
202
|
+
for (let head = 0; head < clusters.length; head++) {
|
|
203
|
+
const cluster = clusters[head];
|
|
204
|
+
const { members } = cluster;
|
|
205
|
+
const size = members.length;
|
|
206
|
+
if (cluster.depth >= maxDepth || size < minClusterSize * 2 || size < 4) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const fiedler = fiedlerVector(members, position, s, arcWeights, random, tolerance, maxIterations);
|
|
210
|
+
const order = Array.from({ length: size }, (_, p) => p).sort((a, b) => fiedler[a] - fiedler[b]);
|
|
211
|
+
const sorted = Uint32Array.from(order, (p) => members[p]);
|
|
212
|
+
|
|
213
|
+
let bestSplit = Math.floor(size / 2);
|
|
214
|
+
let best = -Infinity;
|
|
215
|
+
for (let cut = Math.floor(size * 0.2); cut <= Math.floor(size * 0.8); cut++) {
|
|
216
|
+
const q = modularity([sorted.subarray(0, cut), sorted.subarray(cut)]);
|
|
217
|
+
if (q > best) {
|
|
218
|
+
best = q;
|
|
219
|
+
bestSplit = cut;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const leftMembers = sorted.slice(0, bestSplit);
|
|
223
|
+
const rightMembers = sorted.slice(bestSplit);
|
|
224
|
+
|
|
225
|
+
const byMagnitude = Array.from(fiedler).sort((a, b) => Math.abs(b) - Math.abs(a));
|
|
226
|
+
const threshold = Math.abs(byMagnitude[Math.floor(size * 0.1)]);
|
|
227
|
+
const keyNodes: number[] = [];
|
|
228
|
+
for (let p = 0; p < size && keyNodes.length < 5; p++) {
|
|
229
|
+
if (Math.abs(fiedler[p]) >= threshold) {
|
|
230
|
+
keyNodes.push(members[p]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const spectralScore = Math.abs(byMagnitude[0] - byMagnitude[size - 1]);
|
|
234
|
+
|
|
235
|
+
const leftSerial = nextSerial++;
|
|
236
|
+
const rightSerial = nextSerial++;
|
|
237
|
+
const improvement = best - cluster.modularity;
|
|
238
|
+
if (improvement < -0.01) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const child = (serial: number, childMembers: U32): MutableCluster => ({
|
|
242
|
+
serial,
|
|
243
|
+
members: childMembers,
|
|
244
|
+
depth: cluster.depth + 1,
|
|
245
|
+
modularity: best,
|
|
246
|
+
spectralScore,
|
|
247
|
+
left: INVALID_INDEX,
|
|
248
|
+
right: INVALID_INDEX,
|
|
249
|
+
split: null,
|
|
250
|
+
});
|
|
251
|
+
cluster.left = clusters.length;
|
|
252
|
+
cluster.right = clusters.length + 1;
|
|
253
|
+
cluster.split = {
|
|
254
|
+
improvement,
|
|
255
|
+
bisectionModularity: best,
|
|
256
|
+
keyNodes: Uint32Array.from(keyNodes),
|
|
257
|
+
spectralValues: fiedler,
|
|
258
|
+
};
|
|
259
|
+
clusters.push(child(leftSerial, leftMembers), child(rightSerial, rightMembers));
|
|
260
|
+
scores.push(best);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// One label per leaf, renumbered by each leaf's lowest node index.
|
|
264
|
+
const leafOf = new Uint32Array(n);
|
|
265
|
+
let leaves = 0;
|
|
266
|
+
const stack = [0];
|
|
267
|
+
while (stack.length > 0) {
|
|
268
|
+
const c = clusters[stack.pop() ?? 0];
|
|
269
|
+
if (c.left === INVALID_INDEX) {
|
|
270
|
+
for (const u of c.members) {
|
|
271
|
+
leafOf[u] = leaves;
|
|
272
|
+
}
|
|
273
|
+
leaves++;
|
|
274
|
+
} else {
|
|
275
|
+
stack.push(c.right, c.left);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const { labels, count } = renumberPartition(leafOf);
|
|
279
|
+
return { ...withGroups(labels, count), clusters, modularityScores: Float64Array.from(scores) };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Legacy's approximate Fiedler vector of a cluster's Laplacian: a seeded random start orthogonal to
|
|
284
|
+
* the all-ones vector, then repeated multiplication by -L, re-centred and normalised, until it stops
|
|
285
|
+
* moving. Each row is summed in member order, the order legacy's dense matrix product uses, so the
|
|
286
|
+
* vector matches legacy's to the last bit.
|
|
287
|
+
* @param members - The cluster's node indices
|
|
288
|
+
* @param position - Scratch, -1 everywhere on entry and exit
|
|
289
|
+
* @param s - The snapshot
|
|
290
|
+
* @param arcWeights - Arc weights, or null to count arcs
|
|
291
|
+
* @param random - The generator
|
|
292
|
+
* @param tolerance - Convergence tolerance
|
|
293
|
+
* @param maxIterations - Iteration cap
|
|
294
|
+
* @returns One value per member
|
|
295
|
+
*/
|
|
296
|
+
function fiedlerVector(
|
|
297
|
+
members: U32,
|
|
298
|
+
position: Int32Array,
|
|
299
|
+
s: GraphSnapshot,
|
|
300
|
+
arcWeights: F64 | Float32Array | null,
|
|
301
|
+
random: () => number,
|
|
302
|
+
tolerance: number,
|
|
303
|
+
maxIterations: number,
|
|
304
|
+
): F64 {
|
|
305
|
+
const size = members.length;
|
|
306
|
+
for (let p = 0; p < size; p++) {
|
|
307
|
+
position[members[p]] = p;
|
|
308
|
+
}
|
|
309
|
+
// The Laplacian as sorted sparse rows: off-diagonal -w per member neighbour, diagonal their sum.
|
|
310
|
+
const rowStart = new Uint32Array(size + 1);
|
|
311
|
+
const cols: number[] = [];
|
|
312
|
+
const vals: number[] = [];
|
|
313
|
+
const row = new Map<number, number>();
|
|
314
|
+
for (let p = 0; p < size; p++) {
|
|
315
|
+
const u = members[p];
|
|
316
|
+
row.clear();
|
|
317
|
+
let diagonal = 0;
|
|
318
|
+
for (let a = s.rowPtr[u]; a < s.rowPtr[u + 1]; a++) {
|
|
319
|
+
const q = position[s.colIdx[a]];
|
|
320
|
+
if (q >= 0 && q !== p) {
|
|
321
|
+
const w = arcWeights === null ? 1 : arcWeights[a];
|
|
322
|
+
row.set(q, (row.get(q) ?? 0) - w);
|
|
323
|
+
diagonal += w;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
row.set(p, diagonal);
|
|
327
|
+
for (const q of [...row.keys()].sort((a, b) => a - b)) {
|
|
328
|
+
cols.push(q);
|
|
329
|
+
vals.push(row.get(q) ?? 0);
|
|
330
|
+
}
|
|
331
|
+
rowStart[p + 1] = cols.length;
|
|
332
|
+
}
|
|
333
|
+
for (let p = 0; p < size; p++) {
|
|
334
|
+
position[members[p]] = -1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let vector: F64 = new Float64Array(size);
|
|
338
|
+
for (let p = 0; p < size; p++) {
|
|
339
|
+
vector[p] = random() - 0.5;
|
|
340
|
+
}
|
|
341
|
+
const recentre = (v: F64): void => {
|
|
342
|
+
let sum = 0;
|
|
343
|
+
for (const x of v) {
|
|
344
|
+
sum += x;
|
|
345
|
+
}
|
|
346
|
+
const mean = sum / size;
|
|
347
|
+
for (let p = 0; p < size; p++) {
|
|
348
|
+
v[p] -= mean;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
const norm = (v: F64): number => {
|
|
352
|
+
let sum = 0;
|
|
353
|
+
for (const x of v) {
|
|
354
|
+
sum += x * x;
|
|
355
|
+
}
|
|
356
|
+
return Math.sqrt(sum);
|
|
357
|
+
};
|
|
358
|
+
recentre(vector);
|
|
359
|
+
const start = norm(vector);
|
|
360
|
+
if (start > 0) {
|
|
361
|
+
for (let p = 0; p < size; p++) {
|
|
362
|
+
vector[p] /= start;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
366
|
+
const next = new Float64Array(size);
|
|
367
|
+
for (let p = 0; p < size; p++) {
|
|
368
|
+
let sum = 0;
|
|
369
|
+
for (let k = rowStart[p]; k < rowStart[p + 1]; k++) {
|
|
370
|
+
sum += vals[k] * vector[cols[k]];
|
|
371
|
+
}
|
|
372
|
+
next[p] = -sum;
|
|
373
|
+
}
|
|
374
|
+
recentre(next);
|
|
375
|
+
const length = norm(next);
|
|
376
|
+
if (length < tolerance) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
let moved = 0;
|
|
380
|
+
for (let p = 0; p < size; p++) {
|
|
381
|
+
next[p] /= length;
|
|
382
|
+
moved += Math.abs(next[p] - vector[p]);
|
|
383
|
+
}
|
|
384
|
+
vector = next;
|
|
385
|
+
if (moved < tolerance) {
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return vector;
|
|
390
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { type AdjacencyView, INVALID_INDEX } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
/** How the distance between two clusters is taken from their members' hop distances. @public */
|
|
4
|
+
export type Linkage = "single" | "complete" | "average" | "ward";
|
|
5
|
+
|
|
6
|
+
/** Options of the index-based hierarchical clustering. @public */
|
|
7
|
+
export interface HierarchicalOptions {
|
|
8
|
+
/** Cluster distance: the minimum, maximum or mean member distance, or Ward's scaled mean; default single. */
|
|
9
|
+
readonly linkage?: Linkage | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The dendrogram of an agglomerative clustering. Clusters `0 .. nodeCount - 1` are the nodes
|
|
14
|
+
* themselves; merge `k` creates cluster `nodeCount + k` from `left[k]` and `right[k]`.
|
|
15
|
+
* @public
|
|
16
|
+
*/
|
|
17
|
+
export interface HierarchicalResult {
|
|
18
|
+
/** The node count the clustering ran over. */
|
|
19
|
+
readonly nodeCount: number;
|
|
20
|
+
/** First cluster of every merge. */
|
|
21
|
+
readonly left: Uint32Array;
|
|
22
|
+
/** Second cluster of every merge. */
|
|
23
|
+
readonly right: Uint32Array;
|
|
24
|
+
/** Linkage distance of every merge. */
|
|
25
|
+
readonly distance: Float64Array;
|
|
26
|
+
/** Height of every cluster: 0 for a node, one more than its taller child for a merge. */
|
|
27
|
+
readonly height: Uint32Array;
|
|
28
|
+
/**
|
|
29
|
+
* The clusters left unmerged, in the order the legacy function lists its forest's trees: one
|
|
30
|
+
* entry (the last merge) when every pair of nodes could be linked, one per disconnected part
|
|
31
|
+
* otherwise, and none for an empty snapshot.
|
|
32
|
+
*/
|
|
33
|
+
readonly roots: Uint32Array;
|
|
34
|
+
/**
|
|
35
|
+
* The node indices of a cluster, left subtree first -- the order the legacy function's member
|
|
36
|
+
* Sets iterate in.
|
|
37
|
+
* @param cluster - A cluster index
|
|
38
|
+
*/
|
|
39
|
+
members(cluster: number): Uint32Array;
|
|
40
|
+
/**
|
|
41
|
+
* Cut every root at `height`: a cluster no taller than `height` is kept whole, a taller one is
|
|
42
|
+
* replaced by its two children. The clusters come in left-to-right order, root by root.
|
|
43
|
+
* @param height - The cut height
|
|
44
|
+
*/
|
|
45
|
+
cut(height: number): Uint32Array[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Hop distances from every node, over out-arcs: `nodeCount * nodeCount` entries, `INVALID_INDEX`
|
|
50
|
+
* where no path exists.
|
|
51
|
+
* @param s - The adjacency
|
|
52
|
+
* @returns The row-major hop matrix
|
|
53
|
+
*/
|
|
54
|
+
function hopMatrix(s: AdjacencyView): Uint32Array {
|
|
55
|
+
const n = s.nodeCount;
|
|
56
|
+
const hops = new Uint32Array(n * n).fill(INVALID_INDEX);
|
|
57
|
+
const queue = new Uint32Array(n);
|
|
58
|
+
for (let start = 0; start < n; start++) {
|
|
59
|
+
const row = start * n;
|
|
60
|
+
hops[row + start] = 0;
|
|
61
|
+
queue[0] = start;
|
|
62
|
+
let head = 0;
|
|
63
|
+
let tail = 1;
|
|
64
|
+
while (head < tail) {
|
|
65
|
+
const u = queue[head++];
|
|
66
|
+
const next = hops[row + u] + 1;
|
|
67
|
+
const end = s.rowPtr[u + 1];
|
|
68
|
+
for (let a = s.rowPtr[u]; a < end; a++) {
|
|
69
|
+
const v = s.colIdx[a];
|
|
70
|
+
if (hops[row + v] === INVALID_INDEX) {
|
|
71
|
+
hops[row + v] = next;
|
|
72
|
+
queue[tail++] = v;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return hops;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The rank of every cluster's legacy id string (`leaf-<i>` for a node, `cluster-<k>` for a merge),
|
|
82
|
+
* in string order. The legacy function breaks distance ties by comparing those strings, so the port
|
|
83
|
+
* compares these ranks to make the same merges.
|
|
84
|
+
* @param n - Node count
|
|
85
|
+
* @returns One rank per possible cluster, `2n - 1` of them
|
|
86
|
+
*/
|
|
87
|
+
function legacyIdRanks(n: number): Uint32Array {
|
|
88
|
+
const total = Math.max(0, 2 * n - 1);
|
|
89
|
+
const names = Array.from({ length: total }, (_, c) => (c < n ? `leaf-${c}` : `cluster-${c}`));
|
|
90
|
+
// Code-unit order, as the legacy `>=` on strings compares; localeCompare would not.
|
|
91
|
+
const order = Array.from({ length: total }, (_, c) => c).sort(
|
|
92
|
+
(a, b) => Number(names[a] > names[b]) - Number(names[a] < names[b]),
|
|
93
|
+
);
|
|
94
|
+
const rank = new Uint32Array(total);
|
|
95
|
+
order.forEach((c, r) => (rank[c] = r));
|
|
96
|
+
return rank;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Agglomerative hierarchical clustering over hop distances, the index-based port of the legacy
|
|
101
|
+
* `hierarchicalClustering`: every node starts as its own cluster and the closest pair is merged
|
|
102
|
+
* until one cluster remains or no remaining pair is linked by a path.
|
|
103
|
+
*
|
|
104
|
+
* The distance between two nodes is the BFS hop count over out-arcs (weights are ignored, as the
|
|
105
|
+
* legacy function ignores them); an unreachable pair has none and is left out of every linkage.
|
|
106
|
+
* The distance between two clusters over the member pairs that have one is their minimum
|
|
107
|
+
* (`single`), maximum (`complete`), mean (`average`), or mean times `|A| |B| / (|A| + |B|)`
|
|
108
|
+
* (`ward`). A pair with no connected member pair is never merged. On a directed snapshot the
|
|
109
|
+
* distance is taken in one direction, and which one follows the legacy function exactly, as do the
|
|
110
|
+
* tie-breaks: the merges, their order and their distances equal the legacy function's.
|
|
111
|
+
*
|
|
112
|
+
* One difference in what the result offers: `cut` descends into every root of a disconnected
|
|
113
|
+
* snapshot, where the legacy `clusters` map lists all nodes as one cluster at every height (its
|
|
114
|
+
* forest node has no children, so its cut never splits it).
|
|
115
|
+
*
|
|
116
|
+
* O(n^2) memory for the distance aggregates and O(n^3) time for the pair search; the legacy
|
|
117
|
+
* function is the same order with Map and Set per step.
|
|
118
|
+
* @param s - Any snapshot or adjacency view
|
|
119
|
+
* @param options - The linkage
|
|
120
|
+
* @returns The dendrogram
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
export function hierarchicalClustering(s: AdjacencyView, options: HierarchicalOptions = {}): HierarchicalResult {
|
|
124
|
+
const linkage = options.linkage ?? "single";
|
|
125
|
+
if (!["single", "complete", "average", "ward"].includes(linkage)) {
|
|
126
|
+
throw new RangeError(`unknown linkage "${linkage}"`);
|
|
127
|
+
}
|
|
128
|
+
const n = s.nodeCount;
|
|
129
|
+
const hops = hopMatrix(s);
|
|
130
|
+
// Per ordered pair of cluster SLOTS: the min, max, sum and count of the member hop distances
|
|
131
|
+
// from the first to the second. A merge reuses its first child's slot.
|
|
132
|
+
const min = new Uint32Array(n * n);
|
|
133
|
+
const max = new Uint32Array(n * n);
|
|
134
|
+
const sum = new Float64Array(n * n);
|
|
135
|
+
const count = new Float64Array(n * n);
|
|
136
|
+
for (let i = 0; i < n * n; i++) {
|
|
137
|
+
const h = hops[i];
|
|
138
|
+
if (h !== INVALID_INDEX) {
|
|
139
|
+
min[i] = h;
|
|
140
|
+
max[i] = h;
|
|
141
|
+
sum[i] = h;
|
|
142
|
+
count[i] = 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const total = Math.max(0, 2 * n - 1);
|
|
147
|
+
const rank = legacyIdRanks(n);
|
|
148
|
+
const slot = new Uint32Array(total);
|
|
149
|
+
const size = new Float64Array(total);
|
|
150
|
+
const height = new Uint32Array(total);
|
|
151
|
+
for (let i = 0; i < n; i++) {
|
|
152
|
+
slot[i] = i;
|
|
153
|
+
size[i] = 1;
|
|
154
|
+
}
|
|
155
|
+
const left: number[] = [];
|
|
156
|
+
const right: number[] = [];
|
|
157
|
+
const distance: number[] = [];
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The linkage distance from one cluster to another, as the legacy clusterDistance(x, y).
|
|
161
|
+
* @param x - The cluster the member distances start from
|
|
162
|
+
* @param y - The cluster they end at
|
|
163
|
+
* @returns The distance, Infinity when no member of y is reachable from x
|
|
164
|
+
*/
|
|
165
|
+
const link = (x: number, y: number): number => {
|
|
166
|
+
const at = slot[x] * n + slot[y];
|
|
167
|
+
const c = count[at];
|
|
168
|
+
if (c === 0) {
|
|
169
|
+
return Infinity;
|
|
170
|
+
}
|
|
171
|
+
switch (linkage) {
|
|
172
|
+
case "single":
|
|
173
|
+
return min[at];
|
|
174
|
+
case "complete":
|
|
175
|
+
return max[at];
|
|
176
|
+
case "average":
|
|
177
|
+
return sum[at] / c;
|
|
178
|
+
default:
|
|
179
|
+
return (sum[at] / c) * ((size[x] * size[y]) / (size[x] + size[y]));
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* The distance the legacy function stores for a pair: a leaf pair's from the lower node to the
|
|
184
|
+
* higher, a pair involving a merge from the later-created cluster to the earlier one.
|
|
185
|
+
* @param x - One cluster
|
|
186
|
+
* @param y - The other
|
|
187
|
+
* @returns The stored distance
|
|
188
|
+
*/
|
|
189
|
+
const pairDistance = (x: number, y: number): number => {
|
|
190
|
+
const [early, late] = x < y ? [x, y] : [y, x];
|
|
191
|
+
return late < n ? link(early, late) : link(late, early);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Active clusters in the legacy Set's insertion order: the nodes, then each merge appended.
|
|
195
|
+
let active: number[] = Array.from({ length: n }, (_, i) => i);
|
|
196
|
+
while (active.length > 1) {
|
|
197
|
+
let best = Infinity;
|
|
198
|
+
let a = -1;
|
|
199
|
+
let b = -1;
|
|
200
|
+
for (const x of active) {
|
|
201
|
+
for (const y of active) {
|
|
202
|
+
if (rank[x] >= rank[y]) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const d = pairDistance(x, y);
|
|
206
|
+
if (d < best) {
|
|
207
|
+
best = d;
|
|
208
|
+
a = x;
|
|
209
|
+
b = y;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (a < 0) {
|
|
214
|
+
break; // no linked pair left: the rest stay separate roots
|
|
215
|
+
}
|
|
216
|
+
const created = n + left.length;
|
|
217
|
+
left.push(a);
|
|
218
|
+
right.push(b);
|
|
219
|
+
distance.push(best);
|
|
220
|
+
height[created] = Math.max(height[a], height[b]) + 1;
|
|
221
|
+
size[created] = size[a] + size[b];
|
|
222
|
+
const sa = slot[a];
|
|
223
|
+
const sb = slot[b];
|
|
224
|
+
for (const x of active) {
|
|
225
|
+
if (x === a || x === b) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const sx = slot[x];
|
|
229
|
+
for (const [to, from1, from2] of [
|
|
230
|
+
[sa * n + sx, sa * n + sx, sb * n + sx],
|
|
231
|
+
[sx * n + sa, sx * n + sa, sx * n + sb],
|
|
232
|
+
]) {
|
|
233
|
+
const c1 = count[from1];
|
|
234
|
+
const c2 = count[from2];
|
|
235
|
+
// An empty aggregate's min and max are meaningless: take the other side's.
|
|
236
|
+
if (c1 === 0 || c2 === 0) {
|
|
237
|
+
const from = c1 === 0 ? from2 : from1;
|
|
238
|
+
min[to] = min[from];
|
|
239
|
+
max[to] = max[from];
|
|
240
|
+
} else {
|
|
241
|
+
min[to] = Math.min(min[from1], min[from2]);
|
|
242
|
+
max[to] = Math.max(max[from1], max[from2]);
|
|
243
|
+
}
|
|
244
|
+
sum[to] = sum[from1] + sum[from2];
|
|
245
|
+
count[to] = c1 + c2;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
slot[created] = sa;
|
|
249
|
+
active = active.filter((x) => x !== a && x !== b);
|
|
250
|
+
active.push(created);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const merges = left.length;
|
|
254
|
+
const leftArr = Uint32Array.from(left);
|
|
255
|
+
const rightArr = Uint32Array.from(right);
|
|
256
|
+
const members = (cluster: number): Uint32Array => {
|
|
257
|
+
if (!(cluster >= 0 && cluster < n + merges)) {
|
|
258
|
+
throw new RangeError(`cluster ${cluster} is not in [0, ${n + merges})`);
|
|
259
|
+
}
|
|
260
|
+
const out: number[] = [];
|
|
261
|
+
const stack = [cluster];
|
|
262
|
+
while (stack.length > 0) {
|
|
263
|
+
const c = stack.pop() as number;
|
|
264
|
+
if (c < n) {
|
|
265
|
+
out.push(c);
|
|
266
|
+
} else {
|
|
267
|
+
stack.push(rightArr[c - n], leftArr[c - n]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return Uint32Array.from(out);
|
|
271
|
+
};
|
|
272
|
+
const roots = Uint32Array.from(active);
|
|
273
|
+
return {
|
|
274
|
+
nodeCount: n,
|
|
275
|
+
left: leftArr,
|
|
276
|
+
right: rightArr,
|
|
277
|
+
distance: Float64Array.from(distance),
|
|
278
|
+
height: height.slice(0, n + merges),
|
|
279
|
+
roots,
|
|
280
|
+
members,
|
|
281
|
+
cut(h: number): Uint32Array[] {
|
|
282
|
+
const out: Uint32Array[] = [];
|
|
283
|
+
const visit = (c: number): void => {
|
|
284
|
+
if (c < n || height[c] <= h) {
|
|
285
|
+
out.push(members(c));
|
|
286
|
+
} else {
|
|
287
|
+
visit(leftArr[c - n]);
|
|
288
|
+
visit(rightArr[c - n]);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
roots.forEach(visit);
|
|
292
|
+
return out;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|