@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,550 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdjacencyView,
|
|
3
|
+
type GraphSnapshot,
|
|
4
|
+
type NumericVector,
|
|
5
|
+
renumberPartition,
|
|
6
|
+
type U32,
|
|
7
|
+
} from "@graphty/graph-format";
|
|
8
|
+
|
|
9
|
+
import { type LabelResult, withGroups } from "./components.js";
|
|
10
|
+
|
|
11
|
+
/** Which graph Laplacian the embedding comes from. @public */
|
|
12
|
+
export type LaplacianType = "unnormalized" | "normalized" | "randomWalk";
|
|
13
|
+
|
|
14
|
+
/** Options of the index-based spectral clustering. @public */
|
|
15
|
+
export interface SpectralOptions {
|
|
16
|
+
/** Number of clusters, a positive integer. */
|
|
17
|
+
readonly k: number;
|
|
18
|
+
/** `D - A`, `I - D^-1/2 A D^-1/2` (rows of the embedding scaled to unit length) or `I - D^-1 A`; default normalized. */
|
|
19
|
+
readonly laplacianType?: LaplacianType | undefined;
|
|
20
|
+
/** Cap on k-means rounds; default 100. */
|
|
21
|
+
readonly maxIterations?: number | undefined;
|
|
22
|
+
/** k-means stops when no centroid moves further than this; default 1e-4. */
|
|
23
|
+
readonly tolerance?: number | undefined;
|
|
24
|
+
/** Seed of the starting block and of the k-means seeding; default 42. */
|
|
25
|
+
readonly seed?: number | undefined;
|
|
26
|
+
/** Per-arc weight override, arcCount long -- the facade passes `expandEdges(s, shadow.data)`. */
|
|
27
|
+
readonly weights?: NumericVector | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Result of the index-based spectral clustering. @public */
|
|
31
|
+
export interface SpectralResult extends LabelResult {
|
|
32
|
+
/** The k smallest eigenvalues of the Laplacian, ascending; empty when k >= nodeCount. */
|
|
33
|
+
readonly eigenvalues: Float64Array;
|
|
34
|
+
/** One nodeCount-long eigenvector per eigenvalue; for randomWalk, of `I - D^-1 A`. */
|
|
35
|
+
readonly eigenvectors: Float64Array[];
|
|
36
|
+
/**
|
|
37
|
+
* Whether the eigenpairs met the residual tolerance. False when the iteration stopped at its
|
|
38
|
+
* round cap -- on a graph whose small eigenvalues crowd together, such as a long path -- and
|
|
39
|
+
* the eigenpairs are then approximations (the labels usually still hold). True when there
|
|
40
|
+
* were no eigenpairs to compute.
|
|
41
|
+
*/
|
|
42
|
+
readonly converged: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Relative eigenvector residual at which subspace iteration stops. */
|
|
46
|
+
const RESIDUAL_TOLERANCE = 1e-11;
|
|
47
|
+
|
|
48
|
+
/** Subspace-iteration cap; each round costs one sparse product per block vector. */
|
|
49
|
+
const MAX_SUBSPACE_ROUNDS = 3000;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* mulberry32, the generator the label propagation port uses.
|
|
53
|
+
* @param seed - Generator seed; only its low 32 bits are used
|
|
54
|
+
* @returns The generator, output in [0, 1)
|
|
55
|
+
*/
|
|
56
|
+
function mulberry32(seed: number): () => number {
|
|
57
|
+
let a = seed >>> 0;
|
|
58
|
+
return () => {
|
|
59
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
60
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
61
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
62
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The symmetric weighted adjacency without self-loops, as rows of (neighbour, weight). */
|
|
67
|
+
interface SymmetricRows {
|
|
68
|
+
readonly rowPtr: Uint32Array;
|
|
69
|
+
readonly colIdx: Uint32Array;
|
|
70
|
+
readonly w: Float64Array;
|
|
71
|
+
readonly degree: Float64Array;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The snapshot's adjacency made symmetric -- a directed snapshot's out-row and in-row together, so
|
|
76
|
+
* a reciprocal pair sums -- with self-loops dropped (they cancel in `D - A`) and weights checked.
|
|
77
|
+
* @param s - The snapshot
|
|
78
|
+
* @param weights - Per-arc weights, or null for all ones
|
|
79
|
+
* @returns The rows and the weighted degrees
|
|
80
|
+
*/
|
|
81
|
+
function symmetricRows(s: GraphSnapshot, weights: NumericVector | null): SymmetricRows {
|
|
82
|
+
const n = s.nodeCount;
|
|
83
|
+
const views: { view: AdjacencyView; arcOf: (k: number) => number }[] = [{ view: s, arcOf: (a) => a }];
|
|
84
|
+
if (s.directed) {
|
|
85
|
+
const rev = s.reverse();
|
|
86
|
+
views.push({ view: rev, arcOf: (k) => rev.fwdArc[k] });
|
|
87
|
+
}
|
|
88
|
+
const rowPtr = new Uint32Array(n + 1);
|
|
89
|
+
for (let u = 0; u < n; u++) {
|
|
90
|
+
let kept = 0;
|
|
91
|
+
for (const { view } of views) {
|
|
92
|
+
for (let a = view.rowPtr[u]; a < view.rowPtr[u + 1]; a++) {
|
|
93
|
+
kept += view.colIdx[a] === u ? 0 : 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
rowPtr[u + 1] = rowPtr[u] + kept;
|
|
97
|
+
}
|
|
98
|
+
const colIdx = new Uint32Array(rowPtr[n]);
|
|
99
|
+
const w = new Float64Array(rowPtr[n]);
|
|
100
|
+
const degree = new Float64Array(n);
|
|
101
|
+
let k = 0;
|
|
102
|
+
for (let u = 0; u < n; u++) {
|
|
103
|
+
for (const { view, arcOf } of views) {
|
|
104
|
+
for (let a = view.rowPtr[u]; a < view.rowPtr[u + 1]; a++) {
|
|
105
|
+
const weight = weights === null ? 1 : weights[arcOf(a)];
|
|
106
|
+
if (!(weight >= 0) || weight === Infinity) {
|
|
107
|
+
throw new RangeError(
|
|
108
|
+
`an arc has weight ${weight}; spectral clustering needs finite, non-negative weights`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (view.colIdx[a] !== u) {
|
|
112
|
+
colIdx[k] = view.colIdx[a];
|
|
113
|
+
w[k++] = weight;
|
|
114
|
+
degree[u] += weight;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { rowPtr, colIdx, w, degree };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Orthonormalise the columns of `block` in place (Gram-Schmidt, applied twice for stability). A
|
|
124
|
+
* column that collapses is replaced by a fresh random one.
|
|
125
|
+
* @param block - p columns of length n
|
|
126
|
+
* @param random - The generator for replacements
|
|
127
|
+
*/
|
|
128
|
+
function orthonormalise(block: Float64Array[], random: () => number): void {
|
|
129
|
+
for (let j = 0; j < block.length; j++) {
|
|
130
|
+
const v = block[j];
|
|
131
|
+
for (let attempt = 0; ; attempt++) {
|
|
132
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
133
|
+
for (let i = 0; i < j; i++) {
|
|
134
|
+
const q = block[i];
|
|
135
|
+
let dot = 0;
|
|
136
|
+
for (let r = 0; r < v.length; r++) {
|
|
137
|
+
dot += q[r] * v[r];
|
|
138
|
+
}
|
|
139
|
+
for (let r = 0; r < v.length; r++) {
|
|
140
|
+
v[r] -= dot * q[r];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
let norm = 0;
|
|
145
|
+
for (let r = 0; r < v.length; r++) {
|
|
146
|
+
norm += v[r] * v[r];
|
|
147
|
+
}
|
|
148
|
+
norm = Math.sqrt(norm);
|
|
149
|
+
if (norm > 1e-10 || attempt === 3) {
|
|
150
|
+
for (let r = 0; r < v.length; r++) {
|
|
151
|
+
v[r] = norm > 0 ? v[r] / norm : 0;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
for (let r = 0; r < v.length; r++) {
|
|
156
|
+
v[r] = random() - 0.5;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Eigen-decomposition of a small symmetric matrix by cyclic Jacobi rotations.
|
|
164
|
+
* @param a - p x p row-major, destroyed
|
|
165
|
+
* @param p - The order
|
|
166
|
+
* @returns The eigenvalues and the eigenvectors as the columns of a p x p row-major matrix
|
|
167
|
+
*/
|
|
168
|
+
function jacobi(a: Float64Array, p: number): { values: Float64Array; vectors: Float64Array } {
|
|
169
|
+
const v = new Float64Array(p * p);
|
|
170
|
+
for (let i = 0; i < p; i++) {
|
|
171
|
+
v[i * p + i] = 1;
|
|
172
|
+
}
|
|
173
|
+
for (let sweep = 0; sweep < 100; sweep++) {
|
|
174
|
+
let off = 0;
|
|
175
|
+
for (let i = 0; i < p; i++) {
|
|
176
|
+
for (let j = i + 1; j < p; j++) {
|
|
177
|
+
off += a[i * p + j] * a[i * p + j];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (off < 1e-30) {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
for (let i = 0; i < p; i++) {
|
|
184
|
+
for (let j = i + 1; j < p; j++) {
|
|
185
|
+
const aij = a[i * p + j];
|
|
186
|
+
if (aij === 0) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const theta = (a[j * p + j] - a[i * p + i]) / (2 * aij);
|
|
190
|
+
const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
|
|
191
|
+
const c = 1 / Math.sqrt(t * t + 1);
|
|
192
|
+
const sn = t * c;
|
|
193
|
+
for (let r = 0; r < p; r++) {
|
|
194
|
+
const ari = a[r * p + i];
|
|
195
|
+
const arj = a[r * p + j];
|
|
196
|
+
a[r * p + i] = c * ari - sn * arj;
|
|
197
|
+
a[r * p + j] = sn * ari + c * arj;
|
|
198
|
+
}
|
|
199
|
+
for (let r = 0; r < p; r++) {
|
|
200
|
+
const air = a[i * p + r];
|
|
201
|
+
const ajr = a[j * p + r];
|
|
202
|
+
a[i * p + r] = c * air - sn * ajr;
|
|
203
|
+
a[j * p + r] = sn * air + c * ajr;
|
|
204
|
+
}
|
|
205
|
+
for (let r = 0; r < p; r++) {
|
|
206
|
+
const vri = v[r * p + i];
|
|
207
|
+
const vrj = v[r * p + j];
|
|
208
|
+
v[r * p + i] = c * vri - sn * vrj;
|
|
209
|
+
v[r * p + j] = sn * vri + c * vrj;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { values: Float64Array.from({ length: p }, (_, i) => a[i * p + i]), vectors: v };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The k smallest eigenpairs of a symmetric Laplacian, by subspace iteration on `shift * I - L` (whose
|
|
219
|
+
* largest eigenpairs they are) with a Rayleigh-Ritz step, over a block a few vectors wider than k.
|
|
220
|
+
* @param apply - Writes `L x` into `out`
|
|
221
|
+
* @param n - Order
|
|
222
|
+
* @param k - Eigenpairs wanted
|
|
223
|
+
* @param shift - An upper bound on the largest eigenvalue of L
|
|
224
|
+
* @param random - The generator of the starting block
|
|
225
|
+
* @returns Eigenvalues ascending, their eigenvectors, and whether they met the tolerance
|
|
226
|
+
*/
|
|
227
|
+
function smallestEigenpairs(
|
|
228
|
+
apply: (x: Float64Array, out: Float64Array) => void,
|
|
229
|
+
n: number,
|
|
230
|
+
k: number,
|
|
231
|
+
shift: number,
|
|
232
|
+
random: () => number,
|
|
233
|
+
): { values: Float64Array; vectors: Float64Array[]; converged: boolean } {
|
|
234
|
+
const p = Math.min(n, k + Math.max(k, 8));
|
|
235
|
+
let block: Float64Array[] = Array.from({ length: p }, () => Float64Array.from({ length: n }, () => random() - 0.5));
|
|
236
|
+
orthonormalise(block, random);
|
|
237
|
+
const lx = new Float64Array(n);
|
|
238
|
+
const shifted = (x: Float64Array): Float64Array => {
|
|
239
|
+
apply(x, lx);
|
|
240
|
+
return x.map((xi, r) => shift * xi - lx[r]);
|
|
241
|
+
};
|
|
242
|
+
let ritz: Float64Array = new Float64Array(p);
|
|
243
|
+
let order: number[] = [];
|
|
244
|
+
let vectors: Float64Array = new Float64Array(p * p);
|
|
245
|
+
let converged = false;
|
|
246
|
+
// ponytail: plain subspace iteration converges at the ratio of neighbouring shifted eigenvalues,
|
|
247
|
+
// so a small spectral gap hits the cap; Chebyshev filtering would converge far faster there.
|
|
248
|
+
for (let round = 0; round < MAX_SUBSPACE_ROUNDS; round++) {
|
|
249
|
+
const image = block.map(shifted);
|
|
250
|
+
// Rayleigh-Ritz: the block's projection of the shifted operator, diagonalised.
|
|
251
|
+
const h = new Float64Array(p * p);
|
|
252
|
+
for (let i = 0; i < p; i++) {
|
|
253
|
+
for (let j = i; j < p; j++) {
|
|
254
|
+
let dot = 0;
|
|
255
|
+
for (let r = 0; r < n; r++) {
|
|
256
|
+
dot += block[i][r] * image[j][r];
|
|
257
|
+
}
|
|
258
|
+
h[i * p + j] = dot;
|
|
259
|
+
h[j * p + i] = dot;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const eig = jacobi(h, p);
|
|
263
|
+
({ values: ritz, vectors } = eig);
|
|
264
|
+
order = Array.from({ length: p }, (_, i) => i).sort((a, b) => ritz[b] - ritz[a]);
|
|
265
|
+
// Converged when every wanted Ritz pair (y = X q, theta) has a residual |B y - theta y|
|
|
266
|
+
// below RESIDUAL_TOLERANCE * shift; B y is the image block times q.
|
|
267
|
+
converged = order.slice(0, k).every((col) => {
|
|
268
|
+
let residual = 0;
|
|
269
|
+
for (let r = 0; r < n; r++) {
|
|
270
|
+
let diff = 0;
|
|
271
|
+
for (let i = 0; i < p; i++) {
|
|
272
|
+
diff += vectors[i * p + col] * (image[i][r] - ritz[col] * block[i][r]);
|
|
273
|
+
}
|
|
274
|
+
residual += diff * diff;
|
|
275
|
+
}
|
|
276
|
+
return Math.sqrt(residual) <= RESIDUAL_TOLERANCE * shift;
|
|
277
|
+
});
|
|
278
|
+
if (converged || round === MAX_SUBSPACE_ROUNDS - 1) {
|
|
279
|
+
break; // at the cap the Ritz pairs of this block are the best estimate there is
|
|
280
|
+
}
|
|
281
|
+
block = image;
|
|
282
|
+
orthonormalise(block, random);
|
|
283
|
+
}
|
|
284
|
+
const values = new Float64Array(k);
|
|
285
|
+
const out: Float64Array[] = [];
|
|
286
|
+
for (let rank = 0; rank < k; rank++) {
|
|
287
|
+
const col = order[rank];
|
|
288
|
+
values[rank] = Math.max(0, shift - ritz[col]);
|
|
289
|
+
const v = new Float64Array(n);
|
|
290
|
+
for (let i = 0; i < p; i++) {
|
|
291
|
+
const c = vectors[i * p + col];
|
|
292
|
+
for (let r = 0; r < n; r++) {
|
|
293
|
+
v[r] += c * block[i][r];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
out.push(v);
|
|
297
|
+
}
|
|
298
|
+
return { values, vectors: out, converged };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** k-means runs per clustering; the one with the lowest inertia is kept. */
|
|
302
|
+
const K_MEANS_RUNS = 10;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The k-means objective: the summed squared distance of every row from its cluster's mean.
|
|
306
|
+
* @param points - n rows of length d, row-major
|
|
307
|
+
* @param assign - The cluster of every row
|
|
308
|
+
* @param n - Row count
|
|
309
|
+
* @param d - Row length
|
|
310
|
+
* @param k - Cluster count
|
|
311
|
+
* @returns The inertia
|
|
312
|
+
*/
|
|
313
|
+
function inertiaOf(points: Float64Array, assign: Uint32Array, n: number, d: number, k: number): number {
|
|
314
|
+
const sums = new Float64Array(k * d);
|
|
315
|
+
const sizes = new Float64Array(k);
|
|
316
|
+
for (let i = 0; i < n; i++) {
|
|
317
|
+
sizes[assign[i]]++;
|
|
318
|
+
for (let t = 0; t < d; t++) {
|
|
319
|
+
sums[assign[i] * d + t] += points[i * d + t];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
let total = 0;
|
|
323
|
+
for (let i = 0; i < n; i++) {
|
|
324
|
+
const c = assign[i];
|
|
325
|
+
for (let t = 0; t < d; t++) {
|
|
326
|
+
total += (points[i * d + t] - sums[c * d + t] / sizes[c]) ** 2;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return total;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* k-means over the rows of an n x d embedding, seeded with k-means++ and refined by Lloyd rounds.
|
|
334
|
+
* @param points - n rows of length d, row-major
|
|
335
|
+
* @param n - Row count
|
|
336
|
+
* @param d - Row length
|
|
337
|
+
* @param k - Cluster count
|
|
338
|
+
* @param maxIterations - Round cap
|
|
339
|
+
* @param tolerance - Stop when no centroid moves further than this
|
|
340
|
+
* @param random - The generator
|
|
341
|
+
* @returns The cluster of every row
|
|
342
|
+
*/
|
|
343
|
+
function kMeans(
|
|
344
|
+
points: Float64Array,
|
|
345
|
+
n: number,
|
|
346
|
+
d: number,
|
|
347
|
+
k: number,
|
|
348
|
+
maxIterations: number,
|
|
349
|
+
tolerance: number,
|
|
350
|
+
random: () => number,
|
|
351
|
+
): U32 {
|
|
352
|
+
const dist2 = (i: number, centres: Float64Array, c: number): number => {
|
|
353
|
+
let sum = 0;
|
|
354
|
+
for (let t = 0; t < d; t++) {
|
|
355
|
+
const diff = points[i * d + t] - centres[c * d + t];
|
|
356
|
+
sum += diff * diff;
|
|
357
|
+
}
|
|
358
|
+
return sum;
|
|
359
|
+
};
|
|
360
|
+
const centres = new Float64Array(k * d);
|
|
361
|
+
const nearest = new Float64Array(n).fill(Infinity);
|
|
362
|
+
let pick = Math.floor(random() * n);
|
|
363
|
+
for (let c = 0; c < k; c++) {
|
|
364
|
+
centres.set(points.subarray(pick * d, pick * d + d), c * d);
|
|
365
|
+
let total = 0;
|
|
366
|
+
for (let i = 0; i < n; i++) {
|
|
367
|
+
nearest[i] = Math.min(nearest[i], dist2(i, centres, c));
|
|
368
|
+
total += nearest[i];
|
|
369
|
+
}
|
|
370
|
+
// k-means++: the next centre is a row drawn with probability proportional to its squared
|
|
371
|
+
// distance from the nearest centre so far (the first row when every row sits on a centre).
|
|
372
|
+
let target = random() * total;
|
|
373
|
+
pick = 0;
|
|
374
|
+
for (let i = 0; i < n && total > 0; i++) {
|
|
375
|
+
target -= nearest[i];
|
|
376
|
+
if (target < 0) {
|
|
377
|
+
pick = i;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const assign = new Uint32Array(n);
|
|
383
|
+
for (let round = 0; round < maxIterations; round++) {
|
|
384
|
+
let changed = round === 0;
|
|
385
|
+
for (let i = 0; i < n; i++) {
|
|
386
|
+
let best = 0;
|
|
387
|
+
let bestDist = Infinity;
|
|
388
|
+
for (let c = 0; c < k; c++) {
|
|
389
|
+
const dd = dist2(i, centres, c);
|
|
390
|
+
if (dd < bestDist) {
|
|
391
|
+
bestDist = dd;
|
|
392
|
+
best = c;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
changed ||= assign[i] !== best;
|
|
396
|
+
assign[i] = best;
|
|
397
|
+
}
|
|
398
|
+
if (!changed) {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const sums = new Float64Array(k * d);
|
|
402
|
+
const sizes = new Float64Array(k);
|
|
403
|
+
for (let i = 0; i < n; i++) {
|
|
404
|
+
sizes[assign[i]]++;
|
|
405
|
+
for (let t = 0; t < d; t++) {
|
|
406
|
+
sums[assign[i] * d + t] += points[i * d + t];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
let shift = 0;
|
|
410
|
+
for (let c = 0; c < k; c++) {
|
|
411
|
+
if (sizes[c] === 0) {
|
|
412
|
+
continue; // an emptied cluster keeps its centre
|
|
413
|
+
}
|
|
414
|
+
let moved = 0;
|
|
415
|
+
for (let t = 0; t < d; t++) {
|
|
416
|
+
const next = sums[c * d + t] / sizes[c];
|
|
417
|
+
moved += (next - centres[c * d + t]) ** 2;
|
|
418
|
+
centres[c * d + t] = next;
|
|
419
|
+
}
|
|
420
|
+
shift = Math.max(shift, Math.sqrt(moved));
|
|
421
|
+
}
|
|
422
|
+
if (shift < tolerance) {
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return assign;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Spectral clustering, the index-based port of the legacy `spectralClustering`: the k eigenvectors
|
|
431
|
+
* of the graph Laplacian with the smallest eigenvalues embed every node as a point in k dimensions,
|
|
432
|
+
* and k-means splits the points into at most k clusters.
|
|
433
|
+
*
|
|
434
|
+
* The Laplacian is applied straight from the CSR rows and never built: `D - A`
|
|
435
|
+
* (`unnormalized`), `I - D^-1/2 A D^-1/2` (`normalized`, whose embedded rows are then scaled to
|
|
436
|
+
* unit length, as Ng, Jordan and Weiss do) or `I - D^-1 A` (`randomWalk`, whose eigenvectors are
|
|
437
|
+
* `D^-1/2` times the normalized ones). A node of degree 0 has a zero row. Self-loops are ignored
|
|
438
|
+
* (they cancel in `D - A`) and a directed snapshot is read as undirected, a reciprocal pair
|
|
439
|
+
* summing. A negative, NaN or infinite weight throws. With `k >= nodeCount` every node is its own
|
|
440
|
+
* cluster.
|
|
441
|
+
*
|
|
442
|
+
* The eigenpairs come from subspace iteration with Rayleigh-Ritz, so they are the true smallest
|
|
443
|
+
* ones when `converged` is true; `converged` is false when the iteration stopped at its round cap
|
|
444
|
+
* (a small spectral gap, such as a path of hundreds of nodes), and the eigenpairs are then only
|
|
445
|
+
* approximate. The legacy function uses fixed placeholder eigenvalues for k <= 3 and plain power
|
|
446
|
+
* iteration (which finds the LARGEST eigenvectors) above that, and draws its k-means seeds from
|
|
447
|
+
* `Math.random` unless given a seed. The partitions therefore differ from the legacy function's;
|
|
448
|
+
* one `seed` gives one result. Labels are dense in first-seen order, so `count` is below k when
|
|
449
|
+
* k-means leaves a cluster empty.
|
|
450
|
+
* @param s - Any snapshot
|
|
451
|
+
* @param options - k, the Laplacian, the k-means limits, the seed and the weight override
|
|
452
|
+
* @returns The partition and the eigenpairs it came from
|
|
453
|
+
* @public
|
|
454
|
+
*/
|
|
455
|
+
export function spectralClustering(s: GraphSnapshot, options: SpectralOptions): SpectralResult {
|
|
456
|
+
const { k } = options;
|
|
457
|
+
const type = options.laplacianType ?? "normalized";
|
|
458
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
459
|
+
const tolerance = options.tolerance ?? 1e-4;
|
|
460
|
+
const seed = options.seed ?? 42;
|
|
461
|
+
if (!Number.isInteger(k) || k < 1) {
|
|
462
|
+
throw new RangeError(`k must be a positive integer, got ${k}`);
|
|
463
|
+
}
|
|
464
|
+
if (!["unnormalized", "normalized", "randomWalk"].includes(type)) {
|
|
465
|
+
throw new RangeError(`unknown laplacianType "${type}"`);
|
|
466
|
+
}
|
|
467
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 0 || !(tolerance >= 0) || !Number.isInteger(seed)) {
|
|
468
|
+
throw new RangeError("maxIterations and seed must be integers, maxIterations and tolerance non-negative");
|
|
469
|
+
}
|
|
470
|
+
const weights = options.weights ?? s.weights;
|
|
471
|
+
if (weights !== null && weights.length !== s.arcCount) {
|
|
472
|
+
throw new RangeError(`weights has ${weights.length} entries; the snapshot has ${s.arcCount} arcs`);
|
|
473
|
+
}
|
|
474
|
+
const n = s.nodeCount;
|
|
475
|
+
if (k >= n) {
|
|
476
|
+
const labels = Uint32Array.from({ length: n }, (_, i) => i);
|
|
477
|
+
return { ...withGroups(labels, n), eigenvalues: new Float64Array(0), eigenvectors: [], converged: true };
|
|
478
|
+
}
|
|
479
|
+
const g = symmetricRows(s, weights);
|
|
480
|
+
const scale = g.degree.map((d) => (d > 0 ? 1 / Math.sqrt(d) : 0));
|
|
481
|
+
const normalised = type !== "unnormalized";
|
|
482
|
+
const apply = (x: Float64Array, out: Float64Array): void => {
|
|
483
|
+
for (let u = 0; u < n; u++) {
|
|
484
|
+
let ax = 0;
|
|
485
|
+
for (let a = g.rowPtr[u]; a < g.rowPtr[u + 1]; a++) {
|
|
486
|
+
const v = g.colIdx[a];
|
|
487
|
+
ax += normalised ? g.w[a] * scale[v] * x[v] : g.w[a] * x[v];
|
|
488
|
+
}
|
|
489
|
+
if (normalised) {
|
|
490
|
+
out[u] = g.degree[u] > 0 ? x[u] - scale[u] * ax : 0;
|
|
491
|
+
} else {
|
|
492
|
+
out[u] = g.degree[u] * x[u] - ax;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
// The largest eigenvalue is at most 2 for the normalised Laplacian, and at most the largest
|
|
497
|
+
// d(u) + d(v) over the edges for D - A (Anderson and Morley, 1985).
|
|
498
|
+
let shift = 2;
|
|
499
|
+
if (!normalised) {
|
|
500
|
+
shift = 1;
|
|
501
|
+
for (let u = 0; u < n; u++) {
|
|
502
|
+
for (let a = g.rowPtr[u]; a < g.rowPtr[u + 1]; a++) {
|
|
503
|
+
shift = Math.max(shift, g.degree[u] + g.degree[g.colIdx[a]]);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const random = mulberry32(seed);
|
|
508
|
+
const eig = smallestEigenpairs(apply, n, k, shift, random);
|
|
509
|
+
if (type === "randomWalk") {
|
|
510
|
+
for (const v of eig.vectors) {
|
|
511
|
+
for (let i = 0; i < n; i++) {
|
|
512
|
+
v[i] *= g.degree[i] > 0 ? scale[i] : 1;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const points = new Float64Array(n * k);
|
|
517
|
+
for (let c = 0; c < k; c++) {
|
|
518
|
+
for (let i = 0; i < n; i++) {
|
|
519
|
+
points[i * k + c] = eig.vectors[c][i];
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (type === "normalized") {
|
|
523
|
+
for (let i = 0; i < n; i++) {
|
|
524
|
+
const row = points.subarray(i * k, i * k + k);
|
|
525
|
+
const norm = Math.hypot(...row);
|
|
526
|
+
if (norm > 0) {
|
|
527
|
+
row.forEach((x, c) => (row[c] = x / norm));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
// Ten seeded k-means runs, keeping the tightest (the scikit-learn default): one run from an
|
|
532
|
+
// unlucky seeding can split a clean cluster.
|
|
533
|
+
let best: U32 = new Uint32Array(n);
|
|
534
|
+
let bestInertia = Infinity;
|
|
535
|
+
for (let run = 0; run < K_MEANS_RUNS; run++) {
|
|
536
|
+
const assign = kMeans(points, n, k, k, maxIterations, tolerance, random);
|
|
537
|
+
const inertia = inertiaOf(points, assign, n, k, k);
|
|
538
|
+
if (inertia < bestInertia) {
|
|
539
|
+
bestInertia = inertia;
|
|
540
|
+
best = assign;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const { labels, count } = renumberPartition(best);
|
|
544
|
+
return {
|
|
545
|
+
...withGroups(labels, count),
|
|
546
|
+
eigenvalues: eig.values,
|
|
547
|
+
eigenvectors: eig.vectors,
|
|
548
|
+
converged: eig.converged,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { makeMask, maskCount, maskSet, maskTest, maskToIndices, type NodeMask } from "@graphty/graph-format";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A fixed-length set of indices over graph-format's packed mask words, so `words` can be handed
|
|
5
|
+
* to anything that takes a `NodeMask` or `EdgeMask` (`maskToIndices`, `filterEdges`,
|
|
6
|
+
* `inducedSubgraph({ mask })`) without a copy.
|
|
7
|
+
*/
|
|
8
|
+
export class BitSet {
|
|
9
|
+
/** The packed words, `ceil(length / 32)` of them; shared, not copied. */
|
|
10
|
+
readonly words: NodeMask;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create an empty set over indices in `[0, length)`.
|
|
14
|
+
* @param length - The number of indices the set covers
|
|
15
|
+
*/
|
|
16
|
+
constructor(readonly length: number) {
|
|
17
|
+
this.words = makeMask(length);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether an index is in the set.
|
|
22
|
+
* @param i - An index below `length`
|
|
23
|
+
* @returns True when present
|
|
24
|
+
*/
|
|
25
|
+
has(i: number): boolean {
|
|
26
|
+
return maskTest(this.words, i);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Include an index.
|
|
31
|
+
* @param i - An index below `length`
|
|
32
|
+
*/
|
|
33
|
+
add(i: number): void {
|
|
34
|
+
maskSet(this.words, i, true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Exclude an index.
|
|
39
|
+
* @param i - An index below `length`
|
|
40
|
+
*/
|
|
41
|
+
delete(i: number): void {
|
|
42
|
+
maskSet(this.words, i, false);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Exclude every index. */
|
|
46
|
+
clear(): void {
|
|
47
|
+
this.words.fill(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The number of included indices.
|
|
52
|
+
* @returns The population count
|
|
53
|
+
*/
|
|
54
|
+
count(): number {
|
|
55
|
+
return maskCount(this.words, this.length);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The included indices in ascending order.
|
|
60
|
+
* @returns A fresh array of indices
|
|
61
|
+
*/
|
|
62
|
+
toIndices(): Uint32Array {
|
|
63
|
+
return maskToIndices(this.words, this.length);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { IndexedMinHeap } from "./min-heap.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A binary max-heap over node indices with O(log n) increase-key: an {@link IndexedMinHeap} over
|
|
5
|
+
* negated keys. Negation is exact in f64, so keys come back bit-for-bit. Stoer-Wagner's
|
|
6
|
+
* maximum-adjacency ordering is the caller it exists for: every node starts at 0 and gains the
|
|
7
|
+
* weight of each edge to the growing set, `pushOrIncrease(v, keyOf(v) + w)`. Of two equal keys the
|
|
8
|
+
* lower node index pops first, which is the order the legacy Stoer-Wagner scan picks in.
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export class IndexedMaxHeap {
|
|
12
|
+
private readonly min: IndexedMinHeap;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an empty heap over node indices in `[0, capacity)`.
|
|
16
|
+
* @param capacity - The number of distinct node indices the heap may hold
|
|
17
|
+
*/
|
|
18
|
+
constructor(capacity: number) {
|
|
19
|
+
this.min = new IndexedMinHeap(capacity, true);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether the heap holds no entries.
|
|
24
|
+
* @returns True when the heap holds no entries
|
|
25
|
+
*/
|
|
26
|
+
isEmpty(): boolean {
|
|
27
|
+
return this.min.isEmpty();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether a node is currently in the heap.
|
|
32
|
+
* @param node - The node index
|
|
33
|
+
* @returns True when the node was pushed and has not been popped since
|
|
34
|
+
*/
|
|
35
|
+
has(node: number): boolean {
|
|
36
|
+
return this.min.has(node);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The key a node was last given. Meaningful only while `has(node)` is true.
|
|
41
|
+
* @param node - The node index
|
|
42
|
+
* @returns Its key
|
|
43
|
+
*/
|
|
44
|
+
keyOf(node: number): number {
|
|
45
|
+
return -this.min.keyOf(node);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Insert a node that is not in the heap.
|
|
50
|
+
* @param node - The node index
|
|
51
|
+
* @param key - Its key
|
|
52
|
+
*/
|
|
53
|
+
push(node: number, key: number): void {
|
|
54
|
+
this.min.push(node, -key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Insert the node, or raise its key if it is already present and the new key is larger.
|
|
59
|
+
* @param node - The node index
|
|
60
|
+
* @param key - Its candidate key
|
|
61
|
+
*/
|
|
62
|
+
pushOrIncrease(node: number, key: number): void {
|
|
63
|
+
this.min.pushOrDecrease(node, -key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove and return the node with the largest key. Undefined behaviour on an empty heap;
|
|
68
|
+
* callers guard with `isEmpty()`.
|
|
69
|
+
* @returns The node index
|
|
70
|
+
*/
|
|
71
|
+
pop(): number {
|
|
72
|
+
return this.min.pop();
|
|
73
|
+
}
|
|
74
|
+
}
|