@graphty/algorithms 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +1259 -61
  2. package/dist/algorithms.d.ts +2 -0
  3. package/dist/algorithms.d.ts.map +1 -0
  4. package/dist/algorithms.js +2 -0
  5. package/dist/algorithms.js.map +1 -0
  6. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  7. package/dist/src/algorithms/centrality/betweenness.js +22 -7
  8. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  9. package/dist/src/algorithms/centrality/closeness.js +7 -7
  10. package/dist/src/algorithms/centrality/closeness.js.map +1 -1
  11. package/dist/src/algorithms/centrality/degree.js +1 -1
  12. package/dist/src/algorithms/centrality/degree.js.map +1 -1
  13. package/dist/src/algorithms/centrality/eigenvector.d.ts +27 -0
  14. package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -0
  15. package/dist/src/algorithms/centrality/eigenvector.js +113 -0
  16. package/dist/src/algorithms/centrality/eigenvector.js.map +1 -0
  17. package/dist/src/algorithms/centrality/hits.d.ts +35 -0
  18. package/dist/src/algorithms/centrality/hits.d.ts.map +1 -0
  19. package/dist/src/algorithms/centrality/hits.js +146 -0
  20. package/dist/src/algorithms/centrality/hits.js.map +1 -0
  21. package/dist/src/algorithms/centrality/index.d.ts +6 -0
  22. package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
  23. package/dist/src/algorithms/centrality/index.js +3 -0
  24. package/dist/src/algorithms/centrality/index.js.map +1 -1
  25. package/dist/src/algorithms/centrality/katz.d.ts +30 -0
  26. package/dist/src/algorithms/centrality/katz.d.ts.map +1 -0
  27. package/dist/src/algorithms/centrality/katz.js +85 -0
  28. package/dist/src/algorithms/centrality/katz.js.map +1 -0
  29. package/dist/src/algorithms/centrality/pagerank.js +3 -3
  30. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  31. package/dist/src/algorithms/community/girvan-newman.d.ts +23 -0
  32. package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -0
  33. package/dist/src/algorithms/community/girvan-newman.js +297 -0
  34. package/dist/src/algorithms/community/girvan-newman.js.map +1 -0
  35. package/dist/src/algorithms/community/index.d.ts +12 -0
  36. package/dist/src/algorithms/community/index.d.ts.map +1 -0
  37. package/dist/src/algorithms/community/index.js +12 -0
  38. package/dist/src/algorithms/community/index.js.map +1 -0
  39. package/dist/src/algorithms/community/label-propagation.d.ts +42 -0
  40. package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -0
  41. package/dist/src/algorithms/community/label-propagation.js +307 -0
  42. package/dist/src/algorithms/community/label-propagation.js.map +1 -0
  43. package/dist/src/algorithms/community/leiden.d.ts +33 -0
  44. package/dist/src/algorithms/community/leiden.d.ts.map +1 -0
  45. package/dist/src/algorithms/community/leiden.js +434 -0
  46. package/dist/src/algorithms/community/leiden.js.map +1 -0
  47. package/dist/src/algorithms/community/louvain.d.ts +19 -0
  48. package/dist/src/algorithms/community/louvain.d.ts.map +1 -0
  49. package/dist/src/algorithms/community/louvain.js +219 -0
  50. package/dist/src/algorithms/community/louvain.js.map +1 -0
  51. package/dist/src/algorithms/components/connected.js +7 -7
  52. package/dist/src/algorithms/components/connected.js.map +1 -1
  53. package/dist/src/algorithms/index.d.ts +7 -0
  54. package/dist/src/algorithms/index.d.ts.map +1 -1
  55. package/dist/src/algorithms/index.js +14 -0
  56. package/dist/src/algorithms/index.js.map +1 -1
  57. package/dist/src/algorithms/matching/bipartite.d.ts +37 -0
  58. package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -0
  59. package/dist/src/algorithms/matching/bipartite.js +127 -0
  60. package/dist/src/algorithms/matching/bipartite.js.map +1 -0
  61. package/dist/src/algorithms/matching/index.d.ts +8 -0
  62. package/dist/src/algorithms/matching/index.d.ts.map +1 -0
  63. package/dist/src/algorithms/matching/index.js +6 -0
  64. package/dist/src/algorithms/matching/index.js.map +1 -0
  65. package/dist/src/algorithms/matching/isomorphism.d.ts +32 -0
  66. package/dist/src/algorithms/matching/isomorphism.d.ts.map +1 -0
  67. package/dist/src/algorithms/matching/isomorphism.js +291 -0
  68. package/dist/src/algorithms/matching/isomorphism.js.map +1 -0
  69. package/dist/src/algorithms/mst/index.d.ts +4 -0
  70. package/dist/src/algorithms/mst/index.d.ts.map +1 -0
  71. package/dist/src/algorithms/mst/index.js +3 -0
  72. package/dist/src/algorithms/mst/index.js.map +1 -0
  73. package/dist/src/algorithms/mst/kruskal.d.ts +9 -0
  74. package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -0
  75. package/dist/src/algorithms/mst/kruskal.js +43 -0
  76. package/dist/src/algorithms/mst/kruskal.js.map +1 -0
  77. package/dist/src/algorithms/mst/prim.d.ts +5 -0
  78. package/dist/src/algorithms/mst/prim.d.ts.map +1 -0
  79. package/dist/src/algorithms/mst/prim.js +63 -0
  80. package/dist/src/algorithms/mst/prim.js.map +1 -0
  81. package/dist/src/algorithms/shortest-path/bellman-ford.js +2 -2
  82. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  83. package/dist/src/algorithms/shortest-path/dijkstra.js +5 -5
  84. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  85. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +14 -0
  86. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -0
  87. package/dist/src/algorithms/shortest-path/floyd-warshall.js +129 -0
  88. package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -0
  89. package/dist/src/algorithms/shortest-path/index.d.ts +2 -0
  90. package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -1
  91. package/dist/src/algorithms/shortest-path/index.js +1 -0
  92. package/dist/src/algorithms/shortest-path/index.js.map +1 -1
  93. package/dist/src/algorithms/traversal/bfs.js +2 -2
  94. package/dist/src/algorithms/traversal/bfs.js.map +1 -1
  95. package/dist/src/algorithms/traversal/dfs.js +11 -11
  96. package/dist/src/algorithms/traversal/dfs.js.map +1 -1
  97. package/dist/src/clustering/hierarchical.d.ts +48 -0
  98. package/dist/src/clustering/hierarchical.d.ts.map +1 -0
  99. package/dist/src/clustering/hierarchical.js +439 -0
  100. package/dist/src/clustering/hierarchical.js.map +1 -0
  101. package/dist/src/clustering/index.d.ts +7 -0
  102. package/dist/src/clustering/index.d.ts.map +1 -0
  103. package/dist/src/clustering/index.js +5 -0
  104. package/dist/src/clustering/index.js.map +1 -0
  105. package/dist/src/clustering/k-core.d.ts +63 -0
  106. package/dist/src/clustering/k-core.d.ts.map +1 -0
  107. package/dist/src/clustering/k-core.js +377 -0
  108. package/dist/src/clustering/k-core.js.map +1 -0
  109. package/dist/src/clustering/mcl.d.ts +36 -0
  110. package/dist/src/clustering/mcl.d.ts.map +1 -0
  111. package/dist/src/clustering/mcl.js +343 -0
  112. package/dist/src/clustering/mcl.js.map +1 -0
  113. package/dist/src/clustering/spectral.d.ts +28 -0
  114. package/dist/src/clustering/spectral.d.ts.map +1 -0
  115. package/dist/src/clustering/spectral.js +551 -0
  116. package/dist/src/clustering/spectral.js.map +1 -0
  117. package/dist/src/core/graph.d.ts +9 -0
  118. package/dist/src/core/graph.d.ts.map +1 -1
  119. package/dist/src/core/graph.js +31 -11
  120. package/dist/src/core/graph.js.map +1 -1
  121. package/dist/src/data-structures/priority-queue.js +0 -2
  122. package/dist/src/data-structures/priority-queue.js.map +1 -1
  123. package/dist/src/data-structures/union-find.js +2 -5
  124. package/dist/src/data-structures/union-find.js.map +1 -1
  125. package/dist/src/flow/ford-fulkerson.d.ts +53 -0
  126. package/dist/src/flow/ford-fulkerson.d.ts.map +1 -0
  127. package/dist/src/flow/ford-fulkerson.js +355 -0
  128. package/dist/src/flow/ford-fulkerson.js.map +1 -0
  129. package/dist/src/flow/index.d.ts +3 -0
  130. package/dist/src/flow/index.d.ts.map +1 -0
  131. package/dist/src/flow/index.js +3 -0
  132. package/dist/src/flow/index.js.map +1 -0
  133. package/dist/src/flow/min-cut.d.ts +49 -0
  134. package/dist/src/flow/min-cut.d.ts.map +1 -0
  135. package/dist/src/flow/min-cut.js +383 -0
  136. package/dist/src/flow/min-cut.js.map +1 -0
  137. package/dist/src/index.d.ts +2 -1
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +2 -0
  140. package/dist/src/index.js.map +1 -1
  141. package/dist/src/link-prediction/adamic-adar.d.ts +56 -0
  142. package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -0
  143. package/dist/src/link-prediction/adamic-adar.js +256 -0
  144. package/dist/src/link-prediction/adamic-adar.js.map +1 -0
  145. package/dist/src/link-prediction/common-neighbors.d.ts +50 -0
  146. package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -0
  147. package/dist/src/link-prediction/common-neighbors.js +155 -0
  148. package/dist/src/link-prediction/common-neighbors.js.map +1 -0
  149. package/dist/src/link-prediction/index.d.ts +7 -0
  150. package/dist/src/link-prediction/index.d.ts.map +1 -0
  151. package/dist/src/link-prediction/index.js +6 -0
  152. package/dist/src/link-prediction/index.js.map +1 -0
  153. package/dist/src/pathfinding/astar.d.ts +50 -0
  154. package/dist/src/pathfinding/astar.d.ts.map +1 -0
  155. package/dist/src/pathfinding/astar.js +180 -0
  156. package/dist/src/pathfinding/astar.js.map +1 -0
  157. package/dist/src/pathfinding/index.d.ts +3 -0
  158. package/dist/src/pathfinding/index.d.ts.map +1 -0
  159. package/dist/src/pathfinding/index.js +3 -0
  160. package/dist/src/pathfinding/index.js.map +1 -0
  161. package/dist/src/pathfinding/utils.d.ts +18 -0
  162. package/dist/src/pathfinding/utils.d.ts.map +1 -0
  163. package/dist/src/pathfinding/utils.js +68 -0
  164. package/dist/src/pathfinding/utils.js.map +1 -0
  165. package/dist/src/research/grsbm.d.ts +83 -0
  166. package/dist/src/research/grsbm.d.ts.map +1 -0
  167. package/dist/src/research/grsbm.js +411 -0
  168. package/dist/src/research/grsbm.js.map +1 -0
  169. package/dist/src/research/index.d.ts +12 -0
  170. package/dist/src/research/index.d.ts.map +1 -0
  171. package/dist/src/research/index.js +14 -0
  172. package/dist/src/research/index.js.map +1 -0
  173. package/dist/src/research/sync.d.ts +49 -0
  174. package/dist/src/research/sync.d.ts.map +1 -0
  175. package/dist/src/research/sync.js +342 -0
  176. package/dist/src/research/sync.js.map +1 -0
  177. package/dist/src/research/terahac.d.ts +63 -0
  178. package/dist/src/research/terahac.d.ts.map +1 -0
  179. package/dist/src/research/terahac.js +375 -0
  180. package/dist/src/research/terahac.js.map +1 -0
  181. package/dist/src/types/index.d.ts +9 -0
  182. package/dist/src/types/index.d.ts.map +1 -1
  183. package/dist/src/utils/graphNode.d.ts +8 -0
  184. package/dist/src/utils/graphNode.d.ts.map +1 -0
  185. package/dist/src/utils/graphNode.js +2 -0
  186. package/dist/src/utils/graphNode.js.map +1 -0
  187. package/dist/src/utils/priorityQueue.d.ts +21 -0
  188. package/dist/src/utils/priorityQueue.d.ts.map +1 -0
  189. package/dist/src/utils/priorityQueue.js +101 -0
  190. package/dist/src/utils/priorityQueue.js.map +1 -0
  191. package/package.json +13 -2
  192. package/src/algorithms/centrality/betweenness.ts +25 -7
  193. package/src/algorithms/centrality/closeness.ts +7 -7
  194. package/src/algorithms/centrality/degree.ts +1 -1
  195. package/src/algorithms/centrality/eigenvector.ts +162 -0
  196. package/src/algorithms/centrality/hits.ts +210 -0
  197. package/src/algorithms/centrality/index.ts +6 -0
  198. package/src/algorithms/centrality/katz.ts +139 -0
  199. package/src/algorithms/centrality/pagerank.ts +3 -3
  200. package/src/algorithms/community/girvan-newman.ts +372 -0
  201. package/src/algorithms/community/index.ts +13 -0
  202. package/src/algorithms/community/label-propagation.ts +392 -0
  203. package/src/algorithms/community/leiden.ts +572 -0
  204. package/src/algorithms/community/louvain.ts +298 -0
  205. package/src/algorithms/components/connected.ts +7 -7
  206. package/src/algorithms/index.ts +21 -0
  207. package/src/algorithms/matching/bipartite.ts +179 -0
  208. package/src/algorithms/matching/index.ts +8 -0
  209. package/src/algorithms/matching/isomorphism.ts +392 -0
  210. package/src/algorithms/mst/index.ts +4 -0
  211. package/src/algorithms/mst/kruskal.ts +61 -0
  212. package/src/algorithms/mst/prim.ts +80 -0
  213. package/src/algorithms/shortest-path/bellman-ford.ts +2 -2
  214. package/src/algorithms/shortest-path/dijkstra.ts +5 -5
  215. package/src/algorithms/shortest-path/floyd-warshall.ts +170 -0
  216. package/src/algorithms/shortest-path/index.ts +2 -0
  217. package/src/algorithms/traversal/bfs.ts +2 -2
  218. package/src/algorithms/traversal/dfs.ts +11 -11
  219. package/src/clustering/hierarchical.ts +555 -0
  220. package/src/clustering/index.ts +6 -0
  221. package/src/clustering/k-core.ts +460 -0
  222. package/src/clustering/mcl.ts +454 -0
  223. package/src/clustering/spectral.ts +672 -0
  224. package/src/core/graph.ts +34 -6
  225. package/src/data-structures/union-find.ts +2 -2
  226. package/src/flow/ford-fulkerson.ts +459 -0
  227. package/src/flow/index.ts +2 -0
  228. package/src/flow/min-cut.ts +482 -0
  229. package/src/index.ts +5 -0
  230. package/src/link-prediction/adamic-adar.ts +353 -0
  231. package/src/link-prediction/common-neighbors.ts +252 -0
  232. package/src/link-prediction/index.ts +20 -0
  233. package/src/pathfinding/astar.ts +229 -0
  234. package/src/pathfinding/index.ts +2 -0
  235. package/src/pathfinding/utils.ts +87 -0
  236. package/src/research/grsbm.ts +589 -0
  237. package/src/research/index.ts +17 -0
  238. package/src/research/sync.ts +469 -0
  239. package/src/research/terahac.ts +506 -0
  240. package/src/types/index.ts +11 -0
  241. package/src/utils/graphNode.ts +7 -0
  242. package/src/utils/priorityQueue.ts +120 -0
  243. package/dist/test/browser/basic.test.d.ts +0 -2
  244. package/dist/test/browser/basic.test.d.ts.map +0 -1
  245. package/dist/test/browser/basic.test.js +0 -70
  246. package/dist/test/browser/basic.test.js.map +0 -1
  247. package/dist/test/unit/bellman-ford.test.d.ts +0 -2
  248. package/dist/test/unit/bellman-ford.test.d.ts.map +0 -1
  249. package/dist/test/unit/bellman-ford.test.js +0 -231
  250. package/dist/test/unit/bellman-ford.test.js.map +0 -1
  251. package/dist/test/unit/betweenness-centrality.test.d.ts +0 -2
  252. package/dist/test/unit/betweenness-centrality.test.d.ts.map +0 -1
  253. package/dist/test/unit/betweenness-centrality.test.js +0 -311
  254. package/dist/test/unit/betweenness-centrality.test.js.map +0 -1
  255. package/dist/test/unit/bfs.test.d.ts +0 -2
  256. package/dist/test/unit/bfs.test.d.ts.map +0 -1
  257. package/dist/test/unit/bfs.test.js +0 -277
  258. package/dist/test/unit/bfs.test.js.map +0 -1
  259. package/dist/test/unit/closeness.test.d.ts +0 -2
  260. package/dist/test/unit/closeness.test.d.ts.map +0 -1
  261. package/dist/test/unit/closeness.test.js +0 -266
  262. package/dist/test/unit/closeness.test.js.map +0 -1
  263. package/dist/test/unit/connected-components.test.d.ts +0 -2
  264. package/dist/test/unit/connected-components.test.d.ts.map +0 -1
  265. package/dist/test/unit/connected-components.test.js +0 -355
  266. package/dist/test/unit/connected-components.test.js.map +0 -1
  267. package/dist/test/unit/degree-centrality.test.d.ts +0 -2
  268. package/dist/test/unit/degree-centrality.test.d.ts.map +0 -1
  269. package/dist/test/unit/degree-centrality.test.js +0 -260
  270. package/dist/test/unit/degree-centrality.test.js.map +0 -1
  271. package/dist/test/unit/dfs.test.d.ts +0 -2
  272. package/dist/test/unit/dfs.test.d.ts.map +0 -1
  273. package/dist/test/unit/dfs.test.js +0 -350
  274. package/dist/test/unit/dfs.test.js.map +0 -1
  275. package/dist/test/unit/dijkstra.test.d.ts +0 -2
  276. package/dist/test/unit/dijkstra.test.d.ts.map +0 -1
  277. package/dist/test/unit/dijkstra.test.js +0 -311
  278. package/dist/test/unit/dijkstra.test.js.map +0 -1
  279. package/dist/test/unit/graph.test.d.ts +0 -2
  280. package/dist/test/unit/graph.test.d.ts.map +0 -1
  281. package/dist/test/unit/graph.test.js +0 -175
  282. package/dist/test/unit/graph.test.js.map +0 -1
  283. package/dist/test/unit/pagerank.test.d.ts +0 -2
  284. package/dist/test/unit/pagerank.test.d.ts.map +0 -1
  285. package/dist/test/unit/pagerank.test.js +0 -281
  286. package/dist/test/unit/pagerank.test.js.map +0 -1
  287. package/dist/test/unit/priority-queue.test.d.ts +0 -2
  288. package/dist/test/unit/priority-queue.test.d.ts.map +0 -1
  289. package/dist/test/unit/priority-queue.test.js +0 -189
  290. package/dist/test/unit/priority-queue.test.js.map +0 -1
  291. package/dist/test/unit/union-find.test.d.ts +0 -2
  292. package/dist/test/unit/union-find.test.d.ts.map +0 -1
  293. package/dist/test/unit/union-find.test.js +0 -245
  294. package/dist/test/unit/union-find.test.js.map +0 -1
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Leiden Algorithm for Community Detection
3
+ *
4
+ * An improved version of the Louvain algorithm that guarantees
5
+ * well-connected communities and provides better quality partitions.
6
+ *
7
+ * Reference: Traag, V.A., Waltman, L. & van Eck, N.J. (2019)
8
+ * "From Louvain to Leiden: guaranteeing well-connected communities"
9
+ */
10
+
11
+ // Louvain import removed - not used in this implementation
12
+
13
+ export interface LeidenOptions {
14
+ resolution?: number;
15
+ randomSeed?: number;
16
+ maxIterations?: number;
17
+ threshold?: number;
18
+ }
19
+
20
+ export interface LeidenResult {
21
+ communities: Map<string, number>;
22
+ modularity: number;
23
+ iterations: number;
24
+ }
25
+
26
+ /**
27
+ * Leiden algorithm for community detection
28
+ * Improves upon Louvain by ensuring well-connected communities
29
+ *
30
+ * @param graph - Undirected weighted graph
31
+ * @param options - Algorithm options
32
+ * @returns Community assignments and modularity
33
+ *
34
+ * Time Complexity: O(m) per iteration, typically O(m log m) total
35
+ * Space Complexity: O(n + m)
36
+ */
37
+ export function leiden(
38
+ graph: Map<string, Map<string, number>>,
39
+ options: LeidenOptions = {},
40
+ ): LeidenResult {
41
+ const {
42
+ resolution = 1.0,
43
+ randomSeed = 42,
44
+ maxIterations = 100,
45
+ threshold = 1e-7,
46
+ } = options;
47
+
48
+ // Handle empty graph
49
+ if (graph.size === 0) {
50
+ return {
51
+ communities: new Map(),
52
+ modularity: 0,
53
+ iterations: 0,
54
+ };
55
+ }
56
+
57
+ // Initialize random number generator
58
+ let seed = randomSeed;
59
+ const random = (): number => {
60
+ seed = ((seed * 1664525) + 1013904223) % 2147483647;
61
+ return seed / 2147483647;
62
+ };
63
+
64
+ // Calculate total weight
65
+ let totalWeight = 0;
66
+ const degrees = new Map<string, number>();
67
+
68
+ for (const [node, neighbors] of graph) {
69
+ let degree = 0;
70
+ for (const weight of neighbors.values()) {
71
+ degree += weight;
72
+ totalWeight += weight;
73
+ }
74
+ degrees.set(node, degree);
75
+ }
76
+ totalWeight /= 2; // Each edge counted twice
77
+
78
+ // Initialize communities - each node in its own community
79
+ const communities = new Map<string, number>();
80
+ const nodes = Array.from(graph.keys());
81
+ nodes.forEach((node, i) => communities.set(node, i));
82
+
83
+ let modularity = calculateModularity(graph, communities, degrees, totalWeight, resolution);
84
+ let bestModularity = modularity;
85
+ let bestCommunities = new Map(communities);
86
+ let iterations = 0;
87
+
88
+ // Main Leiden loop
89
+ while (iterations < maxIterations) {
90
+ iterations++;
91
+ let improved = false;
92
+
93
+ // Phase 1: Local moving of nodes (fast)
94
+ const nodeOrder = [... nodes];
95
+ shuffle(nodeOrder, random);
96
+
97
+ for (const node of nodeOrder) {
98
+ const currentCommunity = communities.get(node);
99
+ if (currentCommunity === undefined) {
100
+ continue;
101
+ }
102
+
103
+ const neighborCommunities = getNeighborCommunities(node, graph, communities);
104
+
105
+ let bestCommunity = currentCommunity;
106
+ let bestGain = 0;
107
+
108
+ // Try moving to each neighbor community
109
+ for (const [community] of neighborCommunities) {
110
+ if (community === currentCommunity) {
111
+ continue;
112
+ }
113
+
114
+ const gain = calculateModularityGain(
115
+ node, community, graph, communities, degrees, totalWeight, resolution,
116
+ );
117
+
118
+ if (gain > bestGain) {
119
+ bestGain = gain;
120
+ bestCommunity = community;
121
+ }
122
+ }
123
+
124
+ // Move node if beneficial
125
+ if (bestCommunity !== currentCommunity) {
126
+ communities.set(node, bestCommunity);
127
+ modularity += bestGain;
128
+ improved = true;
129
+ }
130
+ }
131
+
132
+ // Phase 2: Refinement (Leiden improvement over Louvain)
133
+ // Create aggregate network based on current partition
134
+ createAggregateNetwork(graph, communities);
135
+
136
+ // Refine partition using aggregate network
137
+ const subsetPartition = refinePartition(
138
+ graph, communities,
139
+ );
140
+
141
+ // Apply refined partition
142
+ for (const [node, newCommunity] of subsetPartition) {
143
+ communities.set(node, newCommunity);
144
+ }
145
+
146
+ // Recalculate modularity
147
+ modularity = calculateModularity(graph, communities, degrees, totalWeight, resolution);
148
+
149
+ // Check if we've improved
150
+ if (modularity > bestModularity + threshold) {
151
+ bestModularity = modularity;
152
+ bestCommunities = new Map(communities);
153
+ improved = true;
154
+ }
155
+
156
+ if (!improved) {
157
+ break;
158
+ }
159
+
160
+ // Phase 3: Aggregate network (create super-nodes)
161
+ const aggregated = aggregateCommunities(graph, communities);
162
+ if (aggregated.graph.size === graph.size) {
163
+ break;
164
+ } // No aggregation possible
165
+
166
+ // Continue with aggregated network
167
+ const {graph: newGraph} = aggregated;
168
+ graph = newGraph;
169
+ communities.clear();
170
+ let communityId = 0;
171
+ for (const node of graph.keys()) {
172
+ communities.set(node, communityId++);
173
+ }
174
+ }
175
+
176
+ // Map back to original nodes
177
+ const finalCommunities = new Map<string, number>();
178
+ for (const [node, community] of bestCommunities) {
179
+ finalCommunities.set(node, community);
180
+ }
181
+
182
+ // Renumber communities consecutively
183
+ const communityRenumber = new Map<number, number>();
184
+ let newId = 0;
185
+ for (const community of new Set(finalCommunities.values())) {
186
+ communityRenumber.set(community, newId++);
187
+ }
188
+ for (const [node, community] of finalCommunities) {
189
+ const newCommunityId = communityRenumber.get(community);
190
+ if (newCommunityId !== undefined) {
191
+ finalCommunities.set(node, newCommunityId);
192
+ }
193
+ }
194
+
195
+ return {
196
+ communities: finalCommunities,
197
+ modularity: bestModularity,
198
+ iterations,
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Calculate modularity of a partition
204
+ */
205
+ function calculateModularity(
206
+ graph: Map<string, Map<string, number>>,
207
+ communities: Map<string, number>,
208
+ degrees: Map<string, number>,
209
+ totalWeight: number,
210
+ resolution: number,
211
+ ): number {
212
+ let modularity = 0;
213
+ const communityWeights = new Map<number, number>();
214
+
215
+ // Calculate internal weights for each community
216
+ for (const [node, neighbors] of graph) {
217
+ const nodeCommunity = communities.get(node);
218
+ if (nodeCommunity === undefined) {
219
+ continue;
220
+ }
221
+
222
+ for (const [neighbor, weight] of neighbors) {
223
+ const neighborCommunity = communities.get(neighbor);
224
+ if (neighborCommunity === undefined) {
225
+ continue;
226
+ }
227
+
228
+ if (nodeCommunity === neighborCommunity) {
229
+ modularity += weight;
230
+ }
231
+ }
232
+
233
+ const degree = degrees.get(node);
234
+ if (degree !== undefined) {
235
+ communityWeights.set(nodeCommunity, (communityWeights.get(nodeCommunity) ?? 0) + degree);
236
+ }
237
+ }
238
+
239
+ // Handle empty graph or zero weight
240
+ if (totalWeight === 0) {
241
+ return 0;
242
+ }
243
+
244
+ // Normalize and apply resolution
245
+ modularity /= (2 * totalWeight);
246
+
247
+ // Subtract expected edges
248
+ for (const weight of communityWeights.values()) {
249
+ modularity -= resolution * ((weight / (2 * totalWeight)) ** 2);
250
+ }
251
+
252
+ return modularity;
253
+ }
254
+
255
+ /**
256
+ * Get communities of neighbors
257
+ */
258
+ function getNeighborCommunities(
259
+ node: string,
260
+ graph: Map<string, Map<string, number>>,
261
+ communities: Map<string, number>,
262
+ ): Map<number, number> {
263
+ const neighborCommunities = new Map<number, number>();
264
+ const neighbors = graph.get(node);
265
+
266
+ if (neighbors) {
267
+ for (const [neighbor, weight] of neighbors) {
268
+ const community = communities.get(neighbor);
269
+ if (community !== undefined) {
270
+ neighborCommunities.set(community, (neighborCommunities.get(community) ?? 0) + weight);
271
+ }
272
+ }
273
+ }
274
+
275
+ return neighborCommunities;
276
+ }
277
+
278
+ /**
279
+ * Calculate modularity gain from moving a node to a community
280
+ */
281
+ function calculateModularityGain(
282
+ node: string,
283
+ targetCommunity: number,
284
+ graph: Map<string, Map<string, number>>,
285
+ communities: Map<string, number>,
286
+ degrees: Map<string, number>,
287
+ totalWeight: number,
288
+ resolution: number,
289
+ ): number {
290
+ const currentCommunity = communities.get(node);
291
+ const nodeDegree = degrees.get(node);
292
+ if (currentCommunity === undefined || nodeDegree === undefined) {
293
+ return 0;
294
+ }
295
+
296
+ // Weight of edges from node to target community
297
+ let weightToTarget = 0;
298
+ let weightToCurrent = 0;
299
+
300
+ const neighbors = graph.get(node);
301
+ if (neighbors) {
302
+ for (const [neighbor, weight] of neighbors) {
303
+ const neighborCommunity = communities.get(neighbor);
304
+ if (neighborCommunity === undefined) {
305
+ continue;
306
+ }
307
+
308
+ if (neighborCommunity === targetCommunity) {
309
+ weightToTarget += weight;
310
+ } else if (neighborCommunity === currentCommunity && neighbor !== node) {
311
+ weightToCurrent += weight;
312
+ }
313
+ }
314
+ }
315
+
316
+ // Calculate community degrees
317
+ let targetDegree = 0;
318
+ let currentDegree = 0;
319
+
320
+ for (const [n, c] of communities) {
321
+ if (c === targetCommunity && n !== node) {
322
+ const deg = degrees.get(n);
323
+ if (deg !== undefined) {
324
+ targetDegree += deg;
325
+ }
326
+ } else if (c === currentCommunity && n !== node) {
327
+ const deg = degrees.get(n);
328
+ if (deg !== undefined) {
329
+ currentDegree += deg;
330
+ }
331
+ }
332
+ }
333
+
334
+ // Modularity gain calculation
335
+ const m2 = 2 * totalWeight;
336
+ const gain = ((weightToTarget - weightToCurrent) / totalWeight) -
337
+ (resolution * nodeDegree * (targetDegree - currentDegree) / (m2 * m2));
338
+
339
+ return gain;
340
+ }
341
+
342
+ /**
343
+ * Create aggregate network where each community becomes a super-node
344
+ */
345
+ function createAggregateNetwork(
346
+ graph: Map<string, Map<string, number>>,
347
+ communities: Map<string, number>,
348
+ ): {
349
+ aggregateGraph: Map<number, Map<number, number>>;
350
+ nodeMapping: Map<string, number>;
351
+ } {
352
+ const aggregateGraph = new Map<number, Map<number, number>>();
353
+ const nodeMapping = new Map<string, number>();
354
+
355
+ // Create mapping from nodes to communities
356
+ for (const [node, community] of communities) {
357
+ nodeMapping.set(node, community);
358
+ if (!aggregateGraph.has(community)) {
359
+ aggregateGraph.set(community, new Map());
360
+ }
361
+ }
362
+
363
+ // Aggregate edges
364
+ for (const [node, neighbors] of graph) {
365
+ const sourceCommunity = communities.get(node);
366
+ if (sourceCommunity === undefined) {
367
+ continue;
368
+ }
369
+
370
+ for (const [neighbor, weight] of neighbors) {
371
+ const targetCommunity = communities.get(neighbor);
372
+ if (targetCommunity === undefined) {
373
+ continue;
374
+ }
375
+
376
+ const sourceNeighbors = aggregateGraph.get(sourceCommunity);
377
+ if (sourceNeighbors) {
378
+ const current = sourceNeighbors.get(targetCommunity) ?? 0;
379
+ sourceNeighbors.set(targetCommunity, current + weight);
380
+ }
381
+ }
382
+ }
383
+
384
+ return {aggregateGraph, nodeMapping};
385
+ }
386
+
387
+ /**
388
+ * Refine partition (Leiden-specific improvement)
389
+ * Ensures well-connected communities by considering subsets
390
+ */
391
+ function refinePartition(
392
+ originalGraph: Map<string, Map<string, number>>,
393
+ communities: Map<string, number>,
394
+ ): Map<string, number> {
395
+ const refined = new Map<string, number>();
396
+
397
+ // For each community, check if it should be split
398
+ const communityNodes = new Map<number, string[]>();
399
+ for (const [node, community] of communities) {
400
+ if (!communityNodes.has(community)) {
401
+ communityNodes.set(community, []);
402
+ }
403
+
404
+ const nodes = communityNodes.get(community);
405
+ if (nodes) {
406
+ nodes.push(node);
407
+ }
408
+ }
409
+
410
+ let newCommunityId = 0;
411
+
412
+ for (const [community, nodes] of communityNodes) {
413
+ if (nodes.length === 1) {
414
+ // Single node community
415
+ const singleNode = nodes[0];
416
+ if (singleNode) {
417
+ refined.set(singleNode, newCommunityId++);
418
+ }
419
+
420
+ continue;
421
+ }
422
+
423
+ // Check connectivity within community
424
+ const subgraph = new Map<string, Set<string>>();
425
+ for (const node of nodes) {
426
+ subgraph.set(node, new Set());
427
+ const neighbors = originalGraph.get(node);
428
+ if (neighbors) {
429
+ for (const [neighbor] of neighbors) {
430
+ if (communities.get(neighbor) === community) {
431
+ const nodeSet = subgraph.get(node);
432
+ if (nodeSet) {
433
+ nodeSet.add(neighbor);
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+
440
+ // Find connected components within community
441
+ const components = findConnectedComponents(subgraph);
442
+
443
+ // Assign new community IDs to components
444
+ for (const component of components) {
445
+ for (const node of component) {
446
+ refined.set(node, newCommunityId);
447
+ }
448
+ newCommunityId++;
449
+ }
450
+ }
451
+
452
+ return refined;
453
+ }
454
+
455
+ /**
456
+ * Find connected components in undirected graph
457
+ */
458
+ function findConnectedComponents(graph: Map<string, Set<string>>): Set<string>[] {
459
+ const visited = new Set<string>();
460
+ const components: Set<string>[] = [];
461
+
462
+ for (const node of graph.keys()) {
463
+ if (!visited.has(node)) {
464
+ const component = new Set<string>();
465
+ const queue = [node];
466
+
467
+ while (queue.length > 0) {
468
+ const current = queue.shift();
469
+ if (current === undefined || visited.has(current)) {
470
+ continue;
471
+ }
472
+
473
+ visited.add(current);
474
+ component.add(current);
475
+
476
+ const neighbors = graph.get(current);
477
+ if (neighbors) {
478
+ for (const neighbor of neighbors) {
479
+ if (!visited.has(neighbor)) {
480
+ queue.push(neighbor);
481
+ }
482
+ }
483
+ }
484
+ }
485
+
486
+ components.push(component);
487
+ }
488
+ }
489
+
490
+ return components;
491
+ }
492
+
493
+ /**
494
+ * Aggregate communities into super-nodes
495
+ */
496
+ function aggregateCommunities(
497
+ graph: Map<string, Map<string, number>>,
498
+ communities: Map<string, number>,
499
+ ): {
500
+ graph: Map<string, Map<string, number>>;
501
+ mapping: Map<string, string>;
502
+ } {
503
+ const aggregated = new Map<string, Map<string, number>>();
504
+ const mapping = new Map<string, string>();
505
+
506
+ // Create super-nodes
507
+ const communityNodes = new Map<number, string>();
508
+ for (const community of new Set(communities.values())) {
509
+ const superNode = `super_${String(community)}`;
510
+ communityNodes.set(community, superNode);
511
+ aggregated.set(superNode, new Map());
512
+ }
513
+
514
+ // Map original nodes to super-nodes
515
+ for (const [node, community] of communities) {
516
+ const superNode = communityNodes.get(community);
517
+ if (superNode !== undefined) {
518
+ mapping.set(node, superNode);
519
+ }
520
+ }
521
+
522
+ // Aggregate edges
523
+ for (const [node, neighbors] of graph) {
524
+ const sourceCommunity = communities.get(node);
525
+ if (sourceCommunity === undefined) {
526
+ continue;
527
+ }
528
+
529
+ const sourceSuper = communityNodes.get(sourceCommunity);
530
+ if (sourceSuper === undefined) {
531
+ continue;
532
+ }
533
+
534
+ for (const [neighbor, weight] of neighbors) {
535
+ const targetCommunity = communities.get(neighbor);
536
+ if (targetCommunity === undefined) {
537
+ continue;
538
+ }
539
+
540
+ const targetSuper = communityNodes.get(targetCommunity);
541
+ if (targetSuper === undefined) {
542
+ continue;
543
+ }
544
+
545
+ if (sourceSuper !== targetSuper) {
546
+ const sourceNeighbors = aggregated.get(sourceSuper);
547
+ if (sourceNeighbors) {
548
+ const current = sourceNeighbors.get(targetSuper) ?? 0;
549
+ sourceNeighbors.set(targetSuper, current + weight);
550
+ }
551
+ }
552
+ }
553
+ }
554
+
555
+ return {graph: aggregated, mapping};
556
+ }
557
+
558
+ /**
559
+ * Fisher-Yates shuffle
560
+ */
561
+ function shuffle(array: unknown[], random: () => number): void {
562
+ for (let i = array.length - 1; i > 0; i--) {
563
+ const j = Math.floor(random() * (i + 1));
564
+ const temp = array[i];
565
+ const swapItem = array[j];
566
+ if (temp !== undefined && swapItem !== undefined) {
567
+ array[i] = swapItem;
568
+ array[j] = temp;
569
+ }
570
+ }
571
+ }
572
+