@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
package/src/research/grsbm.ts
CHANGED
|
@@ -1,76 +1,27 @@
|
|
|
1
|
-
import type { Graph } from "../core/graph.js";
|
|
2
|
-
import type { NodeId } from "../types/index.js";
|
|
3
|
-
import { SeededRandom } from "../utils/math-utilities.js";
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
2
|
+
* GRSBM (Greedy Recursive Spectral Bisection with Modularity) over a legacy `Graph`. `grsbm`
|
|
3
|
+
* delegates to `indexed.grsbm` over an unweighted snapshot of the graph, counting edges as the old
|
|
4
|
+
* code did, and rebuilds the cluster tree, labels and explanations; the result equals the
|
|
5
|
+
* pre-migration implementation in `grsbm-legacy.ts` exactly. A graph with a self-loop keeps the old
|
|
6
|
+
* code: the port counts a self-loop twice in the degree, the old code once. Unlike the old code the
|
|
7
|
+
* port never replaces `Math.random`.
|
|
7
8
|
*/
|
|
8
|
-
export interface GRSBMConfig {
|
|
9
|
-
/** Maximum depth of recursive bisection */
|
|
10
|
-
maxDepth?: number;
|
|
11
|
-
/** Minimum cluster size to continue splitting */
|
|
12
|
-
minClusterSize?: number;
|
|
13
|
-
/** Number of eigenvectors to use for spectral embedding */
|
|
14
|
-
numEigenvectors?: number;
|
|
15
|
-
/** Convergence tolerance for eigenvalue computation */
|
|
16
|
-
tolerance?: number;
|
|
17
|
-
/** Maximum number of power iterations for eigenvalue computation */
|
|
18
|
-
maxIterations?: number;
|
|
19
|
-
/** Random seed for reproducibility */
|
|
20
|
-
seed?: number;
|
|
21
|
-
}
|
|
22
9
|
|
|
23
|
-
|
|
24
|
-
* Represents a cluster in the hierarchical structure
|
|
25
|
-
*/
|
|
26
|
-
export interface GRSBMCluster {
|
|
27
|
-
/** Unique identifier for this cluster */
|
|
28
|
-
id: string;
|
|
29
|
-
/** Node IDs in this cluster */
|
|
30
|
-
members: Set<NodeId>;
|
|
31
|
-
/** Left child cluster (if split) */
|
|
32
|
-
left?: GRSBMCluster;
|
|
33
|
-
/** Right child cluster (if split) */
|
|
34
|
-
right?: GRSBMCluster;
|
|
35
|
-
/** Modularity score of this cluster */
|
|
36
|
-
modularity: number;
|
|
37
|
-
/** Depth in the hierarchy */
|
|
38
|
-
depth: number;
|
|
39
|
-
/** Spectral embedding quality score */
|
|
40
|
-
spectralScore: number;
|
|
41
|
-
}
|
|
10
|
+
import { INVALID_INDEX } from "@graphty/graph-format";
|
|
42
11
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
modularityScores: number[];
|
|
55
|
-
/** Explanation of the clustering structure */
|
|
56
|
-
explanation: ClusterExplanation[];
|
|
57
|
-
}
|
|
12
|
+
import type { Graph } from "../core/graph.js";
|
|
13
|
+
import { grsbm as indexedGrsbm } from "../indexed/grsbm.js";
|
|
14
|
+
import { toTopologySnapshot } from "../indexed/to-snapshot.js";
|
|
15
|
+
import type { NodeId } from "../types/index.js";
|
|
16
|
+
import {
|
|
17
|
+
type ClusterExplanation,
|
|
18
|
+
grsbm as legacyGrsbm,
|
|
19
|
+
type GRSBMCluster,
|
|
20
|
+
type GRSBMConfig,
|
|
21
|
+
type GRSBMResult,
|
|
22
|
+
} from "./grsbm-legacy.js";
|
|
58
23
|
|
|
59
|
-
|
|
60
|
-
* Explanation for a cluster split decision
|
|
61
|
-
*/
|
|
62
|
-
export interface ClusterExplanation {
|
|
63
|
-
/** Cluster that was split */
|
|
64
|
-
clusterId: string;
|
|
65
|
-
/** Reason for the split */
|
|
66
|
-
reason: string;
|
|
67
|
-
/** Modularity improvement from the split */
|
|
68
|
-
modularityImprovement: number;
|
|
69
|
-
/** Key nodes that influenced the split */
|
|
70
|
-
keyNodes: NodeId[];
|
|
71
|
-
/** Spectral embedding values */
|
|
72
|
-
spectralValues: number[];
|
|
73
|
-
}
|
|
24
|
+
export type { ClusterExplanation, GRSBMCluster, GRSBMConfig, GRSBMResult } from "./grsbm-legacy.js";
|
|
74
25
|
|
|
75
26
|
/**
|
|
76
27
|
* GRSBM - Greedy Recursive Spectral Bisection with Modularity
|
|
@@ -85,502 +36,72 @@ export interface ClusterExplanation {
|
|
|
85
36
|
* @returns Hierarchical clustering result with explanations
|
|
86
37
|
*/
|
|
87
38
|
export function grsbm(graph: Graph, config: GRSBMConfig = {}): GRSBMResult {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
throw new Error("Cannot cluster empty graph");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Initialize root cluster with all nodes
|
|
110
|
-
const rootMembers = new Set(nodes.map((node) => node.id));
|
|
111
|
-
const initialModularity = calculateModularity(graph, new Map([...rootMembers].map((id) => [id, 0])));
|
|
112
|
-
|
|
113
|
-
const root: GRSBMCluster = {
|
|
114
|
-
id: "root",
|
|
115
|
-
members: rootMembers,
|
|
116
|
-
modularity: initialModularity,
|
|
117
|
-
depth: 0,
|
|
118
|
-
spectralScore: 0,
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const explanations: ClusterExplanation[] = [];
|
|
122
|
-
const modularityScores: number[] = [initialModularity];
|
|
123
|
-
|
|
124
|
-
// Perform recursive bisection
|
|
125
|
-
const clusterQueue: GRSBMCluster[] = [root];
|
|
126
|
-
let nextClusterId = 1;
|
|
127
|
-
|
|
128
|
-
while (clusterQueue.length > 0) {
|
|
129
|
-
const currentCluster = clusterQueue.shift();
|
|
130
|
-
if (!currentCluster) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Check stopping criteria
|
|
135
|
-
if (currentCluster.depth >= maxDepth || currentCluster.members.size < minClusterSize * 2) {
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Attempt spectral bisection
|
|
140
|
-
const bisectionResult = spectralBisection(graph, currentCluster, numEigenvectors, tolerance, maxIterations);
|
|
141
|
-
|
|
142
|
-
if (!bisectionResult) {
|
|
143
|
-
continue; // Could not split this cluster
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const { leftMembers, rightMembers, spectralScore, keyNodes, spectralValues, reason } = bisectionResult;
|
|
147
|
-
|
|
148
|
-
// Create child clusters
|
|
149
|
-
const leftCluster: GRSBMCluster = {
|
|
150
|
-
id: `cluster_${String(nextClusterId++)}`,
|
|
151
|
-
members: leftMembers,
|
|
152
|
-
modularity: 0, // Will be calculated below
|
|
153
|
-
depth: currentCluster.depth + 1,
|
|
154
|
-
spectralScore,
|
|
39
|
+
const s = toTopologySnapshot(graph);
|
|
40
|
+
if (s.selfLoopCount > 0) {
|
|
41
|
+
return legacyGrsbm(graph, config);
|
|
42
|
+
}
|
|
43
|
+
// numEigenvectors was never read by the old code either.
|
|
44
|
+
const { maxDepth, minClusterSize, tolerance, maxIterations, seed } = config;
|
|
45
|
+
const r = indexedGrsbm(s, { maxDepth, minClusterSize, tolerance, maxIterations, seed, weighted: false });
|
|
46
|
+
const idOf = (i: number): NodeId => s.ids.idOf(i);
|
|
47
|
+
const name = (serial: number): string => (serial === 0 ? "root" : `cluster_${String(serial)}`);
|
|
48
|
+
|
|
49
|
+
const build = (c: number): GRSBMCluster => {
|
|
50
|
+
const cluster = r.clusters[c];
|
|
51
|
+
const out: GRSBMCluster = {
|
|
52
|
+
id: name(cluster.serial),
|
|
53
|
+
members: new Set(Array.from(cluster.members, idOf)),
|
|
54
|
+
modularity: cluster.modularity,
|
|
55
|
+
depth: cluster.depth,
|
|
56
|
+
spectralScore: cluster.spectralScore,
|
|
155
57
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
members: rightMembers,
|
|
160
|
-
modularity: 0, // Will be calculated below
|
|
161
|
-
depth: currentCluster.depth + 1,
|
|
162
|
-
spectralScore,
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Calculate modularity improvement
|
|
166
|
-
const newAssignment = new Map<NodeId, number>();
|
|
167
|
-
for (const nodeId of leftMembers) {
|
|
168
|
-
newAssignment.set(nodeId, 0);
|
|
169
|
-
}
|
|
170
|
-
for (const nodeId of rightMembers) {
|
|
171
|
-
newAssignment.set(nodeId, 1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const newModularity = calculateModularity(graph, newAssignment);
|
|
175
|
-
const modularityImprovement = newModularity - currentCluster.modularity;
|
|
176
|
-
|
|
177
|
-
// Only split if modularity improves or is neutral (allow small splits)
|
|
178
|
-
if (modularityImprovement >= -0.01) {
|
|
179
|
-
// Small tolerance for neutral splits
|
|
180
|
-
leftCluster.modularity = newModularity;
|
|
181
|
-
rightCluster.modularity = newModularity;
|
|
182
|
-
|
|
183
|
-
currentCluster.left = leftCluster;
|
|
184
|
-
currentCluster.right = rightCluster;
|
|
185
|
-
|
|
186
|
-
// Add explanation
|
|
187
|
-
explanations.push({
|
|
188
|
-
clusterId: currentCluster.id,
|
|
189
|
-
reason,
|
|
190
|
-
modularityImprovement,
|
|
191
|
-
keyNodes,
|
|
192
|
-
spectralValues,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
modularityScores.push(newModularity);
|
|
196
|
-
|
|
197
|
-
// Add children to queue for further processing
|
|
198
|
-
clusterQueue.push(leftCluster, rightCluster);
|
|
58
|
+
if (cluster.left !== INVALID_INDEX) {
|
|
59
|
+
out.left = build(cluster.left);
|
|
60
|
+
out.right = build(cluster.right);
|
|
199
61
|
}
|
|
200
|
-
|
|
62
|
+
return out;
|
|
63
|
+
};
|
|
201
64
|
|
|
202
|
-
//
|
|
65
|
+
// The old labels number the leaves left to right, an emptied leaf included.
|
|
203
66
|
const clusters = new Map<NodeId, number>();
|
|
204
|
-
let
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
clusters.set(nodeId, clusterId);
|
|
67
|
+
let leaves = 0;
|
|
68
|
+
const walk = (c: number): void => {
|
|
69
|
+
const cluster = r.clusters[c];
|
|
70
|
+
if (cluster.left === INVALID_INDEX) {
|
|
71
|
+
for (const i of cluster.members) {
|
|
72
|
+
clusters.set(idOf(i), leaves);
|
|
211
73
|
}
|
|
212
|
-
|
|
74
|
+
leaves++;
|
|
213
75
|
} else {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
assignClusterIds(cluster.left);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (cluster.right) {
|
|
220
|
-
assignClusterIds(cluster.right);
|
|
221
|
-
}
|
|
76
|
+
walk(cluster.left);
|
|
77
|
+
walk(cluster.right);
|
|
222
78
|
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
assignClusterIds(root);
|
|
226
|
-
|
|
227
|
-
// Restore original random function
|
|
228
|
-
Math.random = originalRandom;
|
|
229
|
-
|
|
230
|
-
return {
|
|
231
|
-
root,
|
|
232
|
-
clusters,
|
|
233
|
-
numClusters: clusterId,
|
|
234
|
-
modularityScores,
|
|
235
|
-
explanation: explanations,
|
|
236
79
|
};
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
rightMembers: Set<NodeId>;
|
|
257
|
-
spectralScore: number;
|
|
258
|
-
keyNodes: NodeId[];
|
|
259
|
-
spectralValues: number[];
|
|
260
|
-
reason: string;
|
|
261
|
-
} | null {
|
|
262
|
-
const members = Array.from(cluster.members);
|
|
263
|
-
const n = members.length;
|
|
264
|
-
|
|
265
|
-
if (n < 4) {
|
|
266
|
-
return null; // Too small to split meaningfully
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Create subgraph Laplacian matrix
|
|
270
|
-
const laplacian = createLaplacianMatrix(graph, members);
|
|
271
|
-
|
|
272
|
-
// Compute the second smallest eigenvector (Fiedler vector)
|
|
273
|
-
const eigenvector = computeFiedlerVector(laplacian, tolerance, maxIterations);
|
|
274
|
-
|
|
275
|
-
if (!eigenvector) {
|
|
276
|
-
return null; // Could not compute eigenvector
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Find optimal split point
|
|
280
|
-
const sortedIndices = eigenvector.map((value, index) => ({ value, index })).sort((a, b) => a.value - b.value);
|
|
281
|
-
|
|
282
|
-
let bestSplitIndex = Math.floor(n / 2);
|
|
283
|
-
let bestModularity = -Infinity;
|
|
284
|
-
|
|
285
|
-
// Try different split points
|
|
286
|
-
for (let splitIndex = Math.floor(n * 0.2); splitIndex <= Math.floor(n * 0.8); splitIndex++) {
|
|
287
|
-
const leftIndices = sortedIndices.slice(0, splitIndex).map((item) => item.index);
|
|
288
|
-
const rightIndices = sortedIndices.slice(splitIndex).map((item) => item.index);
|
|
289
|
-
|
|
290
|
-
const assignment = new Map<NodeId, number>();
|
|
291
|
-
for (const idx of leftIndices) {
|
|
292
|
-
const nodeId = members[idx];
|
|
293
|
-
if (nodeId !== undefined) {
|
|
294
|
-
assignment.set(nodeId, 0);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
for (const idx of rightIndices) {
|
|
298
|
-
const nodeId = members[idx];
|
|
299
|
-
if (nodeId !== undefined) {
|
|
300
|
-
assignment.set(nodeId, 1);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const modularity = calculateModularity(graph, assignment);
|
|
305
|
-
|
|
306
|
-
if (modularity > bestModularity) {
|
|
307
|
-
bestModularity = modularity;
|
|
308
|
-
bestSplitIndex = splitIndex;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Create final split
|
|
313
|
-
const leftIndices = sortedIndices.slice(0, bestSplitIndex).map((item) => item.index);
|
|
314
|
-
const rightIndices = sortedIndices.slice(bestSplitIndex).map((item) => item.index);
|
|
315
|
-
|
|
316
|
-
const leftMembers = new Set<NodeId>();
|
|
317
|
-
const rightMembers = new Set<NodeId>();
|
|
318
|
-
|
|
319
|
-
for (const idx of leftIndices) {
|
|
320
|
-
const nodeId = members[idx];
|
|
321
|
-
if (nodeId !== undefined) {
|
|
322
|
-
leftMembers.add(nodeId);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
for (const idx of rightIndices) {
|
|
327
|
-
const nodeId = members[idx];
|
|
328
|
-
if (nodeId !== undefined) {
|
|
329
|
-
rightMembers.add(nodeId);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Identify key nodes (those with extreme spectral values)
|
|
334
|
-
const extremeThreshold = 0.1;
|
|
335
|
-
const sortedValues = [...eigenvector].sort((a, b) => Math.abs(b) - Math.abs(a));
|
|
336
|
-
const threshold = sortedValues[Math.floor(sortedValues.length * extremeThreshold)] ?? 0;
|
|
337
|
-
|
|
338
|
-
const keyNodes: NodeId[] = [];
|
|
339
|
-
for (let i = 0; i < eigenvector.length; i++) {
|
|
340
|
-
const eigenValue = eigenvector[i];
|
|
341
|
-
if (eigenValue !== undefined && Math.abs(eigenValue) >= Math.abs(threshold)) {
|
|
342
|
-
const nodeId = members[i];
|
|
343
|
-
if (nodeId !== undefined) {
|
|
344
|
-
keyNodes.push(nodeId);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Calculate spectral score (based on eigenvalue gap)
|
|
350
|
-
const firstValue = sortedValues[0] ?? 0;
|
|
351
|
-
const lastValue = sortedValues[sortedValues.length - 1] ?? 0;
|
|
352
|
-
const spectralScore = Math.abs(firstValue - lastValue);
|
|
353
|
-
|
|
354
|
-
// Generate explanation
|
|
355
|
-
const reason = `Spectral bisection based on Fiedler vector with modularity ${bestModularity.toFixed(3)}. Split creates clusters of sizes ${String(leftMembers.size)} and ${String(rightMembers.size)}.`;
|
|
80
|
+
walk(0);
|
|
81
|
+
|
|
82
|
+
const explanation: ClusterExplanation[] = r.clusters.flatMap((cluster) => {
|
|
83
|
+
const { split } = cluster;
|
|
84
|
+
if (split === null) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const left = r.clusters[cluster.left];
|
|
88
|
+
const right = r.clusters[cluster.right];
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
clusterId: name(cluster.serial),
|
|
92
|
+
reason: `Spectral bisection based on Fiedler vector with modularity ${split.bisectionModularity.toFixed(3)}. Split creates clusters of sizes ${String(left.members.length)} and ${String(right.members.length)}.`,
|
|
93
|
+
modularityImprovement: split.improvement,
|
|
94
|
+
keyNodes: Array.from(split.keyNodes, idOf),
|
|
95
|
+
spectralValues: Array.from(split.spectralValues),
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
});
|
|
356
99
|
|
|
357
100
|
return {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
reason,
|
|
101
|
+
root: build(0),
|
|
102
|
+
clusters,
|
|
103
|
+
numClusters: leaves,
|
|
104
|
+
modularityScores: Array.from(r.modularityScores),
|
|
105
|
+
explanation,
|
|
364
106
|
};
|
|
365
107
|
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Create Laplacian matrix for a subgraph
|
|
369
|
-
* @param graph - The input graph
|
|
370
|
-
* @param nodes - The nodes to include in the subgraph
|
|
371
|
-
* @returns The Laplacian matrix as a 2D array
|
|
372
|
-
*/
|
|
373
|
-
function createLaplacianMatrix(graph: Graph, nodes: NodeId[]): number[][] {
|
|
374
|
-
const n = nodes.length;
|
|
375
|
-
const nodeIndex = new Map<NodeId, number>();
|
|
376
|
-
|
|
377
|
-
// Create node index mapping
|
|
378
|
-
for (let i = 0; i < n; i++) {
|
|
379
|
-
const node = nodes[i];
|
|
380
|
-
if (node !== undefined) {
|
|
381
|
-
nodeIndex.set(node, i);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Initialize Laplacian matrix
|
|
386
|
-
const laplacian = Array.from({ length: n }, () => new Array<number>(n).fill(0));
|
|
387
|
-
|
|
388
|
-
// Fill adjacency part and calculate degrees
|
|
389
|
-
const degrees = new Array<number>(n).fill(0);
|
|
390
|
-
|
|
391
|
-
for (let i = 0; i < n; i++) {
|
|
392
|
-
const nodeId = nodes[i];
|
|
393
|
-
if (nodeId === undefined) {
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
398
|
-
const neighborIdx = nodeIndex.get(neighbor);
|
|
399
|
-
if (neighborIdx !== undefined && i < laplacian.length && i < degrees.length) {
|
|
400
|
-
const row = laplacian[i];
|
|
401
|
-
if (row) {
|
|
402
|
-
row[neighborIdx] = -1;
|
|
403
|
-
degrees[i] = (degrees[i] ?? 0) + 1;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Set diagonal (degree) values
|
|
410
|
-
for (let i = 0; i < n; i++) {
|
|
411
|
-
const row = laplacian[i];
|
|
412
|
-
const degree = degrees[i];
|
|
413
|
-
if (row && degree !== undefined) {
|
|
414
|
-
row[i] = degree;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return laplacian;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Compute the Fiedler vector (second smallest eigenvector) using power iteration
|
|
423
|
-
* @param laplacian - The Laplacian matrix
|
|
424
|
-
* @param tolerance - Convergence tolerance
|
|
425
|
-
* @param maxIterations - Maximum number of iterations
|
|
426
|
-
* @returns The Fiedler vector, or null if computation failed
|
|
427
|
-
*/
|
|
428
|
-
function computeFiedlerVector(laplacian: number[][], tolerance: number, maxIterations: number): number[] | null {
|
|
429
|
-
const n = laplacian.length;
|
|
430
|
-
if (n < 2) {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Initialize random vector orthogonal to the all-ones vector
|
|
435
|
-
let vector = new Array<number>(n).fill(0);
|
|
436
|
-
for (let i = 0; i < n; i++) {
|
|
437
|
-
vector[i] = Math.random() - 0.5;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Make orthogonal to all-ones vector
|
|
441
|
-
const mean = vector.reduce((sum: number, val: number) => sum + val, 0) / n;
|
|
442
|
-
for (let i = 0; i < n; i++) {
|
|
443
|
-
const val = vector[i];
|
|
444
|
-
if (val !== undefined) {
|
|
445
|
-
vector[i] = val - mean;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Normalize
|
|
450
|
-
const norm = Math.sqrt(vector.reduce((sum: number, val: number) => sum + val * val, 0));
|
|
451
|
-
if (norm > 0) {
|
|
452
|
-
for (let i = 0; i < n; i++) {
|
|
453
|
-
const val = vector[i];
|
|
454
|
-
if (val !== undefined) {
|
|
455
|
-
vector[i] = val / norm;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Power iteration with deflation for second smallest eigenvalue
|
|
461
|
-
for (let iter = 0; iter < maxIterations; iter++) {
|
|
462
|
-
const newVector = new Array<number>(n).fill(0);
|
|
463
|
-
|
|
464
|
-
// Matrix-vector multiplication: newVector = L * vector
|
|
465
|
-
for (let i = 0; i < n; i++) {
|
|
466
|
-
let sum = 0;
|
|
467
|
-
const row = laplacian[i];
|
|
468
|
-
if (row) {
|
|
469
|
-
for (let j = 0; j < n; j++) {
|
|
470
|
-
const matrixVal = row[j];
|
|
471
|
-
const vectorVal = vector[j];
|
|
472
|
-
if (matrixVal !== undefined && vectorVal !== undefined) {
|
|
473
|
-
sum += matrixVal * vectorVal;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
newVector[i] = sum;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// For smallest eigenvalue, we actually want inverse iteration
|
|
482
|
-
// This is simplified - in practice would solve (L - shift*I)x = vector
|
|
483
|
-
// For now, use power iteration on -L (approximately)
|
|
484
|
-
for (let i = 0; i < n; i++) {
|
|
485
|
-
const val = newVector[i];
|
|
486
|
-
if (val !== undefined) {
|
|
487
|
-
newVector[i] = -val;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Orthogonalize against all-ones vector
|
|
492
|
-
const newMean = newVector.reduce((sum, val) => sum + val, 0) / n;
|
|
493
|
-
for (let i = 0; i < n; i++) {
|
|
494
|
-
const val = newVector[i];
|
|
495
|
-
if (val !== undefined) {
|
|
496
|
-
newVector[i] = val - newMean;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Normalize
|
|
501
|
-
const newNorm = Math.sqrt(newVector.reduce((sum, val) => sum + val * val, 0));
|
|
502
|
-
if (newNorm < tolerance) {
|
|
503
|
-
break; // Converged to zero vector
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
for (let i = 0; i < n; i++) {
|
|
507
|
-
const val = newVector[i];
|
|
508
|
-
if (val !== undefined) {
|
|
509
|
-
newVector[i] = val / newNorm;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Check convergence
|
|
514
|
-
let diff = 0;
|
|
515
|
-
for (let i = 0; i < n; i++) {
|
|
516
|
-
const newVal = newVector[i];
|
|
517
|
-
const oldVal = vector[i];
|
|
518
|
-
if (newVal !== undefined && oldVal !== undefined) {
|
|
519
|
-
diff += Math.abs(newVal - oldVal);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
vector = newVector;
|
|
524
|
-
|
|
525
|
-
if (diff < tolerance) {
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
return vector;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Calculate modularity of a graph partitioning
|
|
535
|
-
* @param graph - The input graph
|
|
536
|
-
* @param assignment - Map from node ID to community ID
|
|
537
|
-
* @returns The modularity score
|
|
538
|
-
*/
|
|
539
|
-
function calculateModularity(graph: Graph, assignment: Map<NodeId, number>): number {
|
|
540
|
-
const totalEdges = graph.uniqueEdgeCount;
|
|
541
|
-
if (totalEdges === 0) {
|
|
542
|
-
return 0;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
let modularity = 0;
|
|
546
|
-
const communities = new Map<number, Set<NodeId>>();
|
|
547
|
-
|
|
548
|
-
// Group nodes by community
|
|
549
|
-
for (const [nodeId, communityId] of assignment) {
|
|
550
|
-
if (!communities.has(communityId)) {
|
|
551
|
-
communities.set(communityId, new Set());
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const community = communities.get(communityId);
|
|
555
|
-
if (community) {
|
|
556
|
-
community.add(nodeId);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Calculate modularity for each community
|
|
561
|
-
for (const [, members] of communities) {
|
|
562
|
-
let internalEdges = 0;
|
|
563
|
-
let totalDegree = 0;
|
|
564
|
-
|
|
565
|
-
for (const nodeId of members) {
|
|
566
|
-
const nodeDegree = graph.degree(nodeId);
|
|
567
|
-
totalDegree += nodeDegree;
|
|
568
|
-
|
|
569
|
-
// Count internal edges
|
|
570
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
571
|
-
if (members.has(neighbor)) {
|
|
572
|
-
internalEdges++;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Each edge is counted twice in undirected graphs
|
|
578
|
-
internalEdges /= 2;
|
|
579
|
-
|
|
580
|
-
// Modularity contribution for this community
|
|
581
|
-
const expectedEdges = (totalDegree * totalDegree) / (4 * totalEdges);
|
|
582
|
-
modularity += (internalEdges - expectedEdges) / totalEdges;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
return modularity;
|
|
586
|
-
}
|