@graphty/algorithms 1.2.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/README.md +375 -276
  2. package/dist/algorithms.d.ts +42 -2
  3. package/dist/algorithms.js +11402 -2
  4. package/dist/algorithms.js.map +1 -1
  5. package/dist/src/algorithms/centrality/betweenness.d.ts +14 -0
  6. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  7. package/dist/src/algorithms/centrality/betweenness.js +144 -169
  8. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  9. package/dist/src/algorithms/centrality/closeness.d.ts +35 -1
  10. package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
  11. package/dist/src/algorithms/centrality/closeness.js +70 -149
  12. package/dist/src/algorithms/centrality/closeness.js.map +1 -1
  13. package/dist/src/algorithms/centrality/degree.d.ts +7 -0
  14. package/dist/src/algorithms/centrality/degree.d.ts.map +1 -1
  15. package/dist/src/algorithms/centrality/degree.js +12 -9
  16. package/dist/src/algorithms/centrality/degree.js.map +1 -1
  17. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +57 -0
  18. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
  19. package/dist/src/algorithms/centrality/delta-pagerank-simple.js +304 -0
  20. package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
  21. package/dist/src/algorithms/centrality/delta-pagerank.d.ts +106 -0
  22. package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
  23. package/dist/src/algorithms/centrality/delta-pagerank.js +320 -0
  24. package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
  25. package/dist/src/algorithms/centrality/eigenvector.d.ts +10 -3
  26. package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
  27. package/dist/src/algorithms/centrality/eigenvector.js +11 -4
  28. package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
  29. package/dist/src/algorithms/centrality/hits.d.ts +10 -3
  30. package/dist/src/algorithms/centrality/hits.d.ts.map +1 -1
  31. package/dist/src/algorithms/centrality/hits.js +11 -4
  32. package/dist/src/algorithms/centrality/hits.js.map +1 -1
  33. package/dist/src/algorithms/centrality/index.d.ts +3 -1
  34. package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
  35. package/dist/src/algorithms/centrality/index.js +2 -1
  36. package/dist/src/algorithms/centrality/index.js.map +1 -1
  37. package/dist/src/algorithms/centrality/katz.d.ts +10 -3
  38. package/dist/src/algorithms/centrality/katz.d.ts.map +1 -1
  39. package/dist/src/algorithms/centrality/katz.js +13 -8
  40. package/dist/src/algorithms/centrality/katz.js.map +1 -1
  41. package/dist/src/algorithms/centrality/pagerank.d.ts +31 -0
  42. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
  43. package/dist/src/algorithms/centrality/pagerank.js +61 -1
  44. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  45. package/dist/src/algorithms/community/girvan-newman.d.ts +2 -3
  46. package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
  47. package/dist/src/algorithms/community/girvan-newman.js +67 -57
  48. package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
  49. package/dist/src/algorithms/community/index.d.ts +2 -0
  50. package/dist/src/algorithms/community/index.d.ts.map +1 -1
  51. package/dist/src/algorithms/community/index.js.map +1 -1
  52. package/dist/src/algorithms/community/label-propagation.d.ts +11 -4
  53. package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
  54. package/dist/src/algorithms/community/label-propagation.js +60 -38
  55. package/dist/src/algorithms/community/label-propagation.js.map +1 -1
  56. package/dist/src/algorithms/community/leiden.d.ts +3 -3
  57. package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
  58. package/dist/src/algorithms/community/leiden.js +70 -44
  59. package/dist/src/algorithms/community/leiden.js.map +1 -1
  60. package/dist/src/algorithms/community/louvain-optimized.d.ts +149 -0
  61. package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
  62. package/dist/src/algorithms/community/louvain-optimized.js +338 -0
  63. package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
  64. package/dist/src/algorithms/community/louvain.d.ts +2 -3
  65. package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
  66. package/dist/src/algorithms/community/louvain.js +35 -81
  67. package/dist/src/algorithms/community/louvain.js.map +1 -1
  68. package/dist/src/algorithms/community/modularity-utils.d.ts +55 -0
  69. package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
  70. package/dist/src/algorithms/community/modularity-utils.js +108 -0
  71. package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
  72. package/dist/src/algorithms/components/connected.d.ts +23 -0
  73. package/dist/src/algorithms/components/connected.d.ts.map +1 -1
  74. package/dist/src/algorithms/components/connected.js +29 -3
  75. package/dist/src/algorithms/components/connected.js.map +1 -1
  76. package/dist/src/algorithms/matching/bipartite.d.ts +10 -0
  77. package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
  78. package/dist/src/algorithms/matching/bipartite.js +18 -40
  79. package/dist/src/algorithms/matching/bipartite.js.map +1 -1
  80. package/dist/src/algorithms/matching/index.d.ts.map +1 -1
  81. package/dist/src/algorithms/matching/index.js.map +1 -1
  82. package/dist/src/algorithms/matching/isomorphism.d.ts +8 -0
  83. package/dist/src/algorithms/matching/isomorphism.d.ts.map +1 -1
  84. package/dist/src/algorithms/matching/isomorphism.js +36 -2
  85. package/dist/src/algorithms/matching/isomorphism.js.map +1 -1
  86. package/dist/src/algorithms/mst/index.d.ts.map +1 -1
  87. package/dist/src/algorithms/mst/index.js.map +1 -1
  88. package/dist/src/algorithms/mst/kruskal.d.ts +14 -0
  89. package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -1
  90. package/dist/src/algorithms/mst/kruskal.js +17 -3
  91. package/dist/src/algorithms/mst/kruskal.js.map +1 -1
  92. package/dist/src/algorithms/mst/prim.d.ts +9 -0
  93. package/dist/src/algorithms/mst/prim.d.ts.map +1 -1
  94. package/dist/src/algorithms/mst/prim.js +9 -0
  95. package/dist/src/algorithms/mst/prim.js.map +1 -1
  96. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts +10 -0
  97. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
  98. package/dist/src/algorithms/shortest-path/bellman-ford.js +11 -12
  99. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  100. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +44 -0
  101. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
  102. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +195 -0
  103. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
  104. package/dist/src/algorithms/shortest-path/dijkstra.d.ts +20 -2
  105. package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
  106. package/dist/src/algorithms/shortest-path/dijkstra.js +30 -14
  107. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  108. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +17 -0
  109. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -1
  110. package/dist/src/algorithms/shortest-path/floyd-warshall.js +17 -0
  111. package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -1
  112. package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -1
  113. package/dist/src/algorithms/shortest-path/index.js.map +1 -1
  114. package/dist/src/algorithms/traversal/bfs-unified.d.ts +42 -0
  115. package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
  116. package/dist/src/algorithms/traversal/bfs-unified.js +370 -0
  117. package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
  118. package/dist/src/algorithms/traversal/bfs-variants.d.ts +77 -0
  119. package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
  120. package/dist/src/algorithms/traversal/bfs-variants.js +402 -0
  121. package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
  122. package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
  123. package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
  124. package/dist/src/algorithms/traversal/bfs.js +5 -182
  125. package/dist/src/algorithms/traversal/bfs.js.map +1 -1
  126. package/dist/src/algorithms/traversal/dfs.d.ts +10 -0
  127. package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -1
  128. package/dist/src/algorithms/traversal/dfs.js +48 -0
  129. package/dist/src/algorithms/traversal/dfs.js.map +1 -1
  130. package/dist/src/algorithms/traversal/index.d.ts.map +1 -1
  131. package/dist/src/algorithms/traversal/index.js.map +1 -1
  132. package/dist/src/benchmark-all-algorithms.d.ts +25 -0
  133. package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
  134. package/dist/src/benchmark-all-algorithms.js +491 -0
  135. package/dist/src/benchmark-all-algorithms.js.map +1 -0
  136. package/dist/src/clustering/hierarchical.d.ts +23 -15
  137. package/dist/src/clustering/hierarchical.d.ts.map +1 -1
  138. package/dist/src/clustering/hierarchical.js +226 -224
  139. package/dist/src/clustering/hierarchical.js.map +1 -1
  140. package/dist/src/clustering/index.d.ts +4 -2
  141. package/dist/src/clustering/index.d.ts.map +1 -1
  142. package/dist/src/clustering/index.js +2 -2
  143. package/dist/src/clustering/index.js.map +1 -1
  144. package/dist/src/clustering/k-core.d.ts +17 -19
  145. package/dist/src/clustering/k-core.d.ts.map +1 -1
  146. package/dist/src/clustering/k-core.js +68 -30
  147. package/dist/src/clustering/k-core.js.map +1 -1
  148. package/dist/src/clustering/mcl.d.ts +6 -0
  149. package/dist/src/clustering/mcl.d.ts.map +1 -1
  150. package/dist/src/clustering/mcl.js +34 -3
  151. package/dist/src/clustering/mcl.js.map +1 -1
  152. package/dist/src/clustering/spectral.d.ts +11 -0
  153. package/dist/src/clustering/spectral.d.ts.map +1 -1
  154. package/dist/src/clustering/spectral.js +99 -47
  155. package/dist/src/clustering/spectral.js.map +1 -1
  156. package/dist/src/core/graph.d.ts +45 -0
  157. package/dist/src/core/graph.d.ts.map +1 -1
  158. package/dist/src/core/graph.js +49 -5
  159. package/dist/src/core/graph.js.map +1 -1
  160. package/dist/src/data-structures/index.d.ts.map +1 -1
  161. package/dist/src/data-structures/index.js.map +1 -1
  162. package/dist/src/data-structures/priority-queue.d.ts +18 -1
  163. package/dist/src/data-structures/priority-queue.d.ts.map +1 -1
  164. package/dist/src/data-structures/priority-queue.js +26 -5
  165. package/dist/src/data-structures/priority-queue.js.map +1 -1
  166. package/dist/src/data-structures/union-find.d.ts +21 -0
  167. package/dist/src/data-structures/union-find.d.ts.map +1 -1
  168. package/dist/src/data-structures/union-find.js +21 -0
  169. package/dist/src/data-structures/union-find.js.map +1 -1
  170. package/dist/src/flow/ford-fulkerson.d.ts +20 -12
  171. package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
  172. package/dist/src/flow/ford-fulkerson.js +170 -217
  173. package/dist/src/flow/ford-fulkerson.js.map +1 -1
  174. package/dist/src/flow/min-cut.d.ts +6 -8
  175. package/dist/src/flow/min-cut.d.ts.map +1 -1
  176. package/dist/src/flow/min-cut.js +34 -12
  177. package/dist/src/flow/min-cut.js.map +1 -1
  178. package/dist/src/index.d.ts +3 -1
  179. package/dist/src/index.d.ts.map +1 -1
  180. package/dist/src/index.js +6 -1
  181. package/dist/src/index.js.map +1 -1
  182. package/dist/src/link-prediction/adamic-adar.d.ts +26 -0
  183. package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
  184. package/dist/src/link-prediction/adamic-adar.js +47 -30
  185. package/dist/src/link-prediction/adamic-adar.js.map +1 -1
  186. package/dist/src/link-prediction/common-neighbors.d.ts +21 -0
  187. package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
  188. package/dist/src/link-prediction/common-neighbors.js +31 -14
  189. package/dist/src/link-prediction/common-neighbors.js.map +1 -1
  190. package/dist/src/link-prediction/index.d.ts.map +1 -1
  191. package/dist/src/optimized/bit-packed.d.ts +193 -0
  192. package/dist/src/optimized/bit-packed.d.ts.map +1 -0
  193. package/dist/src/optimized/bit-packed.js +342 -0
  194. package/dist/src/optimized/bit-packed.js.map +1 -0
  195. package/dist/src/optimized/csr-graph.d.ts +149 -0
  196. package/dist/src/optimized/csr-graph.d.ts.map +1 -0
  197. package/dist/src/optimized/csr-graph.js +404 -0
  198. package/dist/src/optimized/csr-graph.js.map +1 -0
  199. package/dist/src/optimized/direction-optimized-bfs.d.ts +95 -0
  200. package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
  201. package/dist/src/optimized/direction-optimized-bfs.js +242 -0
  202. package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
  203. package/dist/src/optimized/graph-adapter.d.ts +111 -0
  204. package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
  205. package/dist/src/optimized/graph-adapter.js +194 -0
  206. package/dist/src/optimized/graph-adapter.js.map +1 -0
  207. package/dist/src/optimized/index.d.ts +14 -0
  208. package/dist/src/optimized/index.d.ts.map +1 -0
  209. package/dist/src/optimized/index.js +30 -0
  210. package/dist/src/optimized/index.js.map +1 -0
  211. package/dist/src/pathfinding/astar.d.ts +25 -9
  212. package/dist/src/pathfinding/astar.d.ts.map +1 -1
  213. package/dist/src/pathfinding/astar.js +25 -10
  214. package/dist/src/pathfinding/astar.js.map +1 -1
  215. package/dist/src/pathfinding/utils.d.ts +15 -6
  216. package/dist/src/pathfinding/utils.d.ts.map +1 -1
  217. package/dist/src/pathfinding/utils.js +19 -7
  218. package/dist/src/pathfinding/utils.js.map +1 -1
  219. package/dist/src/research/grsbm.d.ts +0 -1
  220. package/dist/src/research/grsbm.d.ts.map +1 -1
  221. package/dist/src/research/grsbm.js +28 -23
  222. package/dist/src/research/grsbm.js.map +1 -1
  223. package/dist/src/research/index.d.ts.map +1 -1
  224. package/dist/src/research/index.js.map +1 -1
  225. package/dist/src/research/sync.d.ts +0 -1
  226. package/dist/src/research/sync.d.ts.map +1 -1
  227. package/dist/src/research/sync.js +31 -36
  228. package/dist/src/research/sync.js.map +1 -1
  229. package/dist/src/research/terahac.d.ts +2 -1
  230. package/dist/src/research/terahac.d.ts.map +1 -1
  231. package/dist/src/research/terahac.js +39 -7
  232. package/dist/src/research/terahac.js.map +1 -1
  233. package/dist/src/types/index.d.ts +8 -0
  234. package/dist/src/types/index.d.ts.map +1 -1
  235. package/dist/src/utils/algorithm-utilities.d.ts +2 -0
  236. package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
  237. package/dist/src/utils/algorithm-utilities.js +2 -0
  238. package/dist/src/utils/algorithm-utilities.js.map +1 -0
  239. package/dist/src/utils/graph-converters.d.ts +110 -0
  240. package/dist/src/utils/graph-converters.d.ts.map +1 -0
  241. package/dist/src/utils/graph-converters.js +227 -0
  242. package/dist/src/utils/graph-converters.js.map +1 -0
  243. package/dist/src/utils/graph-utilities.d.ts +62 -0
  244. package/dist/src/utils/graph-utilities.d.ts.map +1 -0
  245. package/dist/src/utils/graph-utilities.js +148 -0
  246. package/dist/src/utils/graph-utilities.js.map +1 -0
  247. package/dist/src/utils/index.d.ts +5 -0
  248. package/dist/src/utils/index.d.ts.map +1 -0
  249. package/dist/src/utils/index.js +5 -0
  250. package/dist/src/utils/index.js.map +1 -0
  251. package/dist/src/utils/math-utilities.d.ts +63 -0
  252. package/dist/src/utils/math-utilities.d.ts.map +1 -0
  253. package/dist/src/utils/math-utilities.js +132 -0
  254. package/dist/src/utils/math-utilities.js.map +1 -0
  255. package/dist/src/utils/matrix-utilities.d.ts +2 -0
  256. package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
  257. package/dist/src/utils/matrix-utilities.js +2 -0
  258. package/dist/src/utils/matrix-utilities.js.map +1 -0
  259. package/dist/src/utils/optimization-helpers.d.ts +42 -0
  260. package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
  261. package/dist/src/utils/optimization-helpers.js +59 -0
  262. package/dist/src/utils/optimization-helpers.js.map +1 -0
  263. package/dist/src/utils/priorityQueue.d.ts +24 -0
  264. package/dist/src/utils/priorityQueue.d.ts.map +1 -1
  265. package/dist/src/utils/priorityQueue.js +34 -4
  266. package/dist/src/utils/priorityQueue.js.map +1 -1
  267. package/package.json +58 -39
  268. package/src/algorithms/centrality/betweenness.ts +185 -200
  269. package/src/algorithms/centrality/closeness.ts +93 -194
  270. package/src/algorithms/centrality/degree.ts +18 -22
  271. package/src/algorithms/centrality/delta-pagerank-simple.ts +374 -0
  272. package/src/algorithms/centrality/delta-pagerank.ts +458 -0
  273. package/src/algorithms/centrality/eigenvector.ts +14 -15
  274. package/src/algorithms/centrality/hits.ts +17 -17
  275. package/src/algorithms/centrality/index.ts +20 -13
  276. package/src/algorithms/centrality/katz.ts +17 -25
  277. package/src/algorithms/centrality/pagerank.ts +91 -16
  278. package/src/algorithms/community/girvan-newman.ts +90 -86
  279. package/src/algorithms/community/index.ts +6 -5
  280. package/src/algorithms/community/label-propagation.ts +69 -48
  281. package/src/algorithms/community/leiden.ts +88 -65
  282. package/src/algorithms/community/louvain-optimized.ts +474 -0
  283. package/src/algorithms/community/louvain.ts +52 -122
  284. package/src/algorithms/community/modularity-utils.ts +134 -0
  285. package/src/algorithms/components/connected.ts +40 -17
  286. package/src/algorithms/matching/bipartite.ts +24 -53
  287. package/src/algorithms/matching/index.ts +4 -4
  288. package/src/algorithms/matching/isomorphism.ts +53 -23
  289. package/src/algorithms/mst/index.ts +3 -4
  290. package/src/algorithms/mst/kruskal.ts +21 -6
  291. package/src/algorithms/mst/prim.ts +13 -4
  292. package/src/algorithms/shortest-path/bellman-ford.ts +16 -28
  293. package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
  294. package/src/algorithms/shortest-path/dijkstra.ts +38 -30
  295. package/src/algorithms/shortest-path/floyd-warshall.ts +21 -4
  296. package/src/algorithms/shortest-path/index.ts +5 -5
  297. package/src/algorithms/traversal/bfs-unified.ts +437 -0
  298. package/src/algorithms/traversal/bfs-variants.ts +520 -0
  299. package/src/algorithms/traversal/bfs.ts +5 -229
  300. package/src/algorithms/traversal/dfs.ts +61 -42
  301. package/src/algorithms/traversal/index.ts +3 -3
  302. package/src/benchmark-all-algorithms.ts +580 -0
  303. package/src/clustering/hierarchical.ts +268 -272
  304. package/src/clustering/index.ts +8 -6
  305. package/src/clustering/k-core.ts +93 -61
  306. package/src/clustering/mcl.ts +53 -23
  307. package/src/clustering/spectral.ts +130 -72
  308. package/src/core/graph.ts +58 -18
  309. package/src/data-structures/index.ts +2 -2
  310. package/src/data-structures/priority-queue.ts +36 -11
  311. package/src/data-structures/union-find.ts +22 -1
  312. package/src/flow/ford-fulkerson.ts +221 -262
  313. package/src/flow/min-cut.ts +59 -51
  314. package/src/index.ts +9 -2
  315. package/src/link-prediction/adamic-adar.ts +75 -74
  316. package/src/link-prediction/common-neighbors.ts +49 -49
  317. package/src/link-prediction/index.ts +1 -1
  318. package/src/optimized/bit-packed.ts +390 -0
  319. package/src/optimized/csr-graph.ts +518 -0
  320. package/src/optimized/direction-optimized-bfs.ts +310 -0
  321. package/src/optimized/graph-adapter.ts +248 -0
  322. package/src/optimized/index.ts +34 -0
  323. package/src/pathfinding/astar.ts +40 -25
  324. package/src/pathfinding/utils.ts +20 -13
  325. package/src/research/grsbm.ts +36 -39
  326. package/src/research/index.ts +4 -4
  327. package/src/research/sync.ts +41 -54
  328. package/src/research/terahac.ts +63 -21
  329. package/src/types/index.ts +8 -0
  330. package/src/utils/algorithm-utilities.ts +2 -0
  331. package/src/utils/graph-converters.ts +269 -0
  332. package/src/utils/graph-utilities.ts +173 -0
  333. package/src/utils/index.ts +4 -0
  334. package/src/utils/math-utilities.ts +149 -0
  335. package/src/utils/matrix-utilities.ts +2 -0
  336. package/src/utils/optimization-helpers.ts +87 -0
  337. package/src/utils/priorityQueue.ts +40 -6
