@graphty/algorithms 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -50
- package/dist/algorithms.js +15260 -10390
- package/dist/algorithms.js.map +1 -1
- package/dist/algorithms.standalone.js +26675 -21542
- package/dist/algorithms.standalone.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +18 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +40 -1
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +20 -0
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +93 -8
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.d.ts +9 -0
- package/dist/src/algorithms/centrality/degree.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +18 -0
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +10 -0
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.js +29 -0
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
- package/dist/src/algorithms/centrality/hits.d.ts +9 -0
- package/dist/src/algorithms/centrality/hits.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/hits.js +23 -0
- package/dist/src/algorithms/centrality/hits.js.map +1 -1
- package/dist/src/algorithms/centrality/katz.d.ts +9 -0
- package/dist/src/algorithms/centrality/katz.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/katz.js +31 -0
- package/dist/src/algorithms/centrality/katz.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +13 -13
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +47 -10
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman-legacy.d.ts +27 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.d.ts.map +1 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.js +335 -0
- package/dist/src/algorithms/community/girvan-newman-legacy.js.map +1 -0
- package/dist/src/algorithms/community/girvan-newman.d.ts +5 -3
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +34 -308
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +9 -6
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +24 -110
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/components/connected.d.ts.map +1 -1
- package/dist/src/algorithms/components/connected.js +28 -77
- package/dist/src/algorithms/components/connected.js.map +1 -1
- package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -1
- package/dist/src/algorithms/mst/kruskal.js +59 -3
- package/dist/src/algorithms/mst/kruskal.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +46 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +27 -0
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +15 -5
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.js +97 -107
- package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +2 -12
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.js +47 -308
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs-variants.js +5 -25
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs.d.ts +0 -3
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +0 -4
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/dfs.js +33 -154
- package/dist/src/algorithms/traversal/dfs.js.map +1 -1
- package/dist/src/clustering/hierarchical-legacy.d.ts +56 -0
- package/dist/src/clustering/hierarchical-legacy.d.ts.map +1 -0
- package/dist/src/clustering/hierarchical-legacy.js +441 -0
- package/dist/src/clustering/hierarchical-legacy.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +7 -39
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +56 -417
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/k-core-legacy.d.ts +30 -0
- package/dist/src/clustering/k-core-legacy.d.ts.map +1 -0
- package/dist/src/clustering/k-core-legacy.js +191 -0
- package/dist/src/clustering/k-core-legacy.js.map +1 -0
- package/dist/src/clustering/k-core.d.ts +8 -7
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +33 -174
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/mcl-legacy.d.ts +42 -0
- package/dist/src/clustering/mcl-legacy.d.ts.map +1 -0
- package/dist/src/clustering/mcl-legacy.js +386 -0
- package/dist/src/clustering/mcl-legacy.js.map +1 -0
- package/dist/src/clustering/mcl.d.ts +9 -32
- package/dist/src/clustering/mcl.d.ts.map +1 -1
- package/dist/src/clustering/mcl.js +48 -374
- package/dist/src/clustering/mcl.js.map +1 -1
- package/dist/src/index.d.ts +29 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/indexed/accelerator.d.ts +141 -11
- package/dist/src/indexed/accelerator.d.ts.map +1 -1
- package/dist/src/indexed/accelerator.js +319 -5
- package/dist/src/indexed/accelerator.js.map +1 -1
- package/dist/src/indexed/all-pairs.d.ts +59 -0
- package/dist/src/indexed/all-pairs.d.ts.map +1 -0
- package/dist/src/indexed/all-pairs.js +274 -0
- package/dist/src/indexed/all-pairs.js.map +1 -0
- package/dist/src/indexed/bellman-ford.d.ts +25 -0
- package/dist/src/indexed/bellman-ford.d.ts.map +1 -0
- package/dist/src/indexed/bellman-ford.js +59 -0
- package/dist/src/indexed/bellman-ford.js.map +1 -0
- package/dist/src/indexed/betweenness.d.ts +85 -0
- package/dist/src/indexed/betweenness.d.ts.map +1 -0
- package/dist/src/indexed/betweenness.js +188 -0
- package/dist/src/indexed/betweenness.js.map +1 -0
- package/dist/src/indexed/bfs.d.ts +69 -2
- package/dist/src/indexed/bfs.d.ts.map +1 -1
- package/dist/src/indexed/bfs.js +148 -1
- package/dist/src/indexed/bfs.js.map +1 -1
- package/dist/src/indexed/bipartite.d.ts +37 -0
- package/dist/src/indexed/bipartite.d.ts.map +1 -0
- package/dist/src/indexed/bipartite.js +54 -0
- package/dist/src/indexed/bipartite.js.map +1 -0
- package/dist/src/indexed/closeness.d.ts +49 -0
- package/dist/src/indexed/closeness.d.ts.map +1 -0
- package/dist/src/indexed/closeness.js +127 -0
- package/dist/src/indexed/closeness.js.map +1 -0
- package/dist/src/indexed/common-neighbors.d.ts +14 -1
- package/dist/src/indexed/common-neighbors.d.ts.map +1 -1
- package/dist/src/indexed/common-neighbors.js +24 -14
- package/dist/src/indexed/common-neighbors.js.map +1 -1
- package/dist/src/indexed/degree.d.ts +19 -0
- package/dist/src/indexed/degree.d.ts.map +1 -0
- package/dist/src/indexed/degree.js +45 -0
- package/dist/src/indexed/degree.js.map +1 -0
- package/dist/src/indexed/delta-pagerank.d.ts +145 -0
- package/dist/src/indexed/delta-pagerank.d.ts.map +1 -0
- package/dist/src/indexed/delta-pagerank.js +428 -0
- package/dist/src/indexed/delta-pagerank.js.map +1 -0
- package/dist/src/indexed/dfs.d.ts +55 -0
- package/dist/src/indexed/dfs.d.ts.map +1 -0
- package/dist/src/indexed/dfs.js +146 -0
- package/dist/src/indexed/dfs.js.map +1 -0
- package/dist/src/indexed/eigenvector.d.ts +48 -0
- package/dist/src/indexed/eigenvector.d.ts.map +1 -0
- package/dist/src/indexed/eigenvector.js +162 -0
- package/dist/src/indexed/eigenvector.js.map +1 -0
- package/dist/src/indexed/facade.d.ts +109 -0
- package/dist/src/indexed/facade.d.ts.map +1 -0
- package/dist/src/indexed/facade.js +204 -0
- package/dist/src/indexed/facade.js.map +1 -0
- package/dist/src/indexed/flow.d.ts +120 -0
- package/dist/src/indexed/flow.d.ts.map +1 -0
- package/dist/src/indexed/flow.js +377 -0
- package/dist/src/indexed/flow.js.map +1 -0
- package/dist/src/indexed/girvan-newman.d.ts +42 -0
- package/dist/src/indexed/girvan-newman.d.ts.map +1 -0
- package/dist/src/indexed/girvan-newman.js +90 -0
- package/dist/src/indexed/girvan-newman.js.map +1 -0
- package/dist/src/indexed/grsbm.d.ts +85 -0
- package/dist/src/indexed/grsbm.d.ts.map +1 -0
- package/dist/src/indexed/grsbm.js +287 -0
- package/dist/src/indexed/grsbm.js.map +1 -0
- package/dist/src/indexed/hierarchical.d.ts +69 -0
- package/dist/src/indexed/hierarchical.d.ts.map +1 -0
- package/dist/src/indexed/hierarchical.js +245 -0
- package/dist/src/indexed/hierarchical.js.map +1 -0
- package/dist/src/indexed/index.d.ts +29 -3
- package/dist/src/indexed/index.d.ts.map +1 -1
- package/dist/src/indexed/index.js +29 -3
- package/dist/src/indexed/index.js.map +1 -1
- package/dist/src/indexed/isomorphism.d.ts +42 -0
- package/dist/src/indexed/isomorphism.d.ts.map +1 -0
- package/dist/src/indexed/isomorphism.js +269 -0
- package/dist/src/indexed/isomorphism.js.map +1 -0
- package/dist/src/indexed/label-propagation.d.ts +134 -0
- package/dist/src/indexed/label-propagation.d.ts.map +1 -0
- package/dist/src/indexed/label-propagation.js +486 -0
- package/dist/src/indexed/label-propagation.js.map +1 -0
- package/dist/src/indexed/leiden.d.ts +52 -0
- package/dist/src/indexed/leiden.d.ts.map +1 -0
- package/dist/src/indexed/leiden.js +384 -0
- package/dist/src/indexed/leiden.js.map +1 -0
- package/dist/src/indexed/link-prediction.d.ts +147 -0
- package/dist/src/indexed/link-prediction.d.ts.map +1 -0
- package/dist/src/indexed/link-prediction.js +317 -0
- package/dist/src/indexed/link-prediction.js.map +1 -0
- package/dist/src/indexed/markov.d.ts +54 -0
- package/dist/src/indexed/markov.d.ts.map +1 -0
- package/dist/src/indexed/markov.js +284 -0
- package/dist/src/indexed/markov.js.map +1 -0
- package/dist/src/indexed/matching.d.ts +53 -0
- package/dist/src/indexed/matching.d.ts.map +1 -0
- package/dist/src/indexed/matching.js +139 -0
- package/dist/src/indexed/matching.js.map +1 -0
- package/dist/src/indexed/min-cut.d.ts +48 -0
- package/dist/src/indexed/min-cut.d.ts.map +1 -0
- package/dist/src/indexed/min-cut.js +175 -0
- package/dist/src/indexed/min-cut.js.map +1 -0
- package/dist/src/indexed/modularity.d.ts +34 -0
- package/dist/src/indexed/modularity.d.ts.map +1 -0
- package/dist/src/indexed/modularity.js +74 -0
- package/dist/src/indexed/modularity.js.map +1 -0
- package/dist/src/indexed/mst.d.ts +29 -1
- package/dist/src/indexed/mst.d.ts.map +1 -1
- package/dist/src/indexed/mst.js +62 -0
- package/dist/src/indexed/mst.js.map +1 -1
- package/dist/src/indexed/pagerank.d.ts +29 -6
- package/dist/src/indexed/pagerank.d.ts.map +1 -1
- package/dist/src/indexed/pagerank.js +107 -11
- package/dist/src/indexed/pagerank.js.map +1 -1
- package/dist/src/indexed/point-to-point.d.ts +53 -0
- package/dist/src/indexed/point-to-point.d.ts.map +1 -0
- package/dist/src/indexed/point-to-point.js +173 -0
- package/dist/src/indexed/point-to-point.js.map +1 -0
- package/dist/src/indexed/scc.d.ts +53 -0
- package/dist/src/indexed/scc.d.ts.map +1 -0
- package/dist/src/indexed/scc.js +110 -0
- package/dist/src/indexed/scc.js.map +1 -0
- package/dist/src/indexed/spectral.d.ts +61 -0
- package/dist/src/indexed/spectral.d.ts.map +1 -0
- package/dist/src/indexed/spectral.js +476 -0
- package/dist/src/indexed/spectral.js.map +1 -0
- package/dist/src/indexed/structures/bit-set.d.ts +45 -0
- package/dist/src/indexed/structures/bit-set.d.ts.map +1 -0
- package/dist/src/indexed/structures/bit-set.js +57 -0
- package/dist/src/indexed/structures/bit-set.js.map +1 -0
- package/dist/src/indexed/structures/max-heap.d.ts +52 -0
- package/dist/src/indexed/structures/max-heap.d.ts.map +1 -0
- package/dist/src/indexed/structures/max-heap.js +66 -0
- package/dist/src/indexed/structures/max-heap.js.map +1 -0
- package/dist/src/indexed/structures/min-heap.d.ts +30 -1
- package/dist/src/indexed/structures/min-heap.d.ts.map +1 -1
- package/dist/src/indexed/structures/min-heap.js +46 -6
- package/dist/src/indexed/structures/min-heap.js.map +1 -1
- package/dist/src/indexed/structures/ring-queue.d.ts +41 -0
- package/dist/src/indexed/structures/ring-queue.d.ts.map +1 -0
- package/dist/src/indexed/structures/ring-queue.js +64 -0
- package/dist/src/indexed/structures/ring-queue.js.map +1 -0
- package/dist/src/indexed/sync.d.ts +50 -0
- package/dist/src/indexed/sync.d.ts.map +1 -0
- package/dist/src/indexed/sync.js +175 -0
- package/dist/src/indexed/sync.js.map +1 -0
- package/dist/src/indexed/terahac.d.ts +58 -0
- package/dist/src/indexed/terahac.d.ts.map +1 -0
- package/dist/src/indexed/terahac.js +244 -0
- package/dist/src/indexed/terahac.js.map +1 -0
- package/dist/src/indexed/to-snapshot.d.ts +45 -2
- package/dist/src/indexed/to-snapshot.d.ts.map +1 -1
- package/dist/src/indexed/to-snapshot.js +140 -6
- package/dist/src/indexed/to-snapshot.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors-legacy.d.ts +71 -0
- package/dist/src/link-prediction/common-neighbors-legacy.d.ts.map +1 -0
- package/dist/src/link-prediction/common-neighbors-legacy.js +172 -0
- package/dist/src/link-prediction/common-neighbors-legacy.js.map +1 -0
- package/dist/src/link-prediction/common-neighbors.d.ts +11 -19
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +56 -123
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/optimized/direction-optimized-bfs.d.ts +6 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -1
- package/dist/src/optimized/direction-optimized-bfs.js +6 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -1
- package/dist/src/research/grsbm-legacy.d.ts +82 -0
- package/dist/src/research/grsbm-legacy.d.ts.map +1 -0
- package/dist/src/research/grsbm-legacy.js +416 -0
- package/dist/src/research/grsbm-legacy.js.map +1 -0
- package/dist/src/research/grsbm.d.ts +9 -66
- package/dist/src/research/grsbm.d.ts.map +1 -1
- package/dist/src/research/grsbm.js +67 -391
- package/dist/src/research/grsbm.js.map +1 -1
- package/dist/src/research/sync-legacy.d.ts +48 -0
- package/dist/src/research/sync-legacy.d.ts.map +1 -0
- package/dist/src/research/sync-legacy.js +337 -0
- package/dist/src/research/sync-legacy.js.map +1 -0
- package/dist/src/research/sync.d.ts +10 -32
- package/dist/src/research/sync.d.ts.map +1 -1
- package/dist/src/research/sync.js +28 -316
- package/dist/src/research/sync.js.map +1 -1
- package/package.json +4 -3
- package/src/algorithms/centrality/betweenness.ts +51 -1
- package/src/algorithms/centrality/closeness.ts +112 -12
- package/src/algorithms/centrality/degree.ts +19 -0
- package/src/algorithms/centrality/eigenvector.ts +34 -0
- package/src/algorithms/centrality/hits.ts +24 -0
- package/src/algorithms/centrality/katz.ts +32 -0
- package/src/algorithms/centrality/pagerank.ts +53 -13
- package/src/algorithms/community/girvan-newman-legacy.ts +414 -0
- package/src/algorithms/community/girvan-newman.ts +36 -386
- package/src/algorithms/community/label-propagation.ts +24 -133
- package/src/algorithms/components/connected.ts +28 -95
- package/src/algorithms/mst/kruskal.ts +65 -4
- package/src/algorithms/shortest-path/bellman-ford.ts +49 -0
- package/src/algorithms/shortest-path/dijkstra.ts +29 -0
- package/src/algorithms/shortest-path/floyd-warshall.ts +106 -136
- package/src/algorithms/traversal/bfs-unified.ts +52 -366
- package/src/algorithms/traversal/bfs-variants.ts +5 -28
- package/src/algorithms/traversal/bfs.ts +0 -4
- package/src/algorithms/traversal/dfs.ts +39 -182
- package/src/clustering/hierarchical-legacy.ts +551 -0
- package/src/clustering/hierarchical.ts +64 -520
- package/src/clustering/k-core-legacy.ts +229 -0
- package/src/clustering/k-core.ts +38 -209
- package/src/clustering/mcl-legacy.ts +498 -0
- package/src/clustering/mcl.ts +46 -479
- package/src/index.ts +90 -3
- package/src/indexed/accelerator.ts +502 -16
- package/src/indexed/all-pairs.ts +342 -0
- package/src/indexed/bellman-ford.ts +72 -0
- package/src/indexed/betweenness.ts +258 -0
- package/src/indexed/bfs.ts +189 -3
- package/src/indexed/bipartite.ts +77 -0
- package/src/indexed/closeness.ts +160 -0
- package/src/indexed/common-neighbors.ts +31 -14
- package/src/indexed/degree.ts +55 -0
- package/src/indexed/delta-pagerank.ts +513 -0
- package/src/indexed/dfs.ts +189 -0
- package/src/indexed/eigenvector.ts +202 -0
- package/src/indexed/facade.ts +229 -0
- package/src/indexed/flow.ts +500 -0
- package/src/indexed/girvan-newman.ts +120 -0
- package/src/indexed/grsbm.ts +390 -0
- package/src/indexed/hierarchical.ts +295 -0
- package/src/indexed/index.ts +94 -3
- package/src/indexed/isomorphism.ts +305 -0
- package/src/indexed/label-propagation.ts +582 -0
- package/src/indexed/leiden.ts +436 -0
- package/src/indexed/link-prediction.ts +399 -0
- package/src/indexed/markov.ts +324 -0
- package/src/indexed/matching.ts +192 -0
- package/src/indexed/min-cut.ts +201 -0
- package/src/indexed/modularity.ts +81 -0
- package/src/indexed/mst.ts +79 -1
- package/src/indexed/pagerank.ts +128 -15
- package/src/indexed/point-to-point.ts +220 -0
- package/src/indexed/scc.ts +132 -0
- package/src/indexed/spectral.ts +550 -0
- package/src/indexed/structures/bit-set.ts +65 -0
- package/src/indexed/structures/max-heap.ts +74 -0
- package/src/indexed/structures/min-heap.ts +51 -6
- package/src/indexed/structures/ring-queue.ts +70 -0
- package/src/indexed/sync.ts +229 -0
- package/src/indexed/terahac.ts +290 -0
- package/src/indexed/to-snapshot.ts +156 -6
- package/src/link-prediction/common-neighbors-legacy.ts +252 -0
- package/src/link-prediction/common-neighbors.ts +65 -168
- package/src/optimized/direction-optimized-bfs.ts +6 -1
- package/src/research/grsbm-legacy.ts +586 -0
- package/src/research/grsbm.ts +76 -555
- package/src/research/sync-legacy.ts +456 -0
- package/src/research/sync.ts +30 -431
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { Graph } from "../../core/graph.js";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import type { Graph } from "../../core/graph.js";
|
|
2
|
+
import { labelsToGroups } from "../../indexed/facade.js";
|
|
3
|
+
import { girvanNewman as indexedGirvanNewman } from "../../indexed/girvan-newman.js";
|
|
4
|
+
import { needsLegacyCode, toSnapshot } from "../../indexed/to-snapshot.js";
|
|
5
|
+
import type { CommunityResult, GirvanNewmanOptions } from "../../types/index.js";
|
|
6
|
+
import { girvanNewman as legacyGirvanNewman } from "./girvan-newman-legacy.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Girvan-Newman community detection algorithm
|
|
@@ -10,399 +13,46 @@ import { connectedComponents } from "../components/connected.js";
|
|
|
10
13
|
* splits into disconnected components.
|
|
11
14
|
*
|
|
12
15
|
* This is a divisive hierarchical clustering algorithm that produces a
|
|
13
|
-
* dendrogram of community structures.
|
|
16
|
+
* dendrogram of community structures. It delegates to `indexed.girvanNewman`; the result equals
|
|
17
|
+
* the pre-migration implementation in `girvan-newman-legacy.ts`.
|
|
14
18
|
*
|
|
15
19
|
* References:
|
|
16
20
|
* - Girvan, M., & Newman, M. E. J. (2002). Community structure in social
|
|
17
21
|
* and biological networks. Proceedings of the National Academy of Sciences,
|
|
18
22
|
* 99(12), 7821-7826.
|
|
19
|
-
* @param graph - The input graph
|
|
23
|
+
* @param graph - The input graph (must be undirected)
|
|
20
24
|
* @param options - Algorithm options
|
|
21
25
|
* @returns Array of community detection results representing the dendrogram
|
|
26
|
+
* @throws Error on a directed graph
|
|
22
27
|
*/
|
|
23
28
|
export function girvanNewman(graph: Graph, options: GirvanNewmanOptions = {}): CommunityResult[] {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const workingGraph = graph.clone();
|
|
30
|
-
let iterations = 0;
|
|
31
|
-
|
|
32
|
-
// Initial state: one large component
|
|
33
|
-
let components = getConnectedComponentsResult(workingGraph);
|
|
34
|
-
dendrogram.push({
|
|
35
|
-
communities: components.components.filter((community) => community.length >= minCommunitySize),
|
|
36
|
-
modularity: calculateModularity(graph, components.componentMap),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
while (Array.from(workingGraph.edges()).length > 0 && iterations < maxIterations) {
|
|
40
|
-
iterations++;
|
|
41
|
-
// Calculate edge betweenness centrality
|
|
42
|
-
const { betweenness: edgeBetweenness, edgeEndpoints } = calculateEdgeBetweenness(workingGraph);
|
|
43
|
-
|
|
44
|
-
if (edgeBetweenness.size === 0) {
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Find edges with maximum betweenness
|
|
49
|
-
const maxBetweenness = Math.max(...edgeBetweenness.values());
|
|
50
|
-
const edgesToRemove: { source: NodeId; target: NodeId }[] = [];
|
|
51
|
-
|
|
52
|
-
for (const [edgeKey, centrality] of edgeBetweenness) {
|
|
53
|
-
if (Math.abs(centrality - maxBetweenness) < 1e-10) {
|
|
54
|
-
// Use original edge endpoints to preserve node ID types (number vs string)
|
|
55
|
-
const endpoints = edgeEndpoints.get(edgeKey);
|
|
56
|
-
if (endpoints) {
|
|
57
|
-
edgesToRemove.push(endpoints);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Remove edges with highest betweenness
|
|
63
|
-
for (const { source, target } of edgesToRemove) {
|
|
64
|
-
workingGraph.removeEdge(source, target);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Find new connected components (communities)
|
|
68
|
-
components = getConnectedComponentsResult(workingGraph);
|
|
69
|
-
|
|
70
|
-
// Filter communities by minimum size
|
|
71
|
-
const validCommunities = components.components.filter((community) => community.length >= minCommunitySize);
|
|
72
|
-
|
|
73
|
-
// Calculate modularity for the new community structure
|
|
74
|
-
const modularity = calculateModularity(graph, components.componentMap);
|
|
75
|
-
|
|
76
|
-
dendrogram.push({
|
|
77
|
-
communities: validCommunities,
|
|
78
|
-
modularity,
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Stop if desired number of communities reached
|
|
82
|
-
if (maxCommunities && validCommunities.length >= maxCommunities) {
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Stop if no more meaningful communities can be formed
|
|
87
|
-
if (validCommunities.length === workingGraph.nodeCount) {
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return dendrogram;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Edge betweenness result containing centrality values and original edge endpoints
|
|
97
|
-
*/
|
|
98
|
-
interface EdgeBetweennessResult {
|
|
99
|
-
betweenness: Map<string, number>;
|
|
100
|
-
edgeEndpoints: Map<string, { source: NodeId; target: NodeId }>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Calculate edge betweenness centrality for all edges in the graph
|
|
105
|
-
*
|
|
106
|
-
* Edge betweenness is the fraction of shortest paths that pass through the edge.
|
|
107
|
-
* We adapt node betweenness centrality calculation to work with edges.
|
|
108
|
-
* @param graph - The input graph to analyze
|
|
109
|
-
* @returns Object with betweenness map and edge endpoints map (preserving original ID types)
|
|
110
|
-
*/
|
|
111
|
-
function calculateEdgeBetweenness(graph: Graph): EdgeBetweennessResult {
|
|
112
|
-
const edgeBetweenness = new Map<string, number>();
|
|
113
|
-
const edgeEndpoints = new Map<string, { source: NodeId; target: NodeId }>();
|
|
114
|
-
|
|
115
|
-
// Initialize all edges with 0 betweenness and store original endpoints
|
|
116
|
-
for (const edge of graph.edges()) {
|
|
117
|
-
const edgeKey = getEdgeKey(edge.source, edge.target);
|
|
118
|
-
edgeBetweenness.set(edgeKey, 0);
|
|
119
|
-
edgeEndpoints.set(edgeKey, { source: edge.source, target: edge.target });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// For each node as source, calculate shortest paths and accumulate edge betweenness
|
|
123
|
-
for (const sourceNode of graph.nodes()) {
|
|
124
|
-
const source = sourceNode.id;
|
|
125
|
-
|
|
126
|
-
// Run BFS to find all shortest paths from source
|
|
127
|
-
const { distances, predecessors, pathCounts } = findAllShortestPaths(graph, source);
|
|
128
|
-
|
|
129
|
-
// Calculate dependency for each node and accumulate edge betweenness
|
|
130
|
-
const dependency = new Map<NodeId, number>();
|
|
131
|
-
|
|
132
|
-
// Initialize dependency
|
|
133
|
-
for (const node of graph.nodes()) {
|
|
134
|
-
dependency.set(node.id, 0);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Process nodes in order of decreasing distance
|
|
138
|
-
const sortedNodes = Array.from(distances.keys())
|
|
139
|
-
.filter((node) => {
|
|
140
|
-
const distance = distances.get(node);
|
|
141
|
-
return distance !== undefined && distance < Infinity;
|
|
142
|
-
})
|
|
143
|
-
.sort((a, b) => {
|
|
144
|
-
const distanceA = distances.get(a);
|
|
145
|
-
const distanceB = distances.get(b);
|
|
146
|
-
if (distanceA === undefined || distanceB === undefined) {
|
|
147
|
-
return 0;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return distanceB - distanceA;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
for (const node of sortedNodes) {
|
|
154
|
-
if (node === source) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const nodeDependency = dependency.get(node);
|
|
159
|
-
const nodePathCount = pathCounts.get(node);
|
|
160
|
-
|
|
161
|
-
if (nodeDependency === undefined || nodePathCount === undefined) {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Distribute dependency to predecessors
|
|
166
|
-
const nodePredecessors = predecessors.get(node) ?? [];
|
|
167
|
-
for (const predecessor of nodePredecessors) {
|
|
168
|
-
const predPathCount = pathCounts.get(predecessor);
|
|
169
|
-
if (predPathCount === undefined) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const edgeDependency = (predPathCount / nodePathCount) * (1 + nodeDependency);
|
|
174
|
-
|
|
175
|
-
// Update predecessor dependency
|
|
176
|
-
const currentDependency = dependency.get(predecessor);
|
|
177
|
-
if (currentDependency !== undefined) {
|
|
178
|
-
dependency.set(predecessor, currentDependency + edgeDependency);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Update edge betweenness
|
|
182
|
-
const edgeKey = getEdgeKey(predecessor, node);
|
|
183
|
-
const currentBetweenness = edgeBetweenness.get(edgeKey) ?? 0;
|
|
184
|
-
edgeBetweenness.set(edgeKey, currentBetweenness + edgeDependency);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
29
|
+
// A directed graph throws from the old code's component step. A self-loop counts once in the
|
|
30
|
+
// old modularity's node degree and twice in the port's; ids spelled alike share the old code's
|
|
31
|
+
// edge keys.
|
|
32
|
+
if (graph.isDirected || needsLegacyCode(graph)) {
|
|
33
|
+
return legacyGirvanNewman(graph, options);
|
|
187
34
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
for (const [edgeKey, betweenness] of edgeBetweenness) {
|
|
192
|
-
edgeBetweenness.set(edgeKey, betweenness / 2);
|
|
193
|
-
}
|
|
35
|
+
const s = toSnapshot(graph);
|
|
36
|
+
if (s.flags.hasSelfLoops) {
|
|
37
|
+
return legacyGirvanNewman(graph, options);
|
|
194
38
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
const distances = new Map<NodeId, number>();
|
|
214
|
-
const predecessors = new Map<NodeId, NodeId[]>();
|
|
215
|
-
const pathCounts = new Map<NodeId, number>();
|
|
216
|
-
const queue: NodeId[] = [];
|
|
217
|
-
const visited = new Set<NodeId>();
|
|
218
|
-
|
|
219
|
-
// Only initialize for reachable nodes
|
|
220
|
-
distances.set(source, 0);
|
|
221
|
-
predecessors.set(source, []);
|
|
222
|
-
pathCounts.set(source, 1);
|
|
223
|
-
queue.push(source);
|
|
224
|
-
|
|
225
|
-
// Optimized BFS with early termination
|
|
226
|
-
let queueIndex = 0;
|
|
227
|
-
while (queueIndex < queue.length) {
|
|
228
|
-
const current = queue[queueIndex++];
|
|
229
|
-
if (current === undefined) {
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (visited.has(current)) {
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
visited.add(current);
|
|
238
|
-
|
|
239
|
-
const currentDistance = distances.get(current);
|
|
240
|
-
if (currentDistance === undefined) {
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
for (const neighbor of graph.neighbors(current)) {
|
|
245
|
-
// Skip if already processed
|
|
246
|
-
if (visited.has(neighbor)) {
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const edgeWeight = 1; // Unweighted for community detection
|
|
251
|
-
const newDistance = currentDistance + edgeWeight;
|
|
252
|
-
const neighborDistance = distances.get(neighbor) ?? Infinity;
|
|
253
|
-
|
|
254
|
-
if (newDistance < neighborDistance) {
|
|
255
|
-
// Found shorter path
|
|
256
|
-
distances.set(neighbor, newDistance);
|
|
257
|
-
predecessors.set(neighbor, [current]);
|
|
258
|
-
const currentPathCount = pathCounts.get(current);
|
|
259
|
-
if (currentPathCount !== undefined) {
|
|
260
|
-
pathCounts.set(neighbor, currentPathCount);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
queue.push(neighbor);
|
|
264
|
-
} else if (newDistance === neighborDistance) {
|
|
265
|
-
// Found alternative shortest path
|
|
266
|
-
const neighborPredecessors = predecessors.get(neighbor);
|
|
267
|
-
if (neighborPredecessors) {
|
|
268
|
-
neighborPredecessors.push(current);
|
|
269
|
-
const currentPathCount = pathCounts.get(current);
|
|
270
|
-
if (currentPathCount !== undefined) {
|
|
271
|
-
pathCounts.set(neighbor, (pathCounts.get(neighbor) ?? 0) + currentPathCount);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return { distances, predecessors, pathCounts };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Generate a consistent edge key for undirected graphs
|
|
283
|
-
* @param source - The source node ID
|
|
284
|
-
* @param target - The target node ID
|
|
285
|
-
* @returns A consistent string key for the edge (ordered alphabetically)
|
|
286
|
-
*/
|
|
287
|
-
function getEdgeKey(source: NodeId, target: NodeId): string {
|
|
288
|
-
// For undirected graphs, ensure consistent ordering
|
|
289
|
-
const sourceStr = String(source);
|
|
290
|
-
const targetStr = String(target);
|
|
291
|
-
|
|
292
|
-
if (sourceStr <= targetStr) {
|
|
293
|
-
return `${sourceStr}|${targetStr}`;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return `${targetStr}|${sourceStr}`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Calculate modularity for a given community structure
|
|
301
|
-
*
|
|
302
|
-
* Newman's Q, written per community:
|
|
303
|
-
*
|
|
304
|
-
* Q = sum over communities c of [ w_in(c) / m - ( K_c / (2m) )^2 ]
|
|
305
|
-
*
|
|
306
|
-
* where m is the total edge weight with each undirected edge counted once, w_in(c) is the summed
|
|
307
|
-
* weight of the edges with both endpoints inside c, and K_c is the summed weighted degree of c's
|
|
308
|
-
* nodes.
|
|
309
|
-
*
|
|
310
|
-
* WHY NOT A SUM OVER EDGES. The textbook double sum runs over every ordered PAIR of nodes inside
|
|
311
|
-
* a community, not only over the pairs an edge happens to join. Collecting the null-model term
|
|
312
|
-
* only where an edge exists leaves the penalty far too small, and the shortfall grows with
|
|
313
|
-
* community size -- so the uncut whole graph, whose modularity is 0 by definition, outscores
|
|
314
|
-
* every real cut and a caller reading the dendrogram the standard way is handed one community
|
|
315
|
-
* containing everything. The per-community form above counts every pair exactly once and still
|
|
316
|
-
* costs one pass over the edges rather than a pass over every pair.
|
|
317
|
-
* @param graph - The original graph
|
|
318
|
-
* @param communityMap - Map from node IDs to community indices
|
|
319
|
-
* @returns The modularity score of the partition
|
|
320
|
-
*/
|
|
321
|
-
function calculateModularity(graph: Graph, communityMap: Map<NodeId, number>): number {
|
|
322
|
-
const totalEdgeWeight = getTotalEdgeWeight(graph);
|
|
323
|
-
if (totalEdgeWeight === 0) {
|
|
324
|
-
return 0;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Summed weighted degree of each community's nodes.
|
|
328
|
-
const degreeSum = new Map<number, number>();
|
|
329
|
-
for (const node of graph.nodes()) {
|
|
330
|
-
const community = communityMap.get(node.id);
|
|
331
|
-
if (community === undefined) {
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
degreeSum.set(community, (degreeSum.get(community) ?? 0) + getNodeDegree(graph, node.id));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Summed weight of the edges that stay inside a community.
|
|
339
|
-
const internalWeight = new Map<number, number>();
|
|
340
|
-
for (const edge of graph.edges()) {
|
|
341
|
-
const communityI = communityMap.get(edge.source);
|
|
342
|
-
const communityJ = communityMap.get(edge.target);
|
|
343
|
-
|
|
344
|
-
if (communityI === undefined || communityI !== communityJ) {
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
internalWeight.set(communityI, (internalWeight.get(communityI) ?? 0) + (edge.weight ?? 1));
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let modularity = 0;
|
|
352
|
-
for (const [community, degrees] of degreeSum) {
|
|
353
|
-
const share = degrees / (2 * totalEdgeWeight);
|
|
354
|
-
modularity += (internalWeight.get(community) ?? 0) / totalEdgeWeight - share * share;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return modularity;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Calculate total edge weight in the graph
|
|
362
|
-
* @param graph - The input graph
|
|
363
|
-
* @returns Total sum of all edge weights
|
|
364
|
-
*/
|
|
365
|
-
function getTotalEdgeWeight(graph: Graph): number {
|
|
366
|
-
let totalWeight = 0;
|
|
367
|
-
|
|
368
|
-
for (const edge of graph.edges()) {
|
|
369
|
-
totalWeight += edge.weight ?? 1;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return totalWeight;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Get the total degree (sum of edge weights) for a node
|
|
377
|
-
* @param graph - The input graph
|
|
378
|
-
* @param nodeId - The node ID to compute degree for
|
|
379
|
-
* @returns The weighted degree of the node
|
|
380
|
-
*/
|
|
381
|
-
function getNodeDegree(graph: Graph, nodeId: NodeId): number {
|
|
382
|
-
let degree = 0;
|
|
383
|
-
|
|
384
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
385
|
-
const edge = graph.getEdge(nodeId, neighbor);
|
|
386
|
-
degree += edge?.weight ?? 1;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
return degree;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Convert connected components result to ComponentResult format
|
|
394
|
-
* @param graph - The input graph
|
|
395
|
-
* @returns Object containing components array and node-to-component map
|
|
396
|
-
*/
|
|
397
|
-
function getConnectedComponentsResult(graph: Graph): ComponentResult {
|
|
398
|
-
const components = connectedComponents(graph);
|
|
399
|
-
const componentMap = new Map<NodeId, number>();
|
|
400
|
-
|
|
401
|
-
components.forEach((component, index) => {
|
|
402
|
-
component.forEach((nodeId) => {
|
|
403
|
-
componentMap.set(nodeId, index);
|
|
404
|
-
});
|
|
39
|
+
const minCommunitySize = options.minCommunitySize ?? 1;
|
|
40
|
+
let { maxCommunities, maxIterations } = options;
|
|
41
|
+
// The old code stopped after the first round on any truthy maxCommunities it had reached, which
|
|
42
|
+
// a negative one always has; the port ignores a count below 1.
|
|
43
|
+
if (maxCommunities !== undefined && maxCommunities < 0) {
|
|
44
|
+
maxCommunities = undefined;
|
|
45
|
+
maxIterations = Math.min(maxIterations ?? 100, 1);
|
|
46
|
+
}
|
|
47
|
+
const r = indexedGirvanNewman(s, { maxCommunities, minCommunitySize, maxIterations });
|
|
48
|
+
return r.levels.map((labels, i) => {
|
|
49
|
+
let count = 0;
|
|
50
|
+
for (const label of labels) {
|
|
51
|
+
count = Math.max(count, label + 1);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
communities: labelsToGroups(s.ids, labels, count).filter((c) => c.length >= minCommunitySize),
|
|
55
|
+
modularity: r.modularity[i],
|
|
56
|
+
};
|
|
405
57
|
});
|
|
406
|
-
|
|
407
|
-
return { components, componentMap };
|
|
408
58
|
}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Graph } from "../../core/graph.js";
|
|
13
|
+
import { labelPropagation as indexedLabelPropagation } from "../../indexed/label-propagation.js";
|
|
14
|
+
import { toSnapshot } from "../../indexed/to-snapshot.js";
|
|
13
15
|
import { graphToMap } from "../../utils/graph-converters.js";
|
|
14
16
|
import { SeededRandom, shuffle } from "../../utils/math-utilities.js";
|
|
15
17
|
|
|
@@ -24,130 +26,6 @@ export interface LabelPropagationResult {
|
|
|
24
26
|
converged: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
/**
|
|
28
|
-
* Internal implementation of Label Propagation Algorithm
|
|
29
|
-
* @param graph - Map representation of the graph (node to neighbors with weights)
|
|
30
|
-
* @param options - Algorithm configuration options
|
|
31
|
-
* @returns Community assignments, iteration count, and convergence status
|
|
32
|
-
*/
|
|
33
|
-
function labelPropagationImpl(
|
|
34
|
-
graph: Map<string, Map<string, number>>,
|
|
35
|
-
options: LabelPropagationOptions = {},
|
|
36
|
-
): LabelPropagationResult {
|
|
37
|
-
const { maxIterations = 100, randomSeed = 42 } = options;
|
|
38
|
-
|
|
39
|
-
if (graph.size === 0) {
|
|
40
|
-
return {
|
|
41
|
-
communities: new Map(),
|
|
42
|
-
iterations: 0,
|
|
43
|
-
converged: true,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Initialize random number generator
|
|
48
|
-
const random = SeededRandom.createGenerator(randomSeed);
|
|
49
|
-
|
|
50
|
-
// Initialize labels - each node gets its own label
|
|
51
|
-
const labels = new Map<string, number>();
|
|
52
|
-
const nodes = Array.from(graph.keys());
|
|
53
|
-
nodes.forEach((node, i) => labels.set(node, i));
|
|
54
|
-
|
|
55
|
-
let iterations = 0;
|
|
56
|
-
let converged = false;
|
|
57
|
-
|
|
58
|
-
// Main loop
|
|
59
|
-
while (iterations < maxIterations && !converged) {
|
|
60
|
-
iterations++;
|
|
61
|
-
converged = true;
|
|
62
|
-
|
|
63
|
-
// Create random order for node updates
|
|
64
|
-
const nodeOrder = [...nodes];
|
|
65
|
-
shuffle(nodeOrder, random);
|
|
66
|
-
|
|
67
|
-
// Update each node's label
|
|
68
|
-
for (const node of nodeOrder) {
|
|
69
|
-
const neighbors = graph.get(node);
|
|
70
|
-
if (!neighbors || neighbors.size === 0) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Count label frequencies among neighbors
|
|
75
|
-
const labelCounts = new Map<number, number>();
|
|
76
|
-
let maxCount = 0;
|
|
77
|
-
const candidateLabels: number[] = [];
|
|
78
|
-
|
|
79
|
-
for (const [neighbor, weight] of neighbors) {
|
|
80
|
-
const neighborLabel = labels.get(neighbor);
|
|
81
|
-
if (neighborLabel === undefined) {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
|
|
86
|
-
labelCounts.set(neighborLabel, count);
|
|
87
|
-
|
|
88
|
-
if (count > maxCount) {
|
|
89
|
-
maxCount = count;
|
|
90
|
-
candidateLabels.length = 0;
|
|
91
|
-
candidateLabels.push(neighborLabel);
|
|
92
|
-
} else if (count === maxCount) {
|
|
93
|
-
candidateLabels.push(neighborLabel);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Choose label (break ties randomly)
|
|
98
|
-
const currentLabel = labels.get(node);
|
|
99
|
-
if (currentLabel === undefined) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let newLabel = currentLabel;
|
|
104
|
-
|
|
105
|
-
if (candidateLabels.length > 0) {
|
|
106
|
-
// Include current label if it has max count
|
|
107
|
-
if (labelCounts.get(currentLabel) === maxCount) {
|
|
108
|
-
candidateLabels.push(currentLabel);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Random selection from candidates
|
|
112
|
-
const index = Math.floor(random() * candidateLabels.length);
|
|
113
|
-
const selectedLabel = candidateLabels[index];
|
|
114
|
-
if (selectedLabel !== undefined) {
|
|
115
|
-
newLabel = selectedLabel;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Update label if changed
|
|
120
|
-
if (newLabel !== currentLabel) {
|
|
121
|
-
labels.set(node, newLabel);
|
|
122
|
-
converged = false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Renumber communities consecutively
|
|
128
|
-
const uniqueLabels = new Set(labels.values());
|
|
129
|
-
const labelMap = new Map<number, number>();
|
|
130
|
-
let communityId = 0;
|
|
131
|
-
|
|
132
|
-
for (const label of uniqueLabels) {
|
|
133
|
-
labelMap.set(label, communityId++);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const communities = new Map<string, number>();
|
|
137
|
-
for (const [node, label] of labels) {
|
|
138
|
-
const mappedLabel = labelMap.get(label);
|
|
139
|
-
if (mappedLabel !== undefined) {
|
|
140
|
-
communities.set(node, mappedLabel);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
communities,
|
|
146
|
-
iterations,
|
|
147
|
-
converged,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
29
|
/**
|
|
152
30
|
* Internal implementation of Asynchronous Label Propagation
|
|
153
31
|
* Updates all nodes simultaneously (can lead to oscillations)
|
|
@@ -366,19 +244,32 @@ function labelPropagationSemiSupervisedImpl(
|
|
|
366
244
|
}
|
|
367
245
|
|
|
368
246
|
/**
|
|
369
|
-
* Label Propagation Algorithm
|
|
370
|
-
* Each node adopts the
|
|
371
|
-
*
|
|
247
|
+
* Label Propagation Algorithm (fast label propagation: a seeded work queue of nodes whose label
|
|
248
|
+
* may no longer be dominant). Each node adopts the label with the largest summed edge weight among
|
|
249
|
+
* its neighbours, ties drawn uniformly; on a directed graph in- and out-neighbours both vote, and a
|
|
250
|
+
* self-loop does not vote. Communities are numbered in order of first appearance in node order.
|
|
251
|
+
* @param graph - The graph (can be weighted)
|
|
372
252
|
* @param options - Algorithm options
|
|
373
|
-
* @returns Community assignments
|
|
253
|
+
* @returns Community assignments keyed by `String(id)`
|
|
254
|
+
* @throws RangeError when an edge weight is negative, NaN or infinite
|
|
374
255
|
*
|
|
375
|
-
* Time Complexity: O(m) per
|
|
376
|
-
* Space Complexity: O(n)
|
|
256
|
+
* Time Complexity: O(m) per full pass, at most maxIterations passes
|
|
257
|
+
* Space Complexity: O(n + m)
|
|
377
258
|
*/
|
|
378
259
|
export function labelPropagation(graph: Graph, options: LabelPropagationOptions = {}): LabelPropagationResult {
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
260
|
+
const s = toSnapshot(graph);
|
|
261
|
+
const n = s.nodeCount;
|
|
262
|
+
// This function always took any number: its loop ran while iterations < maxIterations, and any
|
|
263
|
+
// seed started a stream. The port wants integers, so round here the way that loop counted.
|
|
264
|
+
const max = options.maxIterations ?? 100;
|
|
265
|
+
const cap = max > 0 ? Math.min(Math.ceil(max), Math.floor(Number.MAX_SAFE_INTEGER / Math.max(n, 1))) : 0;
|
|
266
|
+
const seed = options.randomSeed ?? 42;
|
|
267
|
+
const { labels, iterations, converged } = indexedLabelPropagation(s, {
|
|
268
|
+
maxIterations: cap,
|
|
269
|
+
randomSeed: Number.isFinite(seed) ? Math.floor(seed) : 0,
|
|
270
|
+
});
|
|
271
|
+
// With no pass run on a non-empty graph, nothing was checked, so nothing converged.
|
|
272
|
+
return { communities: s.ids.toStringMap(labels), iterations, converged: converged && (cap > 0 || n === 0) };
|
|
382
273
|
}
|
|
383
274
|
|
|
384
275
|
/**
|