@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,110 @@
|
|
|
1
|
+
import { INVALID_INDEX, } from "@graphty/graph-format";
|
|
2
|
+
import { checkArcOrder } from "./bfs.js";
|
|
3
|
+
import { withGroups } from "./components.js";
|
|
4
|
+
/**
|
|
5
|
+
* Strongly connected components by an iterative Tarjan: roots in index order, neighbours in row
|
|
6
|
+
* order unless `options.arcOrder` gives another, and component `c` is the `c`-th to complete -- the numbering the legacy
|
|
7
|
+
* `stronglyConnectedComponents` gives its result array. Completion order is a reverse topological
|
|
8
|
+
* order of the condensation.
|
|
9
|
+
* @param g - A directed adjacency
|
|
10
|
+
* @param options - The neighbour order
|
|
11
|
+
* @returns The partition, labels in completion order
|
|
12
|
+
* @throws Error when the graph is undirected
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
export function stronglyConnectedComponents(g, options = {}) {
|
|
16
|
+
const { labels, count } = tarjan(g, options);
|
|
17
|
+
return withGroups(labels, count);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The Tarjan walk behind {@link stronglyConnectedComponents}, also returning every node in the
|
|
21
|
+
* order it left the component stack. Each component leaves as one run, in label order, its members
|
|
22
|
+
* in reverse discovery order -- the order the legacy `stronglyConnectedComponents` lists them in.
|
|
23
|
+
* @param g - A directed adjacency
|
|
24
|
+
* @param options - The neighbour order
|
|
25
|
+
* @returns The labels, their count and the pop order
|
|
26
|
+
* @throws Error when the graph is undirected
|
|
27
|
+
*/
|
|
28
|
+
export function tarjan(g, options = {}) {
|
|
29
|
+
if (!g.directed) {
|
|
30
|
+
throw new Error("Strongly connected components require a directed graph. Use connectedComponents for an undirected one.");
|
|
31
|
+
}
|
|
32
|
+
const { nodeCount, rowPtr, colIdx } = g;
|
|
33
|
+
const arcOrder = checkArcOrder(g, options.arcOrder);
|
|
34
|
+
const index = new Uint32Array(nodeCount).fill(INVALID_INDEX);
|
|
35
|
+
const low = new Uint32Array(nodeCount);
|
|
36
|
+
const cursor = new Uint32Array(nodeCount);
|
|
37
|
+
const onStack = new Uint8Array(nodeCount);
|
|
38
|
+
const callStack = new Uint32Array(nodeCount);
|
|
39
|
+
const sccStack = new Uint32Array(nodeCount);
|
|
40
|
+
const labels = new Uint32Array(nodeCount);
|
|
41
|
+
const popped = new Uint32Array(nodeCount);
|
|
42
|
+
let poppedCount = 0;
|
|
43
|
+
let next = 0;
|
|
44
|
+
let count = 0;
|
|
45
|
+
let sp = 0;
|
|
46
|
+
let top = 0;
|
|
47
|
+
// The body of the recursive call before its loop over neighbours.
|
|
48
|
+
const enter = (u) => {
|
|
49
|
+
index[u] = next;
|
|
50
|
+
low[u] = next;
|
|
51
|
+
next++;
|
|
52
|
+
cursor[u] = rowPtr[u];
|
|
53
|
+
sccStack[sp++] = u;
|
|
54
|
+
onStack[u] = 1;
|
|
55
|
+
callStack[top++] = u;
|
|
56
|
+
};
|
|
57
|
+
for (let r = 0; r < nodeCount; r++) {
|
|
58
|
+
if (index[r] !== INVALID_INDEX) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
enter(r);
|
|
62
|
+
while (top > 0) {
|
|
63
|
+
const x = callStack[top - 1];
|
|
64
|
+
if (cursor[x] < rowPtr[x + 1]) {
|
|
65
|
+
const a = cursor[x]++;
|
|
66
|
+
const v = colIdx[arcOrder === null ? a : arcOrder[a]];
|
|
67
|
+
if (index[v] === INVALID_INDEX) {
|
|
68
|
+
enter(v);
|
|
69
|
+
}
|
|
70
|
+
else if (onStack[v] === 1 && index[v] < low[x]) {
|
|
71
|
+
low[x] = index[v];
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// x returns: close its component if it is a root, then pass its low link to the caller.
|
|
76
|
+
top--;
|
|
77
|
+
if (low[x] === index[x]) {
|
|
78
|
+
let w;
|
|
79
|
+
do {
|
|
80
|
+
w = sccStack[--sp];
|
|
81
|
+
onStack[w] = 0;
|
|
82
|
+
labels[w] = count;
|
|
83
|
+
popped[poppedCount++] = w;
|
|
84
|
+
} while (w !== x);
|
|
85
|
+
count++;
|
|
86
|
+
}
|
|
87
|
+
if (top > 0 && low[x] < low[callStack[top - 1]]) {
|
|
88
|
+
low[callStack[top - 1]] = low[x];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { labels, count, popped };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The condensation of a directed graph: its strongly connected components, and the DAG with one
|
|
96
|
+
* node per component that `contract()` builds from their labels. The labels already run `0..k-1`,
|
|
97
|
+
* so contract keeps them as block indices and the condensed node numbering is the component
|
|
98
|
+
* numbering.
|
|
99
|
+
* @param s - A directed snapshot
|
|
100
|
+
* @param options - The neighbour order Tarjan tries arcs in, which fixes the component numbering
|
|
101
|
+
* @returns The components and the condensed graph
|
|
102
|
+
* @throws Error when the snapshot is undirected
|
|
103
|
+
* @public
|
|
104
|
+
*/
|
|
105
|
+
export function condensation(s, options = {}) {
|
|
106
|
+
const components = stronglyConnectedComponents(s, options);
|
|
107
|
+
const condensed = s.contract(components.labels, { selfLoops: "drop", parallel: "merge", weights: "first" });
|
|
108
|
+
return { components, condensed };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=scc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scc.js","sourceRoot":"","sources":["../../../src/indexed/scc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type GraphSnapshot, type NumericVector } from "@graphty/graph-format";
|
|
2
|
+
import { type LabelResult } from "./components.js";
|
|
3
|
+
/** Which graph Laplacian the embedding comes from. @public */
|
|
4
|
+
export type LaplacianType = "unnormalized" | "normalized" | "randomWalk";
|
|
5
|
+
/** Options of the index-based spectral clustering. @public */
|
|
6
|
+
export interface SpectralOptions {
|
|
7
|
+
/** Number of clusters, a positive integer. */
|
|
8
|
+
readonly k: number;
|
|
9
|
+
/** `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. */
|
|
10
|
+
readonly laplacianType?: LaplacianType | undefined;
|
|
11
|
+
/** Cap on k-means rounds; default 100. */
|
|
12
|
+
readonly maxIterations?: number | undefined;
|
|
13
|
+
/** k-means stops when no centroid moves further than this; default 1e-4. */
|
|
14
|
+
readonly tolerance?: number | undefined;
|
|
15
|
+
/** Seed of the starting block and of the k-means seeding; default 42. */
|
|
16
|
+
readonly seed?: number | undefined;
|
|
17
|
+
/** Per-arc weight override, arcCount long -- the facade passes `expandEdges(s, shadow.data)`. */
|
|
18
|
+
readonly weights?: NumericVector | undefined;
|
|
19
|
+
}
|
|
20
|
+
/** Result of the index-based spectral clustering. @public */
|
|
21
|
+
export interface SpectralResult extends LabelResult {
|
|
22
|
+
/** The k smallest eigenvalues of the Laplacian, ascending; empty when k >= nodeCount. */
|
|
23
|
+
readonly eigenvalues: Float64Array;
|
|
24
|
+
/** One nodeCount-long eigenvector per eigenvalue; for randomWalk, of `I - D^-1 A`. */
|
|
25
|
+
readonly eigenvectors: Float64Array[];
|
|
26
|
+
/**
|
|
27
|
+
* Whether the eigenpairs met the residual tolerance. False when the iteration stopped at its
|
|
28
|
+
* round cap -- on a graph whose small eigenvalues crowd together, such as a long path -- and
|
|
29
|
+
* the eigenpairs are then approximations (the labels usually still hold). True when there
|
|
30
|
+
* were no eigenpairs to compute.
|
|
31
|
+
*/
|
|
32
|
+
readonly converged: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Spectral clustering, the index-based port of the legacy `spectralClustering`: the k eigenvectors
|
|
36
|
+
* of the graph Laplacian with the smallest eigenvalues embed every node as a point in k dimensions,
|
|
37
|
+
* and k-means splits the points into at most k clusters.
|
|
38
|
+
*
|
|
39
|
+
* The Laplacian is applied straight from the CSR rows and never built: `D - A`
|
|
40
|
+
* (`unnormalized`), `I - D^-1/2 A D^-1/2` (`normalized`, whose embedded rows are then scaled to
|
|
41
|
+
* unit length, as Ng, Jordan and Weiss do) or `I - D^-1 A` (`randomWalk`, whose eigenvectors are
|
|
42
|
+
* `D^-1/2` times the normalized ones). A node of degree 0 has a zero row. Self-loops are ignored
|
|
43
|
+
* (they cancel in `D - A`) and a directed snapshot is read as undirected, a reciprocal pair
|
|
44
|
+
* summing. A negative, NaN or infinite weight throws. With `k >= nodeCount` every node is its own
|
|
45
|
+
* cluster.
|
|
46
|
+
*
|
|
47
|
+
* The eigenpairs come from subspace iteration with Rayleigh-Ritz, so they are the true smallest
|
|
48
|
+
* ones when `converged` is true; `converged` is false when the iteration stopped at its round cap
|
|
49
|
+
* (a small spectral gap, such as a path of hundreds of nodes), and the eigenpairs are then only
|
|
50
|
+
* approximate. The legacy function uses fixed placeholder eigenvalues for k <= 3 and plain power
|
|
51
|
+
* iteration (which finds the LARGEST eigenvectors) above that, and draws its k-means seeds from
|
|
52
|
+
* `Math.random` unless given a seed. The partitions therefore differ from the legacy function's;
|
|
53
|
+
* one `seed` gives one result. Labels are dense in first-seen order, so `count` is below k when
|
|
54
|
+
* k-means leaves a cluster empty.
|
|
55
|
+
* @param s - Any snapshot
|
|
56
|
+
* @param options - k, the Laplacian, the k-means limits, the seed and the weight override
|
|
57
|
+
* @returns The partition and the eigenpairs it came from
|
|
58
|
+
* @public
|
|
59
|
+
*/
|
|
60
|
+
export declare function spectralClustering(s: GraphSnapshot, options: SpectralOptions): SpectralResult;
|
|
61
|
+
//# sourceMappingURL=spectral.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spectral.d.ts","sourceRoot":"","sources":["../../../src/indexed/spectral.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,KAAK,aAAa,EAClB,KAAK,aAAa,EAGrB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,KAAK,WAAW,EAAc,MAAM,iBAAiB,CAAC;AAE/D,8DAA8D;AAC9D,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,YAAY,CAAC;AAEzE,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC5B,8CAA8C;IAC9C,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,wHAAwH;IACxH,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;IACnD,0CAA0C;IAC1C,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,iGAAiG;IACjG,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC;CAChD;AAED,6DAA6D;AAC7D,MAAM,WAAW,cAAe,SAAQ,WAAW;IAC/C,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC;IACnC,sFAAsF;IACtF,QAAQ,CAAC,YAAY,EAAE,YAAY,EAAE,CAAC;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC/B;AAkYD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CA+F7F"}
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { renumberPartition, } from "@graphty/graph-format";
|
|
2
|
+
import { withGroups } from "./components.js";
|
|
3
|
+
/** Relative eigenvector residual at which subspace iteration stops. */
|
|
4
|
+
const RESIDUAL_TOLERANCE = 1e-11;
|
|
5
|
+
/** Subspace-iteration cap; each round costs one sparse product per block vector. */
|
|
6
|
+
const MAX_SUBSPACE_ROUNDS = 3000;
|
|
7
|
+
/**
|
|
8
|
+
* mulberry32, the generator the label propagation port uses.
|
|
9
|
+
* @param seed - Generator seed; only its low 32 bits are used
|
|
10
|
+
* @returns The generator, output in [0, 1)
|
|
11
|
+
*/
|
|
12
|
+
function mulberry32(seed) {
|
|
13
|
+
let a = seed >>> 0;
|
|
14
|
+
return () => {
|
|
15
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
16
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
17
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
18
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The snapshot's adjacency made symmetric -- a directed snapshot's out-row and in-row together, so
|
|
23
|
+
* a reciprocal pair sums -- with self-loops dropped (they cancel in `D - A`) and weights checked.
|
|
24
|
+
* @param s - The snapshot
|
|
25
|
+
* @param weights - Per-arc weights, or null for all ones
|
|
26
|
+
* @returns The rows and the weighted degrees
|
|
27
|
+
*/
|
|
28
|
+
function symmetricRows(s, weights) {
|
|
29
|
+
const n = s.nodeCount;
|
|
30
|
+
const views = [{ view: s, arcOf: (a) => a }];
|
|
31
|
+
if (s.directed) {
|
|
32
|
+
const rev = s.reverse();
|
|
33
|
+
views.push({ view: rev, arcOf: (k) => rev.fwdArc[k] });
|
|
34
|
+
}
|
|
35
|
+
const rowPtr = new Uint32Array(n + 1);
|
|
36
|
+
for (let u = 0; u < n; u++) {
|
|
37
|
+
let kept = 0;
|
|
38
|
+
for (const { view } of views) {
|
|
39
|
+
for (let a = view.rowPtr[u]; a < view.rowPtr[u + 1]; a++) {
|
|
40
|
+
kept += view.colIdx[a] === u ? 0 : 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
rowPtr[u + 1] = rowPtr[u] + kept;
|
|
44
|
+
}
|
|
45
|
+
const colIdx = new Uint32Array(rowPtr[n]);
|
|
46
|
+
const w = new Float64Array(rowPtr[n]);
|
|
47
|
+
const degree = new Float64Array(n);
|
|
48
|
+
let k = 0;
|
|
49
|
+
for (let u = 0; u < n; u++) {
|
|
50
|
+
for (const { view, arcOf } of views) {
|
|
51
|
+
for (let a = view.rowPtr[u]; a < view.rowPtr[u + 1]; a++) {
|
|
52
|
+
const weight = weights === null ? 1 : weights[arcOf(a)];
|
|
53
|
+
if (!(weight >= 0) || weight === Infinity) {
|
|
54
|
+
throw new RangeError(`an arc has weight ${weight}; spectral clustering needs finite, non-negative weights`);
|
|
55
|
+
}
|
|
56
|
+
if (view.colIdx[a] !== u) {
|
|
57
|
+
colIdx[k] = view.colIdx[a];
|
|
58
|
+
w[k++] = weight;
|
|
59
|
+
degree[u] += weight;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { rowPtr, colIdx, w, degree };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Orthonormalise the columns of `block` in place (Gram-Schmidt, applied twice for stability). A
|
|
68
|
+
* column that collapses is replaced by a fresh random one.
|
|
69
|
+
* @param block - p columns of length n
|
|
70
|
+
* @param random - The generator for replacements
|
|
71
|
+
*/
|
|
72
|
+
function orthonormalise(block, random) {
|
|
73
|
+
for (let j = 0; j < block.length; j++) {
|
|
74
|
+
const v = block[j];
|
|
75
|
+
for (let attempt = 0;; attempt++) {
|
|
76
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
77
|
+
for (let i = 0; i < j; i++) {
|
|
78
|
+
const q = block[i];
|
|
79
|
+
let dot = 0;
|
|
80
|
+
for (let r = 0; r < v.length; r++) {
|
|
81
|
+
dot += q[r] * v[r];
|
|
82
|
+
}
|
|
83
|
+
for (let r = 0; r < v.length; r++) {
|
|
84
|
+
v[r] -= dot * q[r];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
let norm = 0;
|
|
89
|
+
for (let r = 0; r < v.length; r++) {
|
|
90
|
+
norm += v[r] * v[r];
|
|
91
|
+
}
|
|
92
|
+
norm = Math.sqrt(norm);
|
|
93
|
+
if (norm > 1e-10 || attempt === 3) {
|
|
94
|
+
for (let r = 0; r < v.length; r++) {
|
|
95
|
+
v[r] = norm > 0 ? v[r] / norm : 0;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
for (let r = 0; r < v.length; r++) {
|
|
100
|
+
v[r] = random() - 0.5;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Eigen-decomposition of a small symmetric matrix by cyclic Jacobi rotations.
|
|
107
|
+
* @param a - p x p row-major, destroyed
|
|
108
|
+
* @param p - The order
|
|
109
|
+
* @returns The eigenvalues and the eigenvectors as the columns of a p x p row-major matrix
|
|
110
|
+
*/
|
|
111
|
+
function jacobi(a, p) {
|
|
112
|
+
const v = new Float64Array(p * p);
|
|
113
|
+
for (let i = 0; i < p; i++) {
|
|
114
|
+
v[i * p + i] = 1;
|
|
115
|
+
}
|
|
116
|
+
for (let sweep = 0; sweep < 100; sweep++) {
|
|
117
|
+
let off = 0;
|
|
118
|
+
for (let i = 0; i < p; i++) {
|
|
119
|
+
for (let j = i + 1; j < p; j++) {
|
|
120
|
+
off += a[i * p + j] * a[i * p + j];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (off < 1e-30) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
for (let i = 0; i < p; i++) {
|
|
127
|
+
for (let j = i + 1; j < p; j++) {
|
|
128
|
+
const aij = a[i * p + j];
|
|
129
|
+
if (aij === 0) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const theta = (a[j * p + j] - a[i * p + i]) / (2 * aij);
|
|
133
|
+
const t = Math.sign(theta || 1) / (Math.abs(theta) + Math.sqrt(theta * theta + 1));
|
|
134
|
+
const c = 1 / Math.sqrt(t * t + 1);
|
|
135
|
+
const sn = t * c;
|
|
136
|
+
for (let r = 0; r < p; r++) {
|
|
137
|
+
const ari = a[r * p + i];
|
|
138
|
+
const arj = a[r * p + j];
|
|
139
|
+
a[r * p + i] = c * ari - sn * arj;
|
|
140
|
+
a[r * p + j] = sn * ari + c * arj;
|
|
141
|
+
}
|
|
142
|
+
for (let r = 0; r < p; r++) {
|
|
143
|
+
const air = a[i * p + r];
|
|
144
|
+
const ajr = a[j * p + r];
|
|
145
|
+
a[i * p + r] = c * air - sn * ajr;
|
|
146
|
+
a[j * p + r] = sn * air + c * ajr;
|
|
147
|
+
}
|
|
148
|
+
for (let r = 0; r < p; r++) {
|
|
149
|
+
const vri = v[r * p + i];
|
|
150
|
+
const vrj = v[r * p + j];
|
|
151
|
+
v[r * p + i] = c * vri - sn * vrj;
|
|
152
|
+
v[r * p + j] = sn * vri + c * vrj;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { values: Float64Array.from({ length: p }, (_, i) => a[i * p + i]), vectors: v };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The k smallest eigenpairs of a symmetric Laplacian, by subspace iteration on `shift * I - L` (whose
|
|
161
|
+
* largest eigenpairs they are) with a Rayleigh-Ritz step, over a block a few vectors wider than k.
|
|
162
|
+
* @param apply - Writes `L x` into `out`
|
|
163
|
+
* @param n - Order
|
|
164
|
+
* @param k - Eigenpairs wanted
|
|
165
|
+
* @param shift - An upper bound on the largest eigenvalue of L
|
|
166
|
+
* @param random - The generator of the starting block
|
|
167
|
+
* @returns Eigenvalues ascending, their eigenvectors, and whether they met the tolerance
|
|
168
|
+
*/
|
|
169
|
+
function smallestEigenpairs(apply, n, k, shift, random) {
|
|
170
|
+
const p = Math.min(n, k + Math.max(k, 8));
|
|
171
|
+
let block = Array.from({ length: p }, () => Float64Array.from({ length: n }, () => random() - 0.5));
|
|
172
|
+
orthonormalise(block, random);
|
|
173
|
+
const lx = new Float64Array(n);
|
|
174
|
+
const shifted = (x) => {
|
|
175
|
+
apply(x, lx);
|
|
176
|
+
return x.map((xi, r) => shift * xi - lx[r]);
|
|
177
|
+
};
|
|
178
|
+
let ritz = new Float64Array(p);
|
|
179
|
+
let order = [];
|
|
180
|
+
let vectors = new Float64Array(p * p);
|
|
181
|
+
let converged = false;
|
|
182
|
+
// ponytail: plain subspace iteration converges at the ratio of neighbouring shifted eigenvalues,
|
|
183
|
+
// so a small spectral gap hits the cap; Chebyshev filtering would converge far faster there.
|
|
184
|
+
for (let round = 0; round < MAX_SUBSPACE_ROUNDS; round++) {
|
|
185
|
+
const image = block.map(shifted);
|
|
186
|
+
// Rayleigh-Ritz: the block's projection of the shifted operator, diagonalised.
|
|
187
|
+
const h = new Float64Array(p * p);
|
|
188
|
+
for (let i = 0; i < p; i++) {
|
|
189
|
+
for (let j = i; j < p; j++) {
|
|
190
|
+
let dot = 0;
|
|
191
|
+
for (let r = 0; r < n; r++) {
|
|
192
|
+
dot += block[i][r] * image[j][r];
|
|
193
|
+
}
|
|
194
|
+
h[i * p + j] = dot;
|
|
195
|
+
h[j * p + i] = dot;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const eig = jacobi(h, p);
|
|
199
|
+
({ values: ritz, vectors } = eig);
|
|
200
|
+
order = Array.from({ length: p }, (_, i) => i).sort((a, b) => ritz[b] - ritz[a]);
|
|
201
|
+
// Converged when every wanted Ritz pair (y = X q, theta) has a residual |B y - theta y|
|
|
202
|
+
// below RESIDUAL_TOLERANCE * shift; B y is the image block times q.
|
|
203
|
+
converged = order.slice(0, k).every((col) => {
|
|
204
|
+
let residual = 0;
|
|
205
|
+
for (let r = 0; r < n; r++) {
|
|
206
|
+
let diff = 0;
|
|
207
|
+
for (let i = 0; i < p; i++) {
|
|
208
|
+
diff += vectors[i * p + col] * (image[i][r] - ritz[col] * block[i][r]);
|
|
209
|
+
}
|
|
210
|
+
residual += diff * diff;
|
|
211
|
+
}
|
|
212
|
+
return Math.sqrt(residual) <= RESIDUAL_TOLERANCE * shift;
|
|
213
|
+
});
|
|
214
|
+
if (converged || round === MAX_SUBSPACE_ROUNDS - 1) {
|
|
215
|
+
break; // at the cap the Ritz pairs of this block are the best estimate there is
|
|
216
|
+
}
|
|
217
|
+
block = image;
|
|
218
|
+
orthonormalise(block, random);
|
|
219
|
+
}
|
|
220
|
+
const values = new Float64Array(k);
|
|
221
|
+
const out = [];
|
|
222
|
+
for (let rank = 0; rank < k; rank++) {
|
|
223
|
+
const col = order[rank];
|
|
224
|
+
values[rank] = Math.max(0, shift - ritz[col]);
|
|
225
|
+
const v = new Float64Array(n);
|
|
226
|
+
for (let i = 0; i < p; i++) {
|
|
227
|
+
const c = vectors[i * p + col];
|
|
228
|
+
for (let r = 0; r < n; r++) {
|
|
229
|
+
v[r] += c * block[i][r];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
out.push(v);
|
|
233
|
+
}
|
|
234
|
+
return { values, vectors: out, converged };
|
|
235
|
+
}
|
|
236
|
+
/** k-means runs per clustering; the one with the lowest inertia is kept. */
|
|
237
|
+
const K_MEANS_RUNS = 10;
|
|
238
|
+
/**
|
|
239
|
+
* The k-means objective: the summed squared distance of every row from its cluster's mean.
|
|
240
|
+
* @param points - n rows of length d, row-major
|
|
241
|
+
* @param assign - The cluster of every row
|
|
242
|
+
* @param n - Row count
|
|
243
|
+
* @param d - Row length
|
|
244
|
+
* @param k - Cluster count
|
|
245
|
+
* @returns The inertia
|
|
246
|
+
*/
|
|
247
|
+
function inertiaOf(points, assign, n, d, k) {
|
|
248
|
+
const sums = new Float64Array(k * d);
|
|
249
|
+
const sizes = new Float64Array(k);
|
|
250
|
+
for (let i = 0; i < n; i++) {
|
|
251
|
+
sizes[assign[i]]++;
|
|
252
|
+
for (let t = 0; t < d; t++) {
|
|
253
|
+
sums[assign[i] * d + t] += points[i * d + t];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
let total = 0;
|
|
257
|
+
for (let i = 0; i < n; i++) {
|
|
258
|
+
const c = assign[i];
|
|
259
|
+
for (let t = 0; t < d; t++) {
|
|
260
|
+
total += (points[i * d + t] - sums[c * d + t] / sizes[c]) ** 2;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return total;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* k-means over the rows of an n x d embedding, seeded with k-means++ and refined by Lloyd rounds.
|
|
267
|
+
* @param points - n rows of length d, row-major
|
|
268
|
+
* @param n - Row count
|
|
269
|
+
* @param d - Row length
|
|
270
|
+
* @param k - Cluster count
|
|
271
|
+
* @param maxIterations - Round cap
|
|
272
|
+
* @param tolerance - Stop when no centroid moves further than this
|
|
273
|
+
* @param random - The generator
|
|
274
|
+
* @returns The cluster of every row
|
|
275
|
+
*/
|
|
276
|
+
function kMeans(points, n, d, k, maxIterations, tolerance, random) {
|
|
277
|
+
const dist2 = (i, centres, c) => {
|
|
278
|
+
let sum = 0;
|
|
279
|
+
for (let t = 0; t < d; t++) {
|
|
280
|
+
const diff = points[i * d + t] - centres[c * d + t];
|
|
281
|
+
sum += diff * diff;
|
|
282
|
+
}
|
|
283
|
+
return sum;
|
|
284
|
+
};
|
|
285
|
+
const centres = new Float64Array(k * d);
|
|
286
|
+
const nearest = new Float64Array(n).fill(Infinity);
|
|
287
|
+
let pick = Math.floor(random() * n);
|
|
288
|
+
for (let c = 0; c < k; c++) {
|
|
289
|
+
centres.set(points.subarray(pick * d, pick * d + d), c * d);
|
|
290
|
+
let total = 0;
|
|
291
|
+
for (let i = 0; i < n; i++) {
|
|
292
|
+
nearest[i] = Math.min(nearest[i], dist2(i, centres, c));
|
|
293
|
+
total += nearest[i];
|
|
294
|
+
}
|
|
295
|
+
// k-means++: the next centre is a row drawn with probability proportional to its squared
|
|
296
|
+
// distance from the nearest centre so far (the first row when every row sits on a centre).
|
|
297
|
+
let target = random() * total;
|
|
298
|
+
pick = 0;
|
|
299
|
+
for (let i = 0; i < n && total > 0; i++) {
|
|
300
|
+
target -= nearest[i];
|
|
301
|
+
if (target < 0) {
|
|
302
|
+
pick = i;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const assign = new Uint32Array(n);
|
|
308
|
+
for (let round = 0; round < maxIterations; round++) {
|
|
309
|
+
let changed = round === 0;
|
|
310
|
+
for (let i = 0; i < n; i++) {
|
|
311
|
+
let best = 0;
|
|
312
|
+
let bestDist = Infinity;
|
|
313
|
+
for (let c = 0; c < k; c++) {
|
|
314
|
+
const dd = dist2(i, centres, c);
|
|
315
|
+
if (dd < bestDist) {
|
|
316
|
+
bestDist = dd;
|
|
317
|
+
best = c;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
changed || (changed = assign[i] !== best);
|
|
321
|
+
assign[i] = best;
|
|
322
|
+
}
|
|
323
|
+
if (!changed) {
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
const sums = new Float64Array(k * d);
|
|
327
|
+
const sizes = new Float64Array(k);
|
|
328
|
+
for (let i = 0; i < n; i++) {
|
|
329
|
+
sizes[assign[i]]++;
|
|
330
|
+
for (let t = 0; t < d; t++) {
|
|
331
|
+
sums[assign[i] * d + t] += points[i * d + t];
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
let shift = 0;
|
|
335
|
+
for (let c = 0; c < k; c++) {
|
|
336
|
+
if (sizes[c] === 0) {
|
|
337
|
+
continue; // an emptied cluster keeps its centre
|
|
338
|
+
}
|
|
339
|
+
let moved = 0;
|
|
340
|
+
for (let t = 0; t < d; t++) {
|
|
341
|
+
const next = sums[c * d + t] / sizes[c];
|
|
342
|
+
moved += (next - centres[c * d + t]) ** 2;
|
|
343
|
+
centres[c * d + t] = next;
|
|
344
|
+
}
|
|
345
|
+
shift = Math.max(shift, Math.sqrt(moved));
|
|
346
|
+
}
|
|
347
|
+
if (shift < tolerance) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return assign;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Spectral clustering, the index-based port of the legacy `spectralClustering`: the k eigenvectors
|
|
355
|
+
* of the graph Laplacian with the smallest eigenvalues embed every node as a point in k dimensions,
|
|
356
|
+
* and k-means splits the points into at most k clusters.
|
|
357
|
+
*
|
|
358
|
+
* The Laplacian is applied straight from the CSR rows and never built: `D - A`
|
|
359
|
+
* (`unnormalized`), `I - D^-1/2 A D^-1/2` (`normalized`, whose embedded rows are then scaled to
|
|
360
|
+
* unit length, as Ng, Jordan and Weiss do) or `I - D^-1 A` (`randomWalk`, whose eigenvectors are
|
|
361
|
+
* `D^-1/2` times the normalized ones). A node of degree 0 has a zero row. Self-loops are ignored
|
|
362
|
+
* (they cancel in `D - A`) and a directed snapshot is read as undirected, a reciprocal pair
|
|
363
|
+
* summing. A negative, NaN or infinite weight throws. With `k >= nodeCount` every node is its own
|
|
364
|
+
* cluster.
|
|
365
|
+
*
|
|
366
|
+
* The eigenpairs come from subspace iteration with Rayleigh-Ritz, so they are the true smallest
|
|
367
|
+
* ones when `converged` is true; `converged` is false when the iteration stopped at its round cap
|
|
368
|
+
* (a small spectral gap, such as a path of hundreds of nodes), and the eigenpairs are then only
|
|
369
|
+
* approximate. The legacy function uses fixed placeholder eigenvalues for k <= 3 and plain power
|
|
370
|
+
* iteration (which finds the LARGEST eigenvectors) above that, and draws its k-means seeds from
|
|
371
|
+
* `Math.random` unless given a seed. The partitions therefore differ from the legacy function's;
|
|
372
|
+
* one `seed` gives one result. Labels are dense in first-seen order, so `count` is below k when
|
|
373
|
+
* k-means leaves a cluster empty.
|
|
374
|
+
* @param s - Any snapshot
|
|
375
|
+
* @param options - k, the Laplacian, the k-means limits, the seed and the weight override
|
|
376
|
+
* @returns The partition and the eigenpairs it came from
|
|
377
|
+
* @public
|
|
378
|
+
*/
|
|
379
|
+
export function spectralClustering(s, options) {
|
|
380
|
+
const { k } = options;
|
|
381
|
+
const type = options.laplacianType ?? "normalized";
|
|
382
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
383
|
+
const tolerance = options.tolerance ?? 1e-4;
|
|
384
|
+
const seed = options.seed ?? 42;
|
|
385
|
+
if (!Number.isInteger(k) || k < 1) {
|
|
386
|
+
throw new RangeError(`k must be a positive integer, got ${k}`);
|
|
387
|
+
}
|
|
388
|
+
if (!["unnormalized", "normalized", "randomWalk"].includes(type)) {
|
|
389
|
+
throw new RangeError(`unknown laplacianType "${type}"`);
|
|
390
|
+
}
|
|
391
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 0 || !(tolerance >= 0) || !Number.isInteger(seed)) {
|
|
392
|
+
throw new RangeError("maxIterations and seed must be integers, maxIterations and tolerance non-negative");
|
|
393
|
+
}
|
|
394
|
+
const weights = options.weights ?? s.weights;
|
|
395
|
+
if (weights !== null && weights.length !== s.arcCount) {
|
|
396
|
+
throw new RangeError(`weights has ${weights.length} entries; the snapshot has ${s.arcCount} arcs`);
|
|
397
|
+
}
|
|
398
|
+
const n = s.nodeCount;
|
|
399
|
+
if (k >= n) {
|
|
400
|
+
const labels = Uint32Array.from({ length: n }, (_, i) => i);
|
|
401
|
+
return { ...withGroups(labels, n), eigenvalues: new Float64Array(0), eigenvectors: [], converged: true };
|
|
402
|
+
}
|
|
403
|
+
const g = symmetricRows(s, weights);
|
|
404
|
+
const scale = g.degree.map((d) => (d > 0 ? 1 / Math.sqrt(d) : 0));
|
|
405
|
+
const normalised = type !== "unnormalized";
|
|
406
|
+
const apply = (x, out) => {
|
|
407
|
+
for (let u = 0; u < n; u++) {
|
|
408
|
+
let ax = 0;
|
|
409
|
+
for (let a = g.rowPtr[u]; a < g.rowPtr[u + 1]; a++) {
|
|
410
|
+
const v = g.colIdx[a];
|
|
411
|
+
ax += normalised ? g.w[a] * scale[v] * x[v] : g.w[a] * x[v];
|
|
412
|
+
}
|
|
413
|
+
if (normalised) {
|
|
414
|
+
out[u] = g.degree[u] > 0 ? x[u] - scale[u] * ax : 0;
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
out[u] = g.degree[u] * x[u] - ax;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
// The largest eigenvalue is at most 2 for the normalised Laplacian, and at most the largest
|
|
422
|
+
// d(u) + d(v) over the edges for D - A (Anderson and Morley, 1985).
|
|
423
|
+
let shift = 2;
|
|
424
|
+
if (!normalised) {
|
|
425
|
+
shift = 1;
|
|
426
|
+
for (let u = 0; u < n; u++) {
|
|
427
|
+
for (let a = g.rowPtr[u]; a < g.rowPtr[u + 1]; a++) {
|
|
428
|
+
shift = Math.max(shift, g.degree[u] + g.degree[g.colIdx[a]]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const random = mulberry32(seed);
|
|
433
|
+
const eig = smallestEigenpairs(apply, n, k, shift, random);
|
|
434
|
+
if (type === "randomWalk") {
|
|
435
|
+
for (const v of eig.vectors) {
|
|
436
|
+
for (let i = 0; i < n; i++) {
|
|
437
|
+
v[i] *= g.degree[i] > 0 ? scale[i] : 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const points = new Float64Array(n * k);
|
|
442
|
+
for (let c = 0; c < k; c++) {
|
|
443
|
+
for (let i = 0; i < n; i++) {
|
|
444
|
+
points[i * k + c] = eig.vectors[c][i];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (type === "normalized") {
|
|
448
|
+
for (let i = 0; i < n; i++) {
|
|
449
|
+
const row = points.subarray(i * k, i * k + k);
|
|
450
|
+
const norm = Math.hypot(...row);
|
|
451
|
+
if (norm > 0) {
|
|
452
|
+
row.forEach((x, c) => (row[c] = x / norm));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Ten seeded k-means runs, keeping the tightest (the scikit-learn default): one run from an
|
|
457
|
+
// unlucky seeding can split a clean cluster.
|
|
458
|
+
let best = new Uint32Array(n);
|
|
459
|
+
let bestInertia = Infinity;
|
|
460
|
+
for (let run = 0; run < K_MEANS_RUNS; run++) {
|
|
461
|
+
const assign = kMeans(points, n, k, k, maxIterations, tolerance, random);
|
|
462
|
+
const inertia = inertiaOf(points, assign, n, k, k);
|
|
463
|
+
if (inertia < bestInertia) {
|
|
464
|
+
bestInertia = inertia;
|
|
465
|
+
best = assign;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const { labels, count } = renumberPartition(best);
|
|
469
|
+
return {
|
|
470
|
+
...withGroups(labels, count),
|
|
471
|
+
eigenvalues: eig.values,
|
|
472
|
+
eigenvectors: eig.vectors,
|
|
473
|
+
converged: eig.converged,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
//# sourceMappingURL=spectral.js.map
|