@@ -6,6 +6,43 @@
6
6
  * structure in graphs.
7
7
  */
8
8
 
9
+ import type { Graph } from "../core/graph.js";
10
+
11
+ /**
12
+ * Convert Graph to adjacency set representation for clustering
13
+ * @param graph - The input graph to convert
14
+ * @returns Map of node IDs to sets of neighbor node IDs
15
+ */
16
+ function graphToAdjacencySet(graph: Graph): Map<string, Set<string>> {
17
+ const adjacency = new Map<string, Set<string>>();
18
+
19
+ // Initialize all nodes
20
+ for (const node of graph.nodes()) {
21
+ adjacency.set(String(node.id), new Set());
22
+ }
23
+
24
+ // Add edges
25
+ for (const edge of graph.edges()) {
26
+ const source = String(edge.source);
27
+ const target = String(edge.target);
28
+
29
+ const sourceSet = adjacency.get(source);
30
+ if (sourceSet) {
31
+ sourceSet.add(target);
32
+ }
33
+
34
+ // For undirected graphs, add reverse edge
35
+ if (!graph.isDirected) {
36
+ const targetSet = adjacency.get(target);
37
+ if (targetSet) {
38
+ targetSet.add(source);
39
+ }
40
+ }
41
+ }
42
+
43
+ return adjacency;
44
+ }
45
+
9
46
  export interface ClusterNode<T> {
10
47
  id: string;
11
48
  members: Set<T>;
@@ -27,11 +64,10 @@ export type LinkageMethod = "single" | "complete" | "average" | "ward";
27
64
 
28
65
  /**
29
66
  * Compute distance between two nodes based on graph structure
67
+ * @param graph - Adjacency map representation of the graph
68
+ * @param distances - Map to store computed distances between nodes
30
69
  */
31
- function computeGraphDistance<T>(
32
- graph: Map<T, Set<T>>,
33
- distances: Map<T, Map<T, number>>,
34
- ): void {
70
+ function computeGraphDistance<T>(graph: Map<T, Set<T>>, distances: Map<T, Map<T, number>>): void {
35
71
  // Use BFS to compute shortest path distances
36
72
  for (const start of graph.keys()) {
37
73
  const dist = new Map<T, number>();
@@ -49,11 +85,7 @@ function computeGraphDistance<T>(
49
85
  if (neighbors) {
50
86
  for (const neighbor of neighbors) {
51
87
  if (!dist.has(neighbor)) {
52
- const nodeDistance = dist.get(node);
53
- if (nodeDistance !== undefined) {
54
- dist.set(neighbor, nodeDistance + 1);
55
- }
56
-
88
+ dist.set(neighbor, (dist.get(node) ?? 0) + 1);
57
89
  queue.push(neighbor);
58
90
  }
59
91
  }
@@ -65,55 +97,65 @@ function computeGraphDistance<T>(
65
97
  }
66
98
 
67
99
  /**
68
- * Compute distance between clusters based on linkage method
100
+ * Compute distance between two clusters based on linkage method
101
+ * @param cluster1 - First cluster node
102
+ * @param cluster2 - Second cluster node
103
+ * @param distances - Precomputed distance matrix between nodes
104
+ * @param linkage - Linkage method to use for computing cluster distance
105
+ * @returns The distance between the two clusters
69
106
  */
70
107
  function clusterDistance<T>(
71
- cluster1: Set<T>,
72
- cluster2: Set<T>,
108
+ cluster1: ClusterNode<T>,
109
+ cluster2: ClusterNode<T>,
73
110
  distances: Map<T, Map<T, number>>,
74
- method: LinkageMethod,
111
+ linkage: LinkageMethod,
75
112
  ): number {
76
- if (cluster1.size === 0 || cluster2.size === 0) {
77
- return Infinity;
78
- }
79
-
80
113
  const allDistances: number[] = [];
81
114
 
82
- for (const node1 of cluster1) {
83
- for (const node2 of cluster2) {
84
- const dist = distances.get(node1)?.get(node2) ?? Infinity;
85
- allDistances.push(dist);
115
+ for (const node1 of cluster1.members) {
116
+ const node1Distances = distances.get(node1);
117
+ if (!node1Distances) {
118
+ continue;
119
+ }
120
+
121
+ for (const node2 of cluster2.members) {
122
+ const dist = node1Distances.get(node2);
123
+ if (dist !== undefined) {
124
+ allDistances.push(dist);
125
+ }
86
126
  }
87
127
  }
88
128
 
89
- switch (method) {
129
+ if (allDistances.length === 0) {
130
+ return Infinity;
131
+ }
132
+
133
+ switch (linkage) {
90
134
  case "single":
91
- return Math.min(... allDistances);
135
+ return Math.min(...allDistances);
92
136
  case "complete":
93
- return Math.max(... allDistances);
137
+ return Math.max(...allDistances);
94
138
  case "average":
95
139
  return allDistances.reduce((a, b) => a + b, 0) / allDistances.length;
96
- case "ward":
97
- // Ward's method minimizes within-cluster variance
98
- // For simplicity, we use average here
99
- return allDistances.reduce((a, b) => a + b, 0) / allDistances.length;
140
+ case "ward": {
141
+ // Simplified Ward's method - minimize within-cluster variance
142
+ const size1 = cluster1.members.size;
143
+ const size2 = cluster2.members.size;
144
+ const avgDist = allDistances.reduce((a, b) => a + b, 0) / allDistances.length;
145
+ return avgDist * ((size1 * size2) / (size1 + size2));
146
+ }
100
147
  default:
101
- return Math.min(... allDistances);
148
+ return Math.min(...allDistances);
102
149
  }
103
150
  }
104
151
 
105
152
  /**
106
- * Agglomerative hierarchical clustering
107
- * Builds clusters bottom-up by merging closest pairs
108
- *
109
- * @param graph - Undirected graph
110
- * @param linkage - Linkage method for cluster distance
111
- * @returns Hierarchical clustering result
112
- *
113
- * Time Complexity: O(n³) naive, O(n² log n) with heap
114
- * Space Complexity: O(n²)
153
+ * Internal implementation of agglomerative hierarchical clustering
154
+ * @param graph - Adjacency map representation of the graph
155
+ * @param linkage - Linkage method for cluster distance computation
156
+ * @returns Hierarchical clustering result with dendrogram and cluster assignments
115
157
  */
116
- export function hierarchicalClustering<T>(
158
+ function hierarchicalClusteringImpl<T>(
117
159
  graph: Map<T, Set<T>>,
118
160
  linkage: LinkageMethod = "single",
119
161
  ): HierarchicalClusteringResult<T> {
@@ -122,7 +164,7 @@ export function hierarchicalClustering<T>(
122
164
 
123
165
  if (n === 0) {
124
166
  return {
125
- root: {id: "empty", members: new Set(), distance: 0, height: 0},
167
+ root: { id: "empty", members: new Set(), distance: 0, height: 0 },
126
168
  dendrogram: [],
127
169
  clusters: new Map(),
128
170
  };
@@ -140,7 +182,7 @@ export function hierarchicalClustering<T>(
140
182
  height: 0,
141
183
  }));
142
184
 
143
- const dendrogram: ClusterNode<T>[] = [... clusters];
185
+ const dendrogram: ClusterNode<T>[] = [...clusters];
144
186
  let clusterCount = n;
145
187
 
146
188
  // Distance matrix between clusters
@@ -148,180 +190,133 @@ export function hierarchicalClustering<T>(
148
190
 
149
191
  // Initialize cluster distances
150
192
  for (let i = 0; i < clusters.length; i++) {
151
- const clusterI = clusters[i];
152
- if (!clusterI) {
193
+ const cluster1 = clusters[i];
194
+ if (!cluster1) {
153
195
  continue;
154
196
  }
155
197
 
156
- clusterDistances.set(clusterI.id, new Map());
198
+ const distMap = new Map<string, number>();
157
199
  for (let j = i + 1; j < clusters.length; j++) {
158
- const clusterJ = clusters[j];
159
- if (!clusterJ) {
200
+ const cluster2 = clusters[j];
201
+ if (!cluster2) {
160
202
  continue;
161
203
  }
162
204
 
163
- const dist = clusterDistance(
164
- clusterI.members,
165
- clusterJ.members,
166
- distances,
167
- linkage,
168
- );
169
- const clusterIDistances = clusterDistances.get(clusterI.id);
170
- if (clusterIDistances) {
171
- clusterIDistances.set(clusterJ.id, dist);
172
- }
205
+ const dist = clusterDistance(cluster1, cluster2, distances, linkage);
206
+ distMap.set(cluster2.id, dist);
173
207
  }
208
+ clusterDistances.set(cluster1.id, distMap);
174
209
  }
175
210
 
211
+ // Active clusters
212
+ const activeClusters = new Set(clusters);
213
+
176
214
  // Merge clusters until only one remains
177
- while (clusters.length > 1) {
178
- // Find closest pair of clusters
215
+ while (activeClusters.size > 1) {
216
+ // Find closest pair of clusters
179
217
  let minDist = Infinity;
180
- let merge1 = 0;
181
- let merge2 = 0;
182
-
183
- for (let i = 0; i < clusters.length; i++) {
184
- for (let j = i + 1; j < clusters.length; j++) {
185
- const clusterI = clusters[i];
186
- const clusterJ = clusters[j];
187
- if (!clusterI || !clusterJ) {
218
+ let mergeCluster1: ClusterNode<T> | null = null;
219
+ let mergeCluster2: ClusterNode<T> | null = null;
220
+
221
+ for (const cluster1 of activeClusters) {
222
+ const distMap = clusterDistances.get(cluster1.id);
223
+ if (!distMap) {
224
+ continue;
225
+ }
226
+
227
+ for (const cluster2 of activeClusters) {
228
+ if (cluster1.id >= cluster2.id) {
188
229
  continue;
189
230
  }
190
231
 
191
- const dist = clusterDistances.get(clusterI.id)?.get(clusterJ.id) ??
192
- clusterDistances.get(clusterJ.id)?.get(clusterI.id) ??
193
- Infinity;
232
+ const dist =
233
+ distMap.get(cluster2.id) ?? clusterDistances.get(cluster2.id)?.get(cluster1.id) ?? Infinity;
194
234
 
195
235
  if (dist < minDist) {
196
236
  minDist = dist;
197
- merge1 = i;
198
- merge2 = j;
237
+ mergeCluster1 = cluster1;
238
+ mergeCluster2 = cluster2;
199
239
  }
200
240
  }
201
241
  }
202
242
 
203
- // Check if we found a valid merge
204
- if (minDist === Infinity || merge1 === merge2) {
205
- // No more clusters can be merged (disconnected components)
243
+ if (!mergeCluster1 || !mergeCluster2 || minDist === Infinity) {
244
+ // No valid merge found - create forest
245
+ const trees = Array.from(activeClusters);
246
+ const forestRoot: ClusterNode<T> = {
247
+ id: `forest-${String(clusterCount++)}`,
248
+ members: new Set(nodes),
249
+ distance: Infinity,
250
+ height: Math.max(...trees.map((t) => t.height)) + 1,
251
+ trees,
252
+ };
253
+ dendrogram.push(forestRoot);
254
+ activeClusters.clear();
255
+ activeClusters.add(forestRoot);
206
256
  break;
207
257
  }
208
258
 
209
259
  // Create new cluster
210
- const cluster1 = clusters[merge1];
211
- const cluster2 = clusters[merge2];
212
- if (!cluster1 || !cluster2) {
213
- continue;
214
- }
215
-
216
260
  const newCluster: ClusterNode<T> = {
217
261
  id: `cluster-${String(clusterCount++)}`,
218
- members: new Set([... cluster1.members, ... cluster2.members]),
219
- left: cluster1,
220
- right: cluster2,
262
+ members: new Set([...mergeCluster1.members, ...mergeCluster2.members]),
263
+ left: mergeCluster1,
264
+ right: mergeCluster2,
221
265
  distance: minDist,
222
- height: Math.max(cluster1.height, cluster2.height) + 1,
266
+ height: Math.max(mergeCluster1.height, mergeCluster2.height) + 1,
223
267
  };
224
268
 
225
269
  dendrogram.push(newCluster);
226
270
 
227
- // Update cluster distances for new cluster
228
- clusterDistances.set(newCluster.id, new Map());
271
+ // Update distances to new cluster
272
+ const newDistMap = new Map<string, number>();
273
+ for (const cluster of activeClusters) {
274
+ if (cluster.id === mergeCluster1.id || cluster.id === mergeCluster2.id) {
275
+ continue;
276
+ }
229
277
 
230
- for (let i = 0; i < clusters.length; i++) {
231
- if (i !== merge1 && i !== merge2) {
232
- const clusterI = clusters[i];
233
- if (!clusterI) {
234
- continue;
235
- }
278
+ const dist = clusterDistance(newCluster, cluster, distances, linkage);
279
+ newDistMap.set(cluster.id, dist);
236
280
 
237
- const newDist = clusterDistance(
238
- newCluster.members,
239
- clusterI.members,
240
- distances,
241
- linkage,
242
- );
243
- const newClusterDistances = clusterDistances.get(newCluster.id);
244
- if (newClusterDistances) {
245
- newClusterDistances.set(clusterI.id, newDist);
246
- }
281
+ // Update reverse direction
282
+ const clusterDistMap = clusterDistances.get(cluster.id);
283
+ if (clusterDistMap) {
284
+ clusterDistMap.set(newCluster.id, dist);
247
285
  }
248
286
  }
287
+ clusterDistances.set(newCluster.id, newDistMap);
249
288
 
250
- // Remove old clusters and their distances
251
- clusterDistances.delete(cluster1.id);
252
- clusterDistances.delete(cluster2.id);
253
- for (const [, dists] of clusterDistances) {
254
- dists.delete(cluster1.id);
255
- dists.delete(cluster2.id);
289
+ // Remove merged clusters
290
+ activeClusters.delete(mergeCluster1);
291
+ activeClusters.delete(mergeCluster2);
292
+ clusterDistances.delete(mergeCluster1.id);
293
+ clusterDistances.delete(mergeCluster2.id);
294
+
295
+ // Clean up references
296
+ for (const distMap of clusterDistances.values()) {
297
+ distMap.delete(mergeCluster1.id);
298
+ distMap.delete(mergeCluster2.id);
256
299
  }
257
300
 
258
- // Remove merged clusters and add new one
259
- clusters.splice(Math.max(merge1, merge2), 1);
260
- clusters.splice(Math.min(merge1, merge2), 1);
261
- clusters.push(newCluster);
301
+ activeClusters.add(newCluster);
262
302
  }
263
303
 
264
- // Generate clusters at different levels
265
- const clustersByLevel = new Map<number, Set<T>[]>();
304
+ // Root is the last remaining cluster
305
+ const root = activeClusters.size > 0 ? Array.from(activeClusters)[0] : dendrogram[dendrogram.length - 1];
266
306
 
267
- function extractClustersAtHeight(node: ClusterNode<T>, height: number, clusters: Set<T>[]): void {
268
- if (node.height <= height || !node.left || !node.right) {
269
- clusters.push(node.members);
270
- } else {
271
- extractClustersAtHeight(node.left, height, clusters);
272
- extractClustersAtHeight(node.right, height, clusters);
273
- }
274
- }
307
+ // Generate clusters at different heights
308
+ const clustersByLevel = new Map<number, Set<T>[]>();
275
309
 
276
- // Handle the case where we have multiple disconnected components
277
- let root: ClusterNode<T>;
278
- if (clusters.length === 0) {
310
+ if (!root) {
279
311
  return {
280
- root: {id: "empty", members: new Set(), distance: 0, height: 0},
312
+ root: { id: "empty", members: new Set(), distance: 0, height: 0 },
281
313
  dendrogram: [],
282
314
  clusters: new Map(),
283
315
  };
284
- } else if (clusters.length === 1 && clusters[0]) {
285
- root = clusters[0];
286
- } else {
287
- // Create a root that combines all remaining clusters (forest)
288
- const allMembers = new Set<T>();
289
- let maxHeight = 0;
290
- for (const cluster of clusters) {
291
- for (const member of cluster.members) {
292
- allMembers.add(member);
293
- }
294
- maxHeight = Math.max(maxHeight, cluster.height);
295
- }
296
-
297
- root = {
298
- id: "root-forest",
299
- members: allMembers,
300
- distance: Infinity,
301
- height: maxHeight + 1,
302
- // Store references to the individual trees
303
- trees: [... clusters],
304
- };
305
-
306
- dendrogram.push(root);
307
316
  }
308
317
 
309
318
  for (let h = 0; h <= root.height; h++) {
310
- const clustersAtHeight: Set<T>[] = [];
311
-
312
- if (clusters.length > 1 && h === root.height) {
313
- // At forest root height, return single cluster with all members
314
- clustersAtHeight.push(root.members);
315
- } else if (clusters.length > 1 && h < root.height) {
316
- // For forest, extract from each tree separately
317
- for (const tree of clusters) {
318
- extractClustersAtHeight(tree, h, clustersAtHeight);
319
- }
320
- } else {
321
- extractClustersAtHeight(root, h, clustersAtHeight);
322
- }
323
-
324
- clustersByLevel.set(h, clustersAtHeight);
319
+ clustersByLevel.set(h, cutDendrogram(root, h));
325
320
  }
326
321
 
327
322
  return {
@@ -332,36 +327,32 @@ export function hierarchicalClustering<T>(
332
327
  }
333
328
 
334
329
  /**
335
- * Cut dendrogram at specified height to get clusters
330
+ * Cut dendrogram at specific height to get clusters
331
+ * @param root - Root node of the dendrogram
332
+ * @param height - Height at which to cut the dendrogram
333
+ * @returns Array of sets representing clusters at the specified height
336
334
  */
337
- export function cutDendrogram<T>(
338
- root: ClusterNode<T>,
339
- height: number,
340
- ): Set<T>[] {
335
+ export function cutDendrogram<T>(root: ClusterNode<T>, height: number): Set<T>[] {
341
336
  const clusters: Set<T>[] = [];
342
337
 
343
338
  function traverse(node: ClusterNode<T>): void {
344
- // Handle forest root specially
345
- if (node.trees) {
346
- if (node.height <= height) {
347
- // If we're at or below the requested height, return the whole forest as one cluster
348
- clusters.push(node.members);
349
- } else {
350
- // Otherwise, traverse each tree in the forest
339
+ if (node.height <= height || (!node.left && !node.right)) {
340
+ clusters.push(node.members);
341
+ } else {
342
+ if (node.trees) {
343
+ // Handle forest nodes
351
344
  for (const tree of node.trees) {
352
345
  traverse(tree);
353
346
  }
354
- }
355
-
356
- return;
357
- }
347
+ } else {
348
+ if (node.left) {
349
+ traverse(node.left);
350
+ }
358
351
 
359
- // Normal binary tree traversal
360
- if (node.height <= height || !node.left || !node.right) {
361
- clusters.push(node.members);
362
- } else {
363
- traverse(node.left);
364
- traverse(node.right);
352
+ if (node.right) {
353
+ traverse(node.right);
354
+ }
355
+ }
365
356
  }
366
357
  }
367
358
 
@@ -370,12 +361,12 @@ export function cutDendrogram<T>(
370
361
  }
371
362
 
372
363
  /**
373
- * Cut dendrogram to get k clusters
364
+ * Cut dendrogram to get exactly k clusters
365
+ * @param root - Root node of the dendrogram
366
+ * @param k - Target number of clusters
367
+ * @returns Array of sets representing exactly k clusters
374
368
  */
375
- export function cutDendrogramKClusters<T>(
376
- root: ClusterNode<T>,
377
- k: number,
378
- ): Set<T>[] {
369
+ export function cutDendrogramKClusters<T>(root: ClusterNode<T>, k: number): Set<T>[] {
379
370
  if (k <= 0) {
380
371
  return [];
381
372
  }
@@ -384,7 +375,7 @@ export function cutDendrogramKClusters<T>(
384
375
  return [root.members];
385
376
  }
386
377
 
387
- // Find the height that gives us k clusters
378
+ // Binary search for the right height
388
379
  let low = 0;
389
380
  let high = root.height;
390
381
 
@@ -407,136 +398,123 @@ export function cutDendrogramKClusters<T>(
407
398
  /**
408
399
  * Compute modularity-based hierarchical clustering
409
400
  * Uses modularity gain to decide merges
401
+ * @param graph - The input graph to cluster
402
+ * @returns Hierarchical clustering result with dendrogram and cluster assignments
410
403
  */
411
- export function modularityHierarchicalClustering<T>(
412
- graph: Map<T, Set<T>>,
413
- ): HierarchicalClusteringResult<T> {
414
- const nodes = Array.from(graph.keys());
404
+ export function modularityHierarchicalClustering(graph: Graph): HierarchicalClusteringResult<string> {
405
+ const adjacencySet = graphToAdjacencySet(graph);
406
+ const nodes = Array.from(adjacencySet.keys());
415
407
  const n = nodes.length;
416
408
 
417
409
  if (n === 0) {
418
410
  return {
419
- root: {id: "empty", members: new Set(), distance: 0, height: 0},
411
+ root: { id: "empty", members: new Set(), distance: 0, height: 0 },
420
412
  dendrogram: [],
421
413
  clusters: new Map(),
422
414
  };
423
415
  }
424
416
 
425
- // Calculate total edges
426
- let m = 0;
427
- for (const neighbors of graph.values()) {
428
- m += neighbors.size;
417
+ // Compute degrees
418
+ const degrees = new Map<string, number>();
419
+ let totalEdges = 0;
420
+
421
+ for (const [node, neighbors] of adjacencySet) {
422
+ degrees.set(node, neighbors.size);
423
+ totalEdges += neighbors.size;
429
424
  }
430
- m = m / 2; // Each edge counted twice
425
+
426
+ // For undirected graphs
427
+ totalEdges = totalEdges / 2;
431
428
 
432
429
  // Initialize clusters
433
- const clusters: ClusterNode<T>[] = nodes.map((node, i) => ({
430
+ const clusters: ClusterNode<string>[] = nodes.map((node, i) => ({
434
431
  id: `leaf-${String(i)}`,
435
432
  members: new Set([node]),
436
433
  distance: 0,
437
434
  height: 0,
438
435
  }));
439
436
 
440
- const dendrogram: ClusterNode<T>[] = [... clusters];
437
+ const dendrogram: ClusterNode<string>[] = [...clusters];
441
438
  let clusterCount = n;
442
439
 
443
- // Track community assignments
444
- const communityMap = new Map<T, number>();
445
- nodes.forEach((node, i) => communityMap.set(node, i));
440
+ // Active clusters
441
+ const activeClusters = new Set(clusters);
446
442
 
447
- // Compute modularity gain for merging communities
448
- function modularityGain(comm1: Set<T>, comm2: Set<T>): number {
449
- let edgesBetween = 0;
450
- let degree1 = 0;
451
- let degree2 = 0;
443
+ // Community assignments
444
+ const communities = new Map<string, number>();
445
+ nodes.forEach((node, i) => communities.set(node, i));
452
446
 
453
- for (const node1 of comm1) {
454
- const neighbors = graph.get(node1);
455
- if (!neighbors) {
456
- continue;
457
- }
458
-
459
- degree1 += neighbors.size;
447
+ // Merge clusters based on modularity gain
448
+ while (activeClusters.size > 1) {
449
+ let maxGain = -Infinity;
450
+ let mergeCluster1: ClusterNode<string> | null = null;
451
+ let mergeCluster2: ClusterNode<string> | null = null;
460
452
 
461
- for (const neighbor of neighbors) {
462
- if (comm2.has(neighbor)) {
463
- edgesBetween++;
453
+ for (const cluster1 of activeClusters) {
454
+ for (const cluster2 of activeClusters) {
455
+ if (cluster1.id >= cluster2.id) {
456
+ continue;
464
457
  }
465
- }
466
- }
467
458
 
468
- for (const node2 of comm2) {
469
- const node2Neighbors = graph.get(node2);
470
- if (node2Neighbors) {
471
- degree2 += node2Neighbors.size;
472
- }
473
- }
459
+ // Calculate modularity gain
460
+ let edgesBetween = 0;
461
+ let degreeProduct = 0;
474
462
 
475
- // Modularity gain formula
476
- const gain = (edgesBetween / m) - ((degree1 * degree2) / (4 * m * m));
477
- return gain;
478
- }
463
+ for (const node1 of cluster1.members) {
464
+ const neighbors = adjacencySet.get(node1);
465
+ const degree1 = degrees.get(node1) ?? 0;
479
466
 
480
- // Merge clusters based on modularity
481
- while (clusters.length > 1) {
482
- let maxGain = -Infinity;
483
- let merge1 = 0;
484
- let merge2 = 0;
485
-
486
- for (let i = 0; i < clusters.length; i++) {
487
- for (let j = i + 1; j < clusters.length; j++) {
488
- const clusterI = clusters[i];
489
- const clusterJ = clusters[j];
490
- if (!clusterI || !clusterJ) {
491
- continue;
467
+ for (const node2 of cluster2.members) {
468
+ if (neighbors?.has(node2)) {
469
+ edgesBetween++;
470
+ }
471
+
472
+ const degree2 = degrees.get(node2) ?? 0;
473
+ degreeProduct += degree1 * degree2;
474
+ }
492
475
  }
493
476
 
494
- const gain = modularityGain(clusterI.members, clusterJ.members);
477
+ const gain = edgesBetween / totalEdges - degreeProduct / (4 * totalEdges * totalEdges);
495
478
 
496
479
  if (gain > maxGain) {
497
480
  maxGain = gain;
498
- merge1 = i;
499
- merge2 = j;
481
+ mergeCluster1 = cluster1;
482
+ mergeCluster2 = cluster2;
500
483
  }
501
484
  }
502
485
  }
503
486
 
504
- // Create new cluster
505
- const cluster1 = clusters[merge1];
506
- const cluster2 = clusters[merge2];
507
- if (!cluster1 || !cluster2) {
508
- continue;
487
+ if (!mergeCluster1 || !mergeCluster2) {
488
+ break;
509
489
  }
510
490
 
511
- const newCluster: ClusterNode<T> = {
491
+ // Create new cluster
492
+ const newCluster: ClusterNode<string> = {
512
493
  id: `cluster-${String(clusterCount++)}`,
513
- members: new Set([... cluster1.members, ... cluster2.members]),
514
- left: cluster1,
515
- right: cluster2,
494
+ members: new Set([...mergeCluster1.members, ...mergeCluster2.members]),
495
+ left: mergeCluster1,
496
+ right: mergeCluster2,
516
497
  distance: -maxGain, // Use negative gain as distance
517
- height: Math.max(cluster1.height, cluster2.height) + 1,
498
+ height: Math.max(mergeCluster1.height, mergeCluster2.height) + 1,
518
499
  };
519
500
 
520
501
  dendrogram.push(newCluster);
521
502
 
522
- // Update community assignments
523
- for (const node of newCluster.members) {
524
- communityMap.set(node, clusterCount - 1);
525
- }
526
-
527
- // Remove merged clusters
528
- clusters.splice(Math.max(merge1, merge2), 1);
529
- clusters.splice(Math.min(merge1, merge2), 1);
530
- clusters.push(newCluster);
503
+ // Remove old and add new
504
+ activeClusters.delete(mergeCluster1);
505
+ activeClusters.delete(mergeCluster2);
506
+ activeClusters.add(newCluster);
531
507
  }
532
508
 
533
- // Generate clusters at different levels
534
- const clustersByLevel = new Map<number, Set<T>[]>();
535
- const root = clusters[0];
509
+ // Root is the last remaining cluster
510
+ const root = Array.from(activeClusters)[0] ?? dendrogram[dendrogram.length - 1];
511
+
512
+ // Generate clusters at different heights
513
+ const clustersByLevel = new Map<number, Set<string>[]>();
536
514
 
537
515
  if (!root) {
538
516
  return {
539
- root: {id: "empty", members: new Set(), distance: 0, height: 0},
517
+ root: { id: "empty", members: new Set(), distance: 0, height: 0 },
540
518
  dendrogram: [],
541
519
  clusters: new Map(),
542
520
  };
@@ -553,3 +531,21 @@ export function modularityHierarchicalClustering<T>(
553
531
  };
554
532
  }
555
533
 
534
+ /**
535
+ * Agglomerative hierarchical clustering
536
+ * Builds clusters bottom-up by merging closest pairs
537
+ * @param graph - Undirected graph - accepts Graph class or Map<T, Set<T>>
538
+ * @param linkage - Linkage method for cluster distance
539
+ * @returns Hierarchical clustering result
540
+ *
541
+ * Time Complexity: O(n³) naive, O(n² log n) with heap
542
+ * Space Complexity: O(n²)
543
+ */
544
+ export function hierarchicalClustering(
545
+ graph: Graph,
546
+ linkage: LinkageMethod = "single",
547
+ ): HierarchicalClusteringResult<string> {
548
+ // Convert Graph to adjacency set representation
549
+ const adjacencySet = graphToAdjacencySet(graph);
550
+ return hierarchicalClusteringImpl(adjacencySet, linkage);
551
+ }