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