@graphty/algorithms 1.2.0 → 1.4.2
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 +375 -276
- package/dist/algorithms.d.ts +42 -2
- package/dist/algorithms.js +11402 -2
- package/dist/algorithms.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +14 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +144 -169
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +35 -1
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +70 -149
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.d.ts +7 -0
- package/dist/src/algorithms/centrality/degree.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +12 -9
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +57 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js +304 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts +106 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js +320 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
- package/dist/src/algorithms/centrality/eigenvector.d.ts +10 -3
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.js +11 -4
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
- package/dist/src/algorithms/centrality/hits.d.ts +10 -3
- package/dist/src/algorithms/centrality/hits.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/hits.js +11 -4
- package/dist/src/algorithms/centrality/hits.js.map +1 -1
- package/dist/src/algorithms/centrality/index.d.ts +3 -1
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/index.js +2 -1
- package/dist/src/algorithms/centrality/index.js.map +1 -1
- package/dist/src/algorithms/centrality/katz.d.ts +10 -3
- package/dist/src/algorithms/centrality/katz.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/katz.js +13 -8
- package/dist/src/algorithms/centrality/katz.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +31 -0
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +61 -1
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.d.ts +2 -3
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +67 -57
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/index.d.ts +2 -0
- package/dist/src/algorithms/community/index.d.ts.map +1 -1
- package/dist/src/algorithms/community/index.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +11 -4
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +60 -38
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/community/leiden.d.ts +3 -3
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
- package/dist/src/algorithms/community/leiden.js +70 -44
- package/dist/src/algorithms/community/leiden.js.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.d.ts +149 -0
- package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
- package/dist/src/algorithms/community/louvain-optimized.js +338 -0
- package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
- package/dist/src/algorithms/community/louvain.d.ts +2 -3
- package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
- package/dist/src/algorithms/community/louvain.js +35 -81
- package/dist/src/algorithms/community/louvain.js.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.d.ts +55 -0
- package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
- package/dist/src/algorithms/community/modularity-utils.js +108 -0
- package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
- package/dist/src/algorithms/components/connected.d.ts +23 -0
- package/dist/src/algorithms/components/connected.d.ts.map +1 -1
- package/dist/src/algorithms/components/connected.js +29 -3
- package/dist/src/algorithms/components/connected.js.map +1 -1
- package/dist/src/algorithms/matching/bipartite.d.ts +10 -0
- package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
- package/dist/src/algorithms/matching/bipartite.js +18 -40
- package/dist/src/algorithms/matching/bipartite.js.map +1 -1
- package/dist/src/algorithms/matching/index.d.ts.map +1 -1
- package/dist/src/algorithms/matching/index.js.map +1 -1
- package/dist/src/algorithms/matching/isomorphism.d.ts +8 -0
- package/dist/src/algorithms/matching/isomorphism.d.ts.map +1 -1
- package/dist/src/algorithms/matching/isomorphism.js +36 -2
- package/dist/src/algorithms/matching/isomorphism.js.map +1 -1
- package/dist/src/algorithms/mst/index.d.ts.map +1 -1
- package/dist/src/algorithms/mst/index.js.map +1 -1
- package/dist/src/algorithms/mst/kruskal.d.ts +14 -0
- package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -1
- package/dist/src/algorithms/mst/kruskal.js +17 -3
- package/dist/src/algorithms/mst/kruskal.js.map +1 -1
- package/dist/src/algorithms/mst/prim.d.ts +9 -0
- package/dist/src/algorithms/mst/prim.d.ts.map +1 -1
- package/dist/src/algorithms/mst/prim.js +9 -0
- package/dist/src/algorithms/mst/prim.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts +10 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +11 -12
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +44 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +195 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts +20 -2
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +30 -14
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +17 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.js +17 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -1
- package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/index.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +42 -0
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-unified.js +370 -0
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts +77 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.js +402 -0
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +5 -182
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/algorithms/traversal/dfs.d.ts +10 -0
- package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/dfs.js +48 -0
- package/dist/src/algorithms/traversal/dfs.js.map +1 -1
- package/dist/src/algorithms/traversal/index.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/index.js.map +1 -1
- package/dist/src/benchmark-all-algorithms.d.ts +25 -0
- package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
- package/dist/src/benchmark-all-algorithms.js +491 -0
- package/dist/src/benchmark-all-algorithms.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +23 -15
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +226 -224
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/index.d.ts +4 -2
- package/dist/src/clustering/index.d.ts.map +1 -1
- package/dist/src/clustering/index.js +2 -2
- package/dist/src/clustering/index.js.map +1 -1
- package/dist/src/clustering/k-core.d.ts +17 -19
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +68 -30
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/mcl.d.ts +6 -0
- package/dist/src/clustering/mcl.d.ts.map +1 -1
- package/dist/src/clustering/mcl.js +34 -3
- package/dist/src/clustering/mcl.js.map +1 -1
- package/dist/src/clustering/spectral.d.ts +11 -0
- package/dist/src/clustering/spectral.d.ts.map +1 -1
- package/dist/src/clustering/spectral.js +99 -47
- package/dist/src/clustering/spectral.js.map +1 -1
- package/dist/src/core/graph.d.ts +45 -0
- package/dist/src/core/graph.d.ts.map +1 -1
- package/dist/src/core/graph.js +49 -5
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/data-structures/index.d.ts.map +1 -1
- package/dist/src/data-structures/index.js.map +1 -1
- package/dist/src/data-structures/priority-queue.d.ts +18 -1
- package/dist/src/data-structures/priority-queue.d.ts.map +1 -1
- package/dist/src/data-structures/priority-queue.js +26 -5
- package/dist/src/data-structures/priority-queue.js.map +1 -1
- package/dist/src/data-structures/union-find.d.ts +21 -0
- package/dist/src/data-structures/union-find.d.ts.map +1 -1
- package/dist/src/data-structures/union-find.js +21 -0
- package/dist/src/data-structures/union-find.js.map +1 -1
- package/dist/src/flow/ford-fulkerson.d.ts +20 -12
- package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
- package/dist/src/flow/ford-fulkerson.js +170 -217
- package/dist/src/flow/ford-fulkerson.js.map +1 -1
- package/dist/src/flow/min-cut.d.ts +6 -8
- package/dist/src/flow/min-cut.d.ts.map +1 -1
- package/dist/src/flow/min-cut.js +34 -12
- package/dist/src/flow/min-cut.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/link-prediction/adamic-adar.d.ts +26 -0
- package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
- package/dist/src/link-prediction/adamic-adar.js +47 -30
- package/dist/src/link-prediction/adamic-adar.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors.d.ts +21 -0
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +31 -14
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/link-prediction/index.d.ts.map +1 -1
- package/dist/src/optimized/bit-packed.d.ts +193 -0
- package/dist/src/optimized/bit-packed.d.ts.map +1 -0
- package/dist/src/optimized/bit-packed.js +342 -0
- package/dist/src/optimized/bit-packed.js.map +1 -0
- package/dist/src/optimized/csr-graph.d.ts +149 -0
- package/dist/src/optimized/csr-graph.d.ts.map +1 -0
- package/dist/src/optimized/csr-graph.js +404 -0
- package/dist/src/optimized/csr-graph.js.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts +95 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.js +242 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
- package/dist/src/optimized/graph-adapter.d.ts +111 -0
- package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
- package/dist/src/optimized/graph-adapter.js +194 -0
- package/dist/src/optimized/graph-adapter.js.map +1 -0
- package/dist/src/optimized/index.d.ts +14 -0
- package/dist/src/optimized/index.d.ts.map +1 -0
- package/dist/src/optimized/index.js +30 -0
- package/dist/src/optimized/index.js.map +1 -0
- package/dist/src/pathfinding/astar.d.ts +25 -9
- package/dist/src/pathfinding/astar.d.ts.map +1 -1
- package/dist/src/pathfinding/astar.js +25 -10
- package/dist/src/pathfinding/astar.js.map +1 -1
- package/dist/src/pathfinding/utils.d.ts +15 -6
- package/dist/src/pathfinding/utils.d.ts.map +1 -1
- package/dist/src/pathfinding/utils.js +19 -7
- package/dist/src/pathfinding/utils.js.map +1 -1
- package/dist/src/research/grsbm.d.ts +0 -1
- package/dist/src/research/grsbm.d.ts.map +1 -1
- package/dist/src/research/grsbm.js +28 -23
- package/dist/src/research/grsbm.js.map +1 -1
- package/dist/src/research/index.d.ts.map +1 -1
- package/dist/src/research/index.js.map +1 -1
- package/dist/src/research/sync.d.ts +0 -1
- package/dist/src/research/sync.d.ts.map +1 -1
- package/dist/src/research/sync.js +31 -36
- package/dist/src/research/sync.js.map +1 -1
- package/dist/src/research/terahac.d.ts +2 -1
- package/dist/src/research/terahac.d.ts.map +1 -1
- package/dist/src/research/terahac.js +39 -7
- package/dist/src/research/terahac.js.map +1 -1
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/utils/algorithm-utilities.d.ts +2 -0
- package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
- package/dist/src/utils/algorithm-utilities.js +2 -0
- package/dist/src/utils/algorithm-utilities.js.map +1 -0
- package/dist/src/utils/graph-converters.d.ts +110 -0
- package/dist/src/utils/graph-converters.d.ts.map +1 -0
- package/dist/src/utils/graph-converters.js +227 -0
- package/dist/src/utils/graph-converters.js.map +1 -0
- package/dist/src/utils/graph-utilities.d.ts +62 -0
- package/dist/src/utils/graph-utilities.d.ts.map +1 -0
- package/dist/src/utils/graph-utilities.js +148 -0
- package/dist/src/utils/graph-utilities.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/math-utilities.d.ts +63 -0
- package/dist/src/utils/math-utilities.d.ts.map +1 -0
- package/dist/src/utils/math-utilities.js +132 -0
- package/dist/src/utils/math-utilities.js.map +1 -0
- package/dist/src/utils/matrix-utilities.d.ts +2 -0
- package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
- package/dist/src/utils/matrix-utilities.js +2 -0
- package/dist/src/utils/matrix-utilities.js.map +1 -0
- package/dist/src/utils/optimization-helpers.d.ts +42 -0
- package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
- package/dist/src/utils/optimization-helpers.js +59 -0
- package/dist/src/utils/optimization-helpers.js.map +1 -0
- package/dist/src/utils/priorityQueue.d.ts +24 -0
- package/dist/src/utils/priorityQueue.d.ts.map +1 -1
- package/dist/src/utils/priorityQueue.js +34 -4
- package/dist/src/utils/priorityQueue.js.map +1 -1
- package/package.json +58 -39
- package/src/algorithms/centrality/betweenness.ts +185 -200
- package/src/algorithms/centrality/closeness.ts +93 -194
- package/src/algorithms/centrality/degree.ts +18 -22
- package/src/algorithms/centrality/delta-pagerank-simple.ts +374 -0
- package/src/algorithms/centrality/delta-pagerank.ts +458 -0
- package/src/algorithms/centrality/eigenvector.ts +14 -15
- package/src/algorithms/centrality/hits.ts +17 -17
- package/src/algorithms/centrality/index.ts +20 -13
- package/src/algorithms/centrality/katz.ts +17 -25
- package/src/algorithms/centrality/pagerank.ts +91 -16
- package/src/algorithms/community/girvan-newman.ts +90 -86
- package/src/algorithms/community/index.ts +6 -5
- package/src/algorithms/community/label-propagation.ts +69 -48
- package/src/algorithms/community/leiden.ts +88 -65
- package/src/algorithms/community/louvain-optimized.ts +474 -0
- package/src/algorithms/community/louvain.ts +52 -122
- package/src/algorithms/community/modularity-utils.ts +134 -0
- package/src/algorithms/components/connected.ts +40 -17
- package/src/algorithms/matching/bipartite.ts +24 -53
- package/src/algorithms/matching/index.ts +4 -4
- package/src/algorithms/matching/isomorphism.ts +53 -23
- package/src/algorithms/mst/index.ts +3 -4
- package/src/algorithms/mst/kruskal.ts +21 -6
- package/src/algorithms/mst/prim.ts +13 -4
- package/src/algorithms/shortest-path/bellman-ford.ts +16 -28
- package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
- package/src/algorithms/shortest-path/dijkstra.ts +38 -30
- package/src/algorithms/shortest-path/floyd-warshall.ts +21 -4
- package/src/algorithms/shortest-path/index.ts +5 -5
- package/src/algorithms/traversal/bfs-unified.ts +437 -0
- package/src/algorithms/traversal/bfs-variants.ts +520 -0
- package/src/algorithms/traversal/bfs.ts +5 -229
- package/src/algorithms/traversal/dfs.ts +61 -42
- package/src/algorithms/traversal/index.ts +3 -3
- package/src/benchmark-all-algorithms.ts +580 -0
- package/src/clustering/hierarchical.ts +268 -272
- package/src/clustering/index.ts +8 -6
- package/src/clustering/k-core.ts +93 -61
- package/src/clustering/mcl.ts +53 -23
- package/src/clustering/spectral.ts +130 -72
- package/src/core/graph.ts +58 -18
- package/src/data-structures/index.ts +2 -2
- package/src/data-structures/priority-queue.ts +36 -11
- package/src/data-structures/union-find.ts +22 -1
- package/src/flow/ford-fulkerson.ts +221 -262
- package/src/flow/min-cut.ts +59 -51
- package/src/index.ts +9 -2
- package/src/link-prediction/adamic-adar.ts +75 -74
- package/src/link-prediction/common-neighbors.ts +49 -49
- package/src/link-prediction/index.ts +1 -1
- package/src/optimized/bit-packed.ts +390 -0
- package/src/optimized/csr-graph.ts +518 -0
- package/src/optimized/direction-optimized-bfs.ts +310 -0
- package/src/optimized/graph-adapter.ts +248 -0
- package/src/optimized/index.ts +34 -0
- package/src/pathfinding/astar.ts +40 -25
- package/src/pathfinding/utils.ts +20 -13
- package/src/research/grsbm.ts +36 -39
- package/src/research/index.ts +4 -4
- package/src/research/sync.ts +41 -54
- package/src/research/terahac.ts +63 -21
- package/src/types/index.ts +8 -0
- package/src/utils/algorithm-utilities.ts +2 -0
- package/src/utils/graph-converters.ts +269 -0
- package/src/utils/graph-utilities.ts +173 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/math-utilities.ts +149 -0
- package/src/utils/matrix-utilities.ts +2 -0
- package/src/utils/optimization-helpers.ts +87 -0
- package/src/utils/priorityQueue.ts +40 -6
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import type { Graph } from "../../core/graph.js";
|
|
2
|
+
import type { CommunityResult, NodeId } from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Optimized Louvain community detection algorithm with early pruning and threshold cycling
|
|
6
|
+
*
|
|
7
|
+
* Key optimizations:
|
|
8
|
+
* - Leaf node pruning: Skip nodes with degree 1
|
|
9
|
+
* - Importance ordering: Process high-impact nodes first
|
|
10
|
+
* - Threshold cycling: Adaptive convergence thresholds
|
|
11
|
+
* - Early termination: Stop when changes become insignificant
|
|
12
|
+
*
|
|
13
|
+
* Expected speedup: 2-5x on large graphs with many leaf nodes
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface OptimizedLouvainOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Resolution parameter (default: 1.0)
|
|
19
|
+
*/
|
|
20
|
+
resolution?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Maximum iterations per level (default: 100)
|
|
23
|
+
*/
|
|
24
|
+
maxIterations?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Convergence tolerance (default: 1e-6)
|
|
27
|
+
*/
|
|
28
|
+
tolerance?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Enable leaf node pruning (default: true)
|
|
31
|
+
*/
|
|
32
|
+
pruneLeaves?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Enable importance-based node ordering (default: true)
|
|
35
|
+
*/
|
|
36
|
+
importanceOrdering?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Base pruning threshold (default: 0.01)
|
|
39
|
+
*/
|
|
40
|
+
pruningThreshold?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Enable adaptive threshold cycling (default: true)
|
|
43
|
+
*/
|
|
44
|
+
thresholdCycling?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PruningStats {
|
|
48
|
+
leafNodesPruned: number;
|
|
49
|
+
lowDegreeNodesPruned: number;
|
|
50
|
+
stableNodesPruned: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Optimized Louvain implementation with early pruning and threshold cycling
|
|
55
|
+
*/
|
|
56
|
+
export class OptimizedLouvain {
|
|
57
|
+
private graph: Graph;
|
|
58
|
+
private communities: Map<NodeId, number>;
|
|
59
|
+
private communityWeights: Map<number, number>;
|
|
60
|
+
private nodeWeights: Map<NodeId, number>;
|
|
61
|
+
private nodeDegrees: Map<NodeId, number>;
|
|
62
|
+
private totalWeight: number;
|
|
63
|
+
private pruningStats: PruningStats;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create an optimized Louvain detector for the given graph
|
|
67
|
+
* @param graph - The input graph to detect communities in
|
|
68
|
+
*/
|
|
69
|
+
constructor(graph: Graph) {
|
|
70
|
+
this.graph = graph;
|
|
71
|
+
this.communities = new Map();
|
|
72
|
+
this.communityWeights = new Map();
|
|
73
|
+
this.nodeWeights = new Map();
|
|
74
|
+
this.nodeDegrees = new Map();
|
|
75
|
+
this.totalWeight = 0;
|
|
76
|
+
this.pruningStats = {
|
|
77
|
+
leafNodesPruned: 0,
|
|
78
|
+
lowDegreeNodesPruned: 0,
|
|
79
|
+
stableNodesPruned: 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run optimized Louvain algorithm
|
|
85
|
+
* @param options - Algorithm configuration options
|
|
86
|
+
* @returns Community detection result with communities, modularity, and iterations
|
|
87
|
+
*/
|
|
88
|
+
public detectCommunities(options: OptimizedLouvainOptions = {}): CommunityResult {
|
|
89
|
+
const {
|
|
90
|
+
resolution = 1.0,
|
|
91
|
+
maxIterations = 100,
|
|
92
|
+
tolerance = 1e-6,
|
|
93
|
+
pruneLeaves = true,
|
|
94
|
+
importanceOrdering = true,
|
|
95
|
+
pruningThreshold = 0.01,
|
|
96
|
+
thresholdCycling = true,
|
|
97
|
+
} = options;
|
|
98
|
+
|
|
99
|
+
// Initialize
|
|
100
|
+
this.initialize();
|
|
101
|
+
let modularity = this.calculateModularity(resolution);
|
|
102
|
+
let iteration = 0;
|
|
103
|
+
let improved = true;
|
|
104
|
+
|
|
105
|
+
while (iteration < maxIterations && improved) {
|
|
106
|
+
// Get nodes in optimal processing order
|
|
107
|
+
const orderedNodes = importanceOrdering
|
|
108
|
+
? this.getNodesInImportanceOrder()
|
|
109
|
+
: Array.from(this.graph.nodes()).map((n) => n.id);
|
|
110
|
+
|
|
111
|
+
// Apply adaptive threshold
|
|
112
|
+
const threshold = thresholdCycling ? this.getAdaptiveThreshold(iteration, pruningThreshold) : 0;
|
|
113
|
+
|
|
114
|
+
// Perform local optimization
|
|
115
|
+
improved = this.performLocalMoving(orderedNodes, {
|
|
116
|
+
pruneLeaves,
|
|
117
|
+
threshold,
|
|
118
|
+
resolution,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (improved) {
|
|
122
|
+
const newModularity = this.calculateModularity(resolution);
|
|
123
|
+
|
|
124
|
+
// Check convergence
|
|
125
|
+
if (Math.abs(newModularity - modularity) < tolerance) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
modularity = newModularity;
|
|
130
|
+
iteration++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Convert community assignments to result format
|
|
135
|
+
const communityGroups = new Map<number, NodeId[]>();
|
|
136
|
+
|
|
137
|
+
for (const [nodeId, community] of this.communities) {
|
|
138
|
+
if (!communityGroups.has(community)) {
|
|
139
|
+
communityGroups.set(community, []);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const group = communityGroups.get(community);
|
|
143
|
+
if (group) {
|
|
144
|
+
group.push(nodeId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
communities: Array.from(communityGroups.values()),
|
|
150
|
+
modularity,
|
|
151
|
+
iterations: iteration,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Initialize data structures
|
|
157
|
+
*/
|
|
158
|
+
private initialize(): void {
|
|
159
|
+
let communityId = 0;
|
|
160
|
+
|
|
161
|
+
// Initialize each node in its own community
|
|
162
|
+
for (const node of this.graph.nodes()) {
|
|
163
|
+
this.communities.set(node.id, communityId);
|
|
164
|
+
|
|
165
|
+
// Calculate node weight and degree
|
|
166
|
+
let nodeWeight = 0;
|
|
167
|
+
let degree = 0;
|
|
168
|
+
|
|
169
|
+
for (const neighbor of Array.from(this.graph.neighbors(node.id))) {
|
|
170
|
+
const edge = this.graph.getEdge(node.id, neighbor);
|
|
171
|
+
const weight = edge?.weight ?? 1;
|
|
172
|
+
nodeWeight += weight;
|
|
173
|
+
degree++;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// For undirected graphs, also check incoming edges
|
|
177
|
+
if (!this.graph.isDirected) {
|
|
178
|
+
for (const neighbor of Array.from(this.graph.inNeighbors(node.id))) {
|
|
179
|
+
if (!this.graph.hasEdge(node.id, neighbor)) {
|
|
180
|
+
const edge = this.graph.getEdge(neighbor, node.id);
|
|
181
|
+
const weight = edge?.weight ?? 1;
|
|
182
|
+
nodeWeight += weight;
|
|
183
|
+
degree++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.nodeWeights.set(node.id, nodeWeight);
|
|
189
|
+
this.nodeDegrees.set(node.id, degree);
|
|
190
|
+
this.communityWeights.set(communityId, nodeWeight);
|
|
191
|
+
this.totalWeight += nodeWeight;
|
|
192
|
+
|
|
193
|
+
communityId++;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Total weight is sum of all edge weights
|
|
197
|
+
// For undirected graphs, each edge is counted twice from both endpoints
|
|
198
|
+
this.totalWeight = this.totalWeight / 2;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get nodes ordered by importance (degree * log(weight))
|
|
203
|
+
* @returns Array of node IDs sorted by descending importance
|
|
204
|
+
*/
|
|
205
|
+
private getNodesInImportanceOrder(): NodeId[] {
|
|
206
|
+
const nodeImportance = new Map<NodeId, number>();
|
|
207
|
+
|
|
208
|
+
for (const [nodeId, degree] of this.nodeDegrees) {
|
|
209
|
+
const weight = this.nodeWeights.get(nodeId) ?? 0;
|
|
210
|
+
// Importance score: combination of degree and weight
|
|
211
|
+
// High-degree nodes and nodes with heavy edges are processed first
|
|
212
|
+
const importance = degree * Math.log(1 + weight);
|
|
213
|
+
nodeImportance.set(nodeId, importance);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sort by importance (descending)
|
|
217
|
+
return Array.from(nodeImportance.entries())
|
|
218
|
+
.sort((a, b) => b[1] - a[1])
|
|
219
|
+
.map(([nodeId]) => nodeId);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Perform local moving phase with optimizations
|
|
224
|
+
* @param nodes - Array of node IDs to process
|
|
225
|
+
* @param options - Local moving options
|
|
226
|
+
* @param options.pruneLeaves - Whether to skip leaf nodes
|
|
227
|
+
* @param options.threshold - Minimum gain threshold for moves
|
|
228
|
+
* @param options.resolution - Resolution parameter for modularity
|
|
229
|
+
* @returns True if any improvement was made, false otherwise
|
|
230
|
+
*/
|
|
231
|
+
private performLocalMoving(
|
|
232
|
+
nodes: NodeId[],
|
|
233
|
+
options: {
|
|
234
|
+
pruneLeaves: boolean;
|
|
235
|
+
threshold: number;
|
|
236
|
+
resolution: number;
|
|
237
|
+
},
|
|
238
|
+
): boolean {
|
|
239
|
+
const { pruneLeaves, threshold, resolution } = options;
|
|
240
|
+
let improvement = false;
|
|
241
|
+
let hasChanged = true;
|
|
242
|
+
|
|
243
|
+
while (hasChanged) {
|
|
244
|
+
hasChanged = false;
|
|
245
|
+
|
|
246
|
+
for (const nodeId of nodes) {
|
|
247
|
+
// Early pruning: skip leaf nodes
|
|
248
|
+
if (pruneLeaves && this.isLeafNode(nodeId)) {
|
|
249
|
+
this.pruningStats.leafNodesPruned++;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const currentCommunity = this.communities.get(nodeId) ?? 0;
|
|
254
|
+
const neighborCommunities = this.getNeighborCommunities(nodeId);
|
|
255
|
+
|
|
256
|
+
// Skip isolated nodes
|
|
257
|
+
if (neighborCommunities.size === 0) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Find best community to move to
|
|
262
|
+
let bestCommunity = currentCommunity;
|
|
263
|
+
let bestGain = 0;
|
|
264
|
+
|
|
265
|
+
// Remove node from its current community to calculate gains
|
|
266
|
+
this.removeNodeFromCommunity(nodeId, currentCommunity);
|
|
267
|
+
|
|
268
|
+
for (const community of neighborCommunities) {
|
|
269
|
+
const gain = this.calculateModularityGain(nodeId, community, resolution);
|
|
270
|
+
|
|
271
|
+
// Apply threshold - only move if gain exceeds threshold
|
|
272
|
+
if (gain > bestGain + threshold) {
|
|
273
|
+
bestGain = gain;
|
|
274
|
+
bestCommunity = community;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Try staying in current community
|
|
279
|
+
const currentGain = this.calculateModularityGain(nodeId, currentCommunity, resolution);
|
|
280
|
+
if (currentGain > bestGain + threshold) {
|
|
281
|
+
bestGain = currentGain;
|
|
282
|
+
bestCommunity = currentCommunity;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Add node to best community
|
|
286
|
+
this.addNodeToCommunity(nodeId, bestCommunity);
|
|
287
|
+
|
|
288
|
+
// Track if node moved
|
|
289
|
+
if (bestCommunity !== currentCommunity) {
|
|
290
|
+
hasChanged = true;
|
|
291
|
+
improvement = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return improvement;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if node is a leaf (degree 1)
|
|
301
|
+
* @param nodeId - The node ID to check
|
|
302
|
+
* @returns True if the node has degree 1, false otherwise
|
|
303
|
+
*/
|
|
304
|
+
private isLeafNode(nodeId: NodeId): boolean {
|
|
305
|
+
const degree = this.nodeDegrees.get(nodeId) ?? 0;
|
|
306
|
+
return degree === 1;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get adaptive threshold that decreases with iterations
|
|
311
|
+
* @param iteration - Current iteration number
|
|
312
|
+
* @param baseThreshold - Base threshold value to scale
|
|
313
|
+
* @returns Adaptive threshold value that decays over iterations
|
|
314
|
+
*/
|
|
315
|
+
private getAdaptiveThreshold(iteration: number, baseThreshold: number): number {
|
|
316
|
+
// Exponentially decay threshold with iterations
|
|
317
|
+
// This allows coarse movements early and fine-tuning later
|
|
318
|
+
return baseThreshold * Math.pow(0.5, iteration / 10);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Calculate modularity gain from moving a node to a community
|
|
323
|
+
* @param nodeId - The node ID to move
|
|
324
|
+
* @param targetCommunity - The target community ID
|
|
325
|
+
* @param resolution - Resolution parameter for modularity calculation
|
|
326
|
+
* @returns The modularity gain from moving the node
|
|
327
|
+
*/
|
|
328
|
+
private calculateModularityGain(nodeId: NodeId, targetCommunity: number, resolution: number): number {
|
|
329
|
+
const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
|
|
330
|
+
|
|
331
|
+
// Sum of weights from node to target community
|
|
332
|
+
let weightToTarget = 0;
|
|
333
|
+
|
|
334
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
335
|
+
if (this.communities.get(neighbor) === targetCommunity) {
|
|
336
|
+
const edge = this.graph.getEdge(nodeId, neighbor);
|
|
337
|
+
weightToTarget += edge?.weight ?? 1;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// For undirected graphs, also check incoming edges
|
|
342
|
+
if (!this.graph.isDirected) {
|
|
343
|
+
for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
|
|
344
|
+
if (this.communities.get(neighbor) === targetCommunity && !this.graph.hasEdge(nodeId, neighbor)) {
|
|
345
|
+
const edge = this.graph.getEdge(neighbor, nodeId);
|
|
346
|
+
weightToTarget += edge?.weight ?? 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Weight of target community
|
|
352
|
+
const targetWeight = this.communityWeights.get(targetCommunity) ?? 0;
|
|
353
|
+
|
|
354
|
+
// Modularity gain formula
|
|
355
|
+
const gain =
|
|
356
|
+
(weightToTarget - (resolution * nodeWeight * targetWeight) / (2 * this.totalWeight)) / this.totalWeight;
|
|
357
|
+
|
|
358
|
+
return gain;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Remove node from community (for gain calculation)
|
|
363
|
+
* @param nodeId - The node ID to remove
|
|
364
|
+
* @param community - The community ID to remove from
|
|
365
|
+
*/
|
|
366
|
+
private removeNodeFromCommunity(nodeId: NodeId, community: number): void {
|
|
367
|
+
const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
|
|
368
|
+
this.communityWeights.set(community, (this.communityWeights.get(community) ?? 0) - nodeWeight);
|
|
369
|
+
this.communities.delete(nodeId);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Add node to community
|
|
374
|
+
* @param nodeId - The node ID to add
|
|
375
|
+
* @param community - The community ID to add to
|
|
376
|
+
*/
|
|
377
|
+
private addNodeToCommunity(nodeId: NodeId, community: number): void {
|
|
378
|
+
const nodeWeight = this.nodeWeights.get(nodeId) ?? 0;
|
|
379
|
+
this.communityWeights.set(community, (this.communityWeights.get(community) ?? 0) + nodeWeight);
|
|
380
|
+
this.communities.set(nodeId, community);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get neighboring communities of a node
|
|
385
|
+
* @param nodeId - The node ID to find neighbor communities for
|
|
386
|
+
* @returns Set of community IDs that neighbors belong to
|
|
387
|
+
*/
|
|
388
|
+
private getNeighborCommunities(nodeId: NodeId): Set<number> {
|
|
389
|
+
const communities = new Set<number>();
|
|
390
|
+
|
|
391
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
392
|
+
const community = this.communities.get(neighbor);
|
|
393
|
+
if (community !== undefined) {
|
|
394
|
+
communities.add(community);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// For undirected graphs, also check incoming edges
|
|
399
|
+
if (!this.graph.isDirected) {
|
|
400
|
+
for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
|
|
401
|
+
const community = this.communities.get(neighbor);
|
|
402
|
+
if (community !== undefined) {
|
|
403
|
+
communities.add(community);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return communities;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Calculate total modularity
|
|
413
|
+
* @param resolution - Resolution parameter for modularity calculation
|
|
414
|
+
* @returns The modularity score of the current partition
|
|
415
|
+
*/
|
|
416
|
+
private calculateModularity(resolution: number): number {
|
|
417
|
+
if (this.totalWeight === 0) {
|
|
418
|
+
return 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let modularity = 0;
|
|
422
|
+
|
|
423
|
+
// Sum over all communities
|
|
424
|
+
const communityInternalWeights = new Map<number, number>();
|
|
425
|
+
|
|
426
|
+
// Calculate internal weights for each community
|
|
427
|
+
for (const node of this.graph.nodes()) {
|
|
428
|
+
const nodeId = node.id;
|
|
429
|
+
const community = this.communities.get(nodeId) ?? 0;
|
|
430
|
+
|
|
431
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
432
|
+
if (this.communities.get(neighbor) === community) {
|
|
433
|
+
const edge = this.graph.getEdge(nodeId, neighbor);
|
|
434
|
+
const weight = edge?.weight ?? 1;
|
|
435
|
+
communityInternalWeights.set(community, (communityInternalWeights.get(community) ?? 0) + weight);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Calculate modularity
|
|
441
|
+
for (const [community, internalWeight] of communityInternalWeights) {
|
|
442
|
+
const communityWeight = this.communityWeights.get(community) ?? 0;
|
|
443
|
+
// For undirected graphs, internal weights are counted twice
|
|
444
|
+
const aIn = this.graph.isDirected ? internalWeight : internalWeight / 2;
|
|
445
|
+
const aTotal = communityWeight;
|
|
446
|
+
|
|
447
|
+
// Modularity formula: sum of (fraction of edges within community - expected fraction)
|
|
448
|
+
const actualFraction = aIn / this.totalWeight;
|
|
449
|
+
const expectedFraction = resolution * Math.pow(aTotal / (2 * this.totalWeight), 2);
|
|
450
|
+
modularity += actualFraction - expectedFraction;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return modularity;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get pruning statistics
|
|
458
|
+
* @returns Statistics about nodes pruned during optimization
|
|
459
|
+
*/
|
|
460
|
+
public getPruningStats(): PruningStats {
|
|
461
|
+
return { ...this.pruningStats };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Optimized Louvain algorithm with automatic optimization selection
|
|
467
|
+
* @param graph - The input graph to detect communities in
|
|
468
|
+
* @param options - Algorithm configuration options
|
|
469
|
+
* @returns Community detection result with communities, modularity, and iterations
|
|
470
|
+
*/
|
|
471
|
+
export function louvainOptimized(graph: Graph, options: OptimizedLouvainOptions = {}): CommunityResult {
|
|
472
|
+
const optimizer = new OptimizedLouvain(graph);
|
|
473
|
+
return optimizer.detectCommunities(options);
|
|
474
|
+
}
|