@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.
Files changed (237) hide show
  1. package/README.md +820 -235
  2. package/dist/algorithms.d.ts +42 -2
  3. package/dist/algorithms.js +11099 -2
  4. package/dist/algorithms.js.map +1 -1
  5. package/dist/src/algorithms/centrality/betweenness.d.ts +4 -0
  6. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  7. package/dist/src/algorithms/centrality/betweenness.js +124 -170
  8. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  9. package/dist/src/algorithms/centrality/closeness.d.ts +26 -1
  10. package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
  11. package/dist/src/algorithms/centrality/closeness.js +56 -149
  12. package/dist/src/algorithms/centrality/closeness.js.map +1 -1
  13. package/dist/src/algorithms/centrality/degree.js +3 -3
  14. package/dist/src/algorithms/centrality/degree.js.map +1 -1
  15. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +39 -0
  16. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
  17. package/dist/src/algorithms/centrality/delta-pagerank-simple.js +286 -0
  18. package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
  19. package/dist/src/algorithms/centrality/delta-pagerank.d.ts +80 -0
  20. package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
  21. package/dist/src/algorithms/centrality/delta-pagerank.js +294 -0
  22. package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
  23. package/dist/src/algorithms/centrality/index.d.ts +2 -0
  24. package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
  25. package/dist/src/algorithms/centrality/index.js +1 -0
  26. package/dist/src/algorithms/centrality/index.js.map +1 -1
  27. package/dist/src/algorithms/centrality/pagerank.d.ts +17 -0
  28. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
  29. package/dist/src/algorithms/centrality/pagerank.js +45 -0
  30. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  31. package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
  32. package/dist/src/algorithms/community/girvan-newman.js +47 -54
  33. package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
  34. package/dist/src/algorithms/community/index.d.ts +2 -0
  35. package/dist/src/algorithms/community/index.d.ts.map +1 -1
  36. package/dist/src/algorithms/community/index.js.map +1 -1
  37. package/dist/src/algorithms/community/label-propagation.d.ts +13 -3
  38. package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
  39. package/dist/src/algorithms/community/label-propagation.js +50 -35
  40. package/dist/src/algorithms/community/label-propagation.js.map +1 -1
  41. package/dist/src/algorithms/community/leiden.d.ts +3 -2
  42. package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
  43. package/dist/src/algorithms/community/leiden.js +34 -39
  44. package/dist/src/algorithms/community/leiden.js.map +1 -1
  45. package/dist/src/algorithms/community/louvain-optimized.d.ts +112 -0
  46. package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
  47. package/dist/src/algorithms/community/louvain-optimized.js +303 -0
  48. package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
  49. package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
  50. package/dist/src/algorithms/community/louvain.js +17 -76
  51. package/dist/src/algorithms/community/louvain.js.map +1 -1
  52. package/dist/src/algorithms/community/modularity-utils.d.ts +59 -0
  53. package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
  54. package/dist/src/algorithms/community/modularity-utils.js +112 -0
  55. package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
  56. package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
  57. package/dist/src/algorithms/matching/bipartite.js +8 -40
  58. package/dist/src/algorithms/matching/bipartite.js.map +1 -1
  59. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
  60. package/dist/src/algorithms/shortest-path/bellman-ford.js +1 -12
  61. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  62. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +37 -0
  63. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
  64. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +193 -0
  65. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
  66. package/dist/src/algorithms/shortest-path/dijkstra.d.ts +5 -2
  67. package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
  68. package/dist/src/algorithms/shortest-path/dijkstra.js +15 -14
  69. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  70. package/dist/src/algorithms/traversal/bfs-unified.d.ts +29 -0
  71. package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
  72. package/dist/src/algorithms/traversal/bfs-unified.js +333 -0
  73. package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
  74. package/dist/src/algorithms/traversal/bfs-variants.d.ts +54 -0
  75. package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
  76. package/dist/src/algorithms/traversal/bfs-variants.js +368 -0
  77. package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
  78. package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
  79. package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
  80. package/dist/src/algorithms/traversal/bfs.js +5 -182
  81. package/dist/src/algorithms/traversal/bfs.js.map +1 -1
  82. package/dist/src/benchmark-all-algorithms.d.ts +23 -0
  83. package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
  84. package/dist/src/benchmark-all-algorithms.js +462 -0
  85. package/dist/src/benchmark-all-algorithms.js.map +1 -0
  86. package/dist/src/clustering/hierarchical.d.ts +16 -15
  87. package/dist/src/clustering/hierarchical.d.ts.map +1 -1
  88. package/dist/src/clustering/hierarchical.js +209 -223
  89. package/dist/src/clustering/hierarchical.js.map +1 -1
  90. package/dist/src/clustering/index.d.ts +4 -2
  91. package/dist/src/clustering/index.d.ts.map +1 -1
  92. package/dist/src/clustering/index.js +2 -2
  93. package/dist/src/clustering/index.js.map +1 -1
  94. package/dist/src/clustering/k-core.d.ts +16 -15
  95. package/dist/src/clustering/k-core.d.ts.map +1 -1
  96. package/dist/src/clustering/k-core.js +57 -21
  97. package/dist/src/clustering/k-core.js.map +1 -1
  98. package/dist/src/clustering/spectral.d.ts +8 -0
  99. package/dist/src/clustering/spectral.d.ts.map +1 -1
  100. package/dist/src/clustering/spectral.js +158 -35
  101. package/dist/src/clustering/spectral.js.map +1 -1
  102. package/dist/src/core/graph.js +4 -4
  103. package/dist/src/core/graph.js.map +1 -1
  104. package/dist/src/flow/ford-fulkerson.d.ts +17 -11
  105. package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
  106. package/dist/src/flow/ford-fulkerson.js +141 -217
  107. package/dist/src/flow/ford-fulkerson.js.map +1 -1
  108. package/dist/src/flow/min-cut.d.ts +6 -5
  109. package/dist/src/flow/min-cut.d.ts.map +1 -1
  110. package/dist/src/flow/min-cut.js +17 -8
  111. package/dist/src/flow/min-cut.js.map +1 -1
  112. package/dist/src/index.d.ts +2 -0
  113. package/dist/src/index.d.ts.map +1 -1
  114. package/dist/src/index.js +6 -0
  115. package/dist/src/index.js.map +1 -1
  116. package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
  117. package/dist/src/link-prediction/adamic-adar.js +17 -26
  118. package/dist/src/link-prediction/adamic-adar.js.map +1 -1
  119. package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
  120. package/dist/src/link-prediction/common-neighbors.js +7 -11
  121. package/dist/src/link-prediction/common-neighbors.js.map +1 -1
  122. package/dist/src/optimized/bit-packed.d.ts +143 -0
  123. package/dist/src/optimized/bit-packed.d.ts.map +1 -0
  124. package/dist/src/optimized/bit-packed.js +292 -0
  125. package/dist/src/optimized/bit-packed.js.map +1 -0
  126. package/dist/src/optimized/csr-graph.d.ts +86 -0
  127. package/dist/src/optimized/csr-graph.d.ts.map +1 -0
  128. package/dist/src/optimized/csr-graph.js +341 -0
  129. package/dist/src/optimized/csr-graph.js.map +1 -0
  130. package/dist/src/optimized/direction-optimized-bfs.d.ts +74 -0
  131. package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
  132. package/dist/src/optimized/direction-optimized-bfs.js +223 -0
  133. package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
  134. package/dist/src/optimized/graph-adapter.d.ts +62 -0
  135. package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
  136. package/dist/src/optimized/graph-adapter.js +146 -0
  137. package/dist/src/optimized/graph-adapter.js.map +1 -0
  138. package/dist/src/optimized/index.d.ts +14 -0
  139. package/dist/src/optimized/index.d.ts.map +1 -0
  140. package/dist/src/optimized/index.js +28 -0
  141. package/dist/src/optimized/index.js.map +1 -0
  142. package/dist/src/research/grsbm.d.ts +83 -0
  143. package/dist/src/research/grsbm.d.ts.map +1 -0
  144. package/dist/src/research/grsbm.js +403 -0
  145. package/dist/src/research/grsbm.js.map +1 -0
  146. package/dist/src/research/index.d.ts +12 -0
  147. package/dist/src/research/index.d.ts.map +1 -0
  148. package/dist/src/research/index.js +14 -0
  149. package/dist/src/research/index.js.map +1 -0
  150. package/dist/src/research/sync.d.ts +49 -0
  151. package/dist/src/research/sync.d.ts.map +1 -0
  152. package/dist/src/research/sync.js +316 -0
  153. package/dist/src/research/sync.js.map +1 -0
  154. package/dist/src/research/terahac.d.ts +65 -0
  155. package/dist/src/research/terahac.d.ts.map +1 -0
  156. package/dist/src/research/terahac.js +379 -0
  157. package/dist/src/research/terahac.js.map +1 -0
  158. package/dist/src/types/index.d.ts +8 -0
  159. package/dist/src/types/index.d.ts.map +1 -1
  160. package/dist/src/utils/algorithm-utilities.d.ts +2 -0
  161. package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
  162. package/dist/src/utils/algorithm-utilities.js +2 -0
  163. package/dist/src/utils/algorithm-utilities.js.map +1 -0
  164. package/dist/src/utils/graph-converters.d.ts +79 -0
  165. package/dist/src/utils/graph-converters.d.ts.map +1 -0
  166. package/dist/src/utils/graph-converters.js +196 -0
  167. package/dist/src/utils/graph-converters.js.map +1 -0
  168. package/dist/src/utils/graph-utilities.d.ts +62 -0
  169. package/dist/src/utils/graph-utilities.d.ts.map +1 -0
  170. package/dist/src/utils/graph-utilities.js +148 -0
  171. package/dist/src/utils/graph-utilities.js.map +1 -0
  172. package/dist/src/utils/index.d.ts +5 -0
  173. package/dist/src/utils/index.d.ts.map +1 -0
  174. package/dist/src/utils/index.js +5 -0
  175. package/dist/src/utils/index.js.map +1 -0
  176. package/dist/src/utils/math-utilities.d.ts +52 -0
  177. package/dist/src/utils/math-utilities.d.ts.map +1 -0
  178. package/dist/src/utils/math-utilities.js +123 -0
  179. package/dist/src/utils/math-utilities.js.map +1 -0
  180. package/dist/src/utils/matrix-utilities.d.ts +2 -0
  181. package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
  182. package/dist/src/utils/matrix-utilities.js +2 -0
  183. package/dist/src/utils/matrix-utilities.js.map +1 -0
  184. package/dist/src/utils/optimization-helpers.d.ts +33 -0
  185. package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
  186. package/dist/src/utils/optimization-helpers.js +47 -0
  187. package/dist/src/utils/optimization-helpers.js.map +1 -0
  188. package/package.json +28 -4
  189. package/src/algorithms/centrality/betweenness.ts +167 -199
  190. package/src/algorithms/centrality/closeness.ts +73 -191
  191. package/src/algorithms/centrality/degree.ts +3 -3
  192. package/src/algorithms/centrality/delta-pagerank-simple.ts +366 -0
  193. package/src/algorithms/centrality/delta-pagerank.ts +435 -0
  194. package/src/algorithms/centrality/index.ts +2 -0
  195. package/src/algorithms/centrality/pagerank.ts +66 -0
  196. package/src/algorithms/community/girvan-newman.ts +52 -59
  197. package/src/algorithms/community/index.ts +2 -0
  198. package/src/algorithms/community/label-propagation.ts +64 -35
  199. package/src/algorithms/community/leiden.ts +40 -41
  200. package/src/algorithms/community/louvain-optimized.ts +445 -0
  201. package/src/algorithms/community/louvain.ts +24 -104
  202. package/src/algorithms/community/modularity-utils.ts +146 -0
  203. package/src/algorithms/matching/bipartite.ts +8 -44
  204. package/src/algorithms/shortest-path/bellman-ford.ts +1 -14
  205. package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
  206. package/src/algorithms/shortest-path/dijkstra.ts +17 -15
  207. package/src/algorithms/traversal/bfs-unified.ts +433 -0
  208. package/src/algorithms/traversal/bfs-variants.ts +496 -0
  209. package/src/algorithms/traversal/bfs.ts +10 -228
  210. package/src/benchmark-all-algorithms.ts +548 -0
  211. package/src/clustering/hierarchical.ts +241 -251
  212. package/src/clustering/index.ts +4 -2
  213. package/src/clustering/k-core.ts +87 -39
  214. package/src/clustering/spectral.ts +185 -34
  215. package/src/core/graph.ts +4 -4
  216. package/src/flow/ford-fulkerson.ts +186 -254
  217. package/src/flow/min-cut.ts +21 -11
  218. package/src/index.ts +9 -0
  219. package/src/link-prediction/adamic-adar.ts +17 -30
  220. package/src/link-prediction/common-neighbors.ts +7 -16
  221. package/src/optimized/bit-packed.ts +340 -0
  222. package/src/optimized/csr-graph.ts +455 -0
  223. package/src/optimized/direction-optimized-bfs.ts +291 -0
  224. package/src/optimized/graph-adapter.ts +200 -0
  225. package/src/optimized/index.ts +36 -0
  226. package/src/research/grsbm.ts +580 -0
  227. package/src/research/index.ts +17 -0
  228. package/src/research/sync.ts +440 -0
  229. package/src/research/terahac.ts +514 -0
  230. package/src/types/index.ts +8 -0
  231. package/src/utils/algorithm-utilities.ts +2 -0
  232. package/src/utils/graph-converters.ts +241 -0
  233. package/src/utils/graph-utilities.ts +199 -0
  234. package/src/utils/index.ts +4 -0
  235. package/src/utils/math-utilities.ts +140 -0
  236. package/src/utils/matrix-utilities.ts +2 -0
  237. 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
- }