@graphty/algorithms 2.1.2 → 2.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 (355) hide show
  1. package/README.md +139 -50
  2. package/dist/algorithms.js +15260 -10390
  3. package/dist/algorithms.js.map +1 -1
  4. package/dist/algorithms.standalone.js +26675 -21542
  5. package/dist/algorithms.standalone.js.map +1 -1
  6. package/dist/src/algorithms/centrality/betweenness.d.ts +18 -0
  7. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  8. package/dist/src/algorithms/centrality/betweenness.js +40 -1
  9. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  10. package/dist/src/algorithms/centrality/closeness.d.ts +20 -0
  11. package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
  12. package/dist/src/algorithms/centrality/closeness.js +93 -8
  13. package/dist/src/algorithms/centrality/closeness.js.map +1 -1
  14. package/dist/src/algorithms/centrality/degree.d.ts +9 -0
  15. package/dist/src/algorithms/centrality/degree.d.ts.map +1 -1
  16. package/dist/src/algorithms/centrality/degree.js +18 -0
  17. package/dist/src/algorithms/centrality/degree.js.map +1 -1
  18. package/dist/src/algorithms/centrality/eigenvector.d.ts +10 -0
  19. package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -1
  20. package/dist/src/algorithms/centrality/eigenvector.js +29 -0
  21. package/dist/src/algorithms/centrality/eigenvector.js.map +1 -1
  22. package/dist/src/algorithms/centrality/hits.d.ts +9 -0
  23. package/dist/src/algorithms/centrality/hits.d.ts.map +1 -1
  24. package/dist/src/algorithms/centrality/hits.js +23 -0
  25. package/dist/src/algorithms/centrality/hits.js.map +1 -1
  26. package/dist/src/algorithms/centrality/katz.d.ts +9 -0
  27. package/dist/src/algorithms/centrality/katz.d.ts.map +1 -1
  28. package/dist/src/algorithms/centrality/katz.js +31 -0
  29. package/dist/src/algorithms/centrality/katz.js.map +1 -1
  30. package/dist/src/algorithms/centrality/pagerank.d.ts +13 -13
  31. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
  32. package/dist/src/algorithms/centrality/pagerank.js +47 -10
  33. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  34. package/dist/src/algorithms/community/girvan-newman-legacy.d.ts +27 -0
  35. package/dist/src/algorithms/community/girvan-newman-legacy.d.ts.map +1 -0
  36. package/dist/src/algorithms/community/girvan-newman-legacy.js +335 -0
  37. package/dist/src/algorithms/community/girvan-newman-legacy.js.map +1 -0
  38. package/dist/src/algorithms/community/girvan-newman.d.ts +5 -3
  39. package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
  40. package/dist/src/algorithms/community/girvan-newman.js +34 -308
  41. package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
  42. package/dist/src/algorithms/community/label-propagation.d.ts +9 -6
  43. package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
  44. package/dist/src/algorithms/community/label-propagation.js +24 -110
  45. package/dist/src/algorithms/community/label-propagation.js.map +1 -1
  46. package/dist/src/algorithms/components/connected.d.ts.map +1 -1
  47. package/dist/src/algorithms/components/connected.js +28 -77
  48. package/dist/src/algorithms/components/connected.js.map +1 -1
  49. package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -1
  50. package/dist/src/algorithms/mst/kruskal.js +59 -3
  51. package/dist/src/algorithms/mst/kruskal.js.map +1 -1
  52. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
  53. package/dist/src/algorithms/shortest-path/bellman-ford.js +46 -0
  54. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  55. package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
  56. package/dist/src/algorithms/shortest-path/dijkstra.js +27 -0
  57. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  58. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +15 -5
  59. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -1
  60. package/dist/src/algorithms/shortest-path/floyd-warshall.js +97 -107
  61. package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -1
  62. package/dist/src/algorithms/traversal/bfs-unified.d.ts +2 -12
  63. package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -1
  64. package/dist/src/algorithms/traversal/bfs-unified.js +47 -308
  65. package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -1
  66. package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -1
  67. package/dist/src/algorithms/traversal/bfs-variants.js +5 -25
  68. package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -1
  69. package/dist/src/algorithms/traversal/bfs.d.ts +0 -3
  70. package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
  71. package/dist/src/algorithms/traversal/bfs.js +0 -4
  72. package/dist/src/algorithms/traversal/bfs.js.map +1 -1
  73. package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -1
  74. package/dist/src/algorithms/traversal/dfs.js +33 -154
  75. package/dist/src/algorithms/traversal/dfs.js.map +1 -1
  76. package/dist/src/clustering/hierarchical-legacy.d.ts +56 -0
  77. package/dist/src/clustering/hierarchical-legacy.d.ts.map +1 -0
  78. package/dist/src/clustering/hierarchical-legacy.js +441 -0
  79. package/dist/src/clustering/hierarchical-legacy.js.map +1 -0
  80. package/dist/src/clustering/hierarchical.d.ts +7 -39
  81. package/dist/src/clustering/hierarchical.d.ts.map +1 -1
  82. package/dist/src/clustering/hierarchical.js +56 -417
  83. package/dist/src/clustering/hierarchical.js.map +1 -1
  84. package/dist/src/clustering/k-core-legacy.d.ts +30 -0
  85. package/dist/src/clustering/k-core-legacy.d.ts.map +1 -0
  86. package/dist/src/clustering/k-core-legacy.js +191 -0
  87. package/dist/src/clustering/k-core-legacy.js.map +1 -0
  88. package/dist/src/clustering/k-core.d.ts +8 -7
  89. package/dist/src/clustering/k-core.d.ts.map +1 -1
  90. package/dist/src/clustering/k-core.js +33 -174
  91. package/dist/src/clustering/k-core.js.map +1 -1
  92. package/dist/src/clustering/mcl-legacy.d.ts +42 -0
  93. package/dist/src/clustering/mcl-legacy.d.ts.map +1 -0
  94. package/dist/src/clustering/mcl-legacy.js +386 -0
  95. package/dist/src/clustering/mcl-legacy.js.map +1 -0
  96. package/dist/src/clustering/mcl.d.ts +9 -32
  97. package/dist/src/clustering/mcl.d.ts.map +1 -1
  98. package/dist/src/clustering/mcl.js +48 -374
  99. package/dist/src/clustering/mcl.js.map +1 -1
  100. package/dist/src/index.d.ts +29 -3
  101. package/dist/src/index.d.ts.map +1 -1
  102. package/dist/src/index.js +2 -1
  103. package/dist/src/index.js.map +1 -1
  104. package/dist/src/indexed/accelerator.d.ts +141 -11
  105. package/dist/src/indexed/accelerator.d.ts.map +1 -1
  106. package/dist/src/indexed/accelerator.js +319 -5
  107. package/dist/src/indexed/accelerator.js.map +1 -1
  108. package/dist/src/indexed/all-pairs.d.ts +59 -0
  109. package/dist/src/indexed/all-pairs.d.ts.map +1 -0
  110. package/dist/src/indexed/all-pairs.js +274 -0
  111. package/dist/src/indexed/all-pairs.js.map +1 -0
  112. package/dist/src/indexed/bellman-ford.d.ts +25 -0
  113. package/dist/src/indexed/bellman-ford.d.ts.map +1 -0
  114. package/dist/src/indexed/bellman-ford.js +59 -0
  115. package/dist/src/indexed/bellman-ford.js.map +1 -0
  116. package/dist/src/indexed/betweenness.d.ts +85 -0
  117. package/dist/src/indexed/betweenness.d.ts.map +1 -0
  118. package/dist/src/indexed/betweenness.js +188 -0
  119. package/dist/src/indexed/betweenness.js.map +1 -0
  120. package/dist/src/indexed/bfs.d.ts +69 -2
  121. package/dist/src/indexed/bfs.d.ts.map +1 -1
  122. package/dist/src/indexed/bfs.js +148 -1
  123. package/dist/src/indexed/bfs.js.map +1 -1
  124. package/dist/src/indexed/bipartite.d.ts +37 -0
  125. package/dist/src/indexed/bipartite.d.ts.map +1 -0
  126. package/dist/src/indexed/bipartite.js +54 -0
  127. package/dist/src/indexed/bipartite.js.map +1 -0
  128. package/dist/src/indexed/closeness.d.ts +49 -0
  129. package/dist/src/indexed/closeness.d.ts.map +1 -0
  130. package/dist/src/indexed/closeness.js +127 -0
  131. package/dist/src/indexed/closeness.js.map +1 -0
  132. package/dist/src/indexed/common-neighbors.d.ts +14 -1
  133. package/dist/src/indexed/common-neighbors.d.ts.map +1 -1
  134. package/dist/src/indexed/common-neighbors.js +24 -14
  135. package/dist/src/indexed/common-neighbors.js.map +1 -1
  136. package/dist/src/indexed/degree.d.ts +19 -0
  137. package/dist/src/indexed/degree.d.ts.map +1 -0
  138. package/dist/src/indexed/degree.js +45 -0
  139. package/dist/src/indexed/degree.js.map +1 -0
  140. package/dist/src/indexed/delta-pagerank.d.ts +145 -0
  141. package/dist/src/indexed/delta-pagerank.d.ts.map +1 -0
  142. package/dist/src/indexed/delta-pagerank.js +428 -0
  143. package/dist/src/indexed/delta-pagerank.js.map +1 -0
  144. package/dist/src/indexed/dfs.d.ts +55 -0
  145. package/dist/src/indexed/dfs.d.ts.map +1 -0
  146. package/dist/src/indexed/dfs.js +146 -0
  147. package/dist/src/indexed/dfs.js.map +1 -0
  148. package/dist/src/indexed/eigenvector.d.ts +48 -0
  149. package/dist/src/indexed/eigenvector.d.ts.map +1 -0
  150. package/dist/src/indexed/eigenvector.js +162 -0
  151. package/dist/src/indexed/eigenvector.js.map +1 -0
  152. package/dist/src/indexed/facade.d.ts +109 -0
  153. package/dist/src/indexed/facade.d.ts.map +1 -0
  154. package/dist/src/indexed/facade.js +204 -0
  155. package/dist/src/indexed/facade.js.map +1 -0
  156. package/dist/src/indexed/flow.d.ts +120 -0
  157. package/dist/src/indexed/flow.d.ts.map +1 -0
  158. package/dist/src/indexed/flow.js +377 -0
  159. package/dist/src/indexed/flow.js.map +1 -0
  160. package/dist/src/indexed/girvan-newman.d.ts +42 -0
  161. package/dist/src/indexed/girvan-newman.d.ts.map +1 -0
  162. package/dist/src/indexed/girvan-newman.js +90 -0
  163. package/dist/src/indexed/girvan-newman.js.map +1 -0
  164. package/dist/src/indexed/grsbm.d.ts +85 -0
  165. package/dist/src/indexed/grsbm.d.ts.map +1 -0
  166. package/dist/src/indexed/grsbm.js +287 -0
  167. package/dist/src/indexed/grsbm.js.map +1 -0
  168. package/dist/src/indexed/hierarchical.d.ts +69 -0
  169. package/dist/src/indexed/hierarchical.d.ts.map +1 -0
  170. package/dist/src/indexed/hierarchical.js +245 -0
  171. package/dist/src/indexed/hierarchical.js.map +1 -0
  172. package/dist/src/indexed/index.d.ts +29 -3
  173. package/dist/src/indexed/index.d.ts.map +1 -1
  174. package/dist/src/indexed/index.js +29 -3
  175. package/dist/src/indexed/index.js.map +1 -1
  176. package/dist/src/indexed/isomorphism.d.ts +42 -0
  177. package/dist/src/indexed/isomorphism.d.ts.map +1 -0
  178. package/dist/src/indexed/isomorphism.js +269 -0
  179. package/dist/src/indexed/isomorphism.js.map +1 -0
  180. package/dist/src/indexed/label-propagation.d.ts +134 -0
  181. package/dist/src/indexed/label-propagation.d.ts.map +1 -0
  182. package/dist/src/indexed/label-propagation.js +486 -0
  183. package/dist/src/indexed/label-propagation.js.map +1 -0
  184. package/dist/src/indexed/leiden.d.ts +52 -0
  185. package/dist/src/indexed/leiden.d.ts.map +1 -0
  186. package/dist/src/indexed/leiden.js +384 -0
  187. package/dist/src/indexed/leiden.js.map +1 -0
  188. package/dist/src/indexed/link-prediction.d.ts +147 -0
  189. package/dist/src/indexed/link-prediction.d.ts.map +1 -0
  190. package/dist/src/indexed/link-prediction.js +317 -0
  191. package/dist/src/indexed/link-prediction.js.map +1 -0
  192. package/dist/src/indexed/markov.d.ts +54 -0
  193. package/dist/src/indexed/markov.d.ts.map +1 -0
  194. package/dist/src/indexed/markov.js +284 -0
  195. package/dist/src/indexed/markov.js.map +1 -0
  196. package/dist/src/indexed/matching.d.ts +53 -0
  197. package/dist/src/indexed/matching.d.ts.map +1 -0
  198. package/dist/src/indexed/matching.js +139 -0
  199. package/dist/src/indexed/matching.js.map +1 -0
  200. package/dist/src/indexed/min-cut.d.ts +48 -0
  201. package/dist/src/indexed/min-cut.d.ts.map +1 -0
  202. package/dist/src/indexed/min-cut.js +175 -0
  203. package/dist/src/indexed/min-cut.js.map +1 -0
  204. package/dist/src/indexed/modularity.d.ts +34 -0
  205. package/dist/src/indexed/modularity.d.ts.map +1 -0
  206. package/dist/src/indexed/modularity.js +74 -0
  207. package/dist/src/indexed/modularity.js.map +1 -0
  208. package/dist/src/indexed/mst.d.ts +29 -1
  209. package/dist/src/indexed/mst.d.ts.map +1 -1
  210. package/dist/src/indexed/mst.js +62 -0
  211. package/dist/src/indexed/mst.js.map +1 -1
  212. package/dist/src/indexed/pagerank.d.ts +29 -6
  213. package/dist/src/indexed/pagerank.d.ts.map +1 -1
  214. package/dist/src/indexed/pagerank.js +107 -11
  215. package/dist/src/indexed/pagerank.js.map +1 -1
  216. package/dist/src/indexed/point-to-point.d.ts +53 -0
  217. package/dist/src/indexed/point-to-point.d.ts.map +1 -0
  218. package/dist/src/indexed/point-to-point.js +173 -0
  219. package/dist/src/indexed/point-to-point.js.map +1 -0
  220. package/dist/src/indexed/scc.d.ts +53 -0
  221. package/dist/src/indexed/scc.d.ts.map +1 -0
  222. package/dist/src/indexed/scc.js +110 -0
  223. package/dist/src/indexed/scc.js.map +1 -0
  224. package/dist/src/indexed/spectral.d.ts +61 -0
  225. package/dist/src/indexed/spectral.d.ts.map +1 -0
  226. package/dist/src/indexed/spectral.js +476 -0
  227. package/dist/src/indexed/spectral.js.map +1 -0
  228. package/dist/src/indexed/structures/bit-set.d.ts +45 -0
  229. package/dist/src/indexed/structures/bit-set.d.ts.map +1 -0
  230. package/dist/src/indexed/structures/bit-set.js +57 -0
  231. package/dist/src/indexed/structures/bit-set.js.map +1 -0
  232. package/dist/src/indexed/structures/max-heap.d.ts +52 -0
  233. package/dist/src/indexed/structures/max-heap.d.ts.map +1 -0
  234. package/dist/src/indexed/structures/max-heap.js +66 -0
  235. package/dist/src/indexed/structures/max-heap.js.map +1 -0
  236. package/dist/src/indexed/structures/min-heap.d.ts +30 -1
  237. package/dist/src/indexed/structures/min-heap.d.ts.map +1 -1
  238. package/dist/src/indexed/structures/min-heap.js +46 -6
  239. package/dist/src/indexed/structures/min-heap.js.map +1 -1
  240. package/dist/src/indexed/structures/ring-queue.d.ts +41 -0
  241. package/dist/src/indexed/structures/ring-queue.d.ts.map +1 -0
  242. package/dist/src/indexed/structures/ring-queue.js +64 -0
  243. package/dist/src/indexed/structures/ring-queue.js.map +1 -0
  244. package/dist/src/indexed/sync.d.ts +50 -0
  245. package/dist/src/indexed/sync.d.ts.map +1 -0
  246. package/dist/src/indexed/sync.js +175 -0
  247. package/dist/src/indexed/sync.js.map +1 -0
  248. package/dist/src/indexed/terahac.d.ts +58 -0
  249. package/dist/src/indexed/terahac.d.ts.map +1 -0
  250. package/dist/src/indexed/terahac.js +244 -0
  251. package/dist/src/indexed/terahac.js.map +1 -0
  252. package/dist/src/indexed/to-snapshot.d.ts +45 -2
  253. package/dist/src/indexed/to-snapshot.d.ts.map +1 -1
  254. package/dist/src/indexed/to-snapshot.js +140 -6
  255. package/dist/src/indexed/to-snapshot.js.map +1 -1
  256. package/dist/src/link-prediction/common-neighbors-legacy.d.ts +71 -0
  257. package/dist/src/link-prediction/common-neighbors-legacy.d.ts.map +1 -0
  258. package/dist/src/link-prediction/common-neighbors-legacy.js +172 -0
  259. package/dist/src/link-prediction/common-neighbors-legacy.js.map +1 -0
  260. package/dist/src/link-prediction/common-neighbors.d.ts +11 -19
  261. package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
  262. package/dist/src/link-prediction/common-neighbors.js +56 -123
  263. package/dist/src/link-prediction/common-neighbors.js.map +1 -1
  264. package/dist/src/optimized/direction-optimized-bfs.d.ts +6 -0
  265. package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -1
  266. package/dist/src/optimized/direction-optimized-bfs.js +6 -0
  267. package/dist/src/optimized/direction-optimized-bfs.js.map +1 -1
  268. package/dist/src/research/grsbm-legacy.d.ts +82 -0
  269. package/dist/src/research/grsbm-legacy.d.ts.map +1 -0
  270. package/dist/src/research/grsbm-legacy.js +416 -0
  271. package/dist/src/research/grsbm-legacy.js.map +1 -0
  272. package/dist/src/research/grsbm.d.ts +9 -66
  273. package/dist/src/research/grsbm.d.ts.map +1 -1
  274. package/dist/src/research/grsbm.js +67 -391
  275. package/dist/src/research/grsbm.js.map +1 -1
  276. package/dist/src/research/sync-legacy.d.ts +48 -0
  277. package/dist/src/research/sync-legacy.d.ts.map +1 -0
  278. package/dist/src/research/sync-legacy.js +337 -0
  279. package/dist/src/research/sync-legacy.js.map +1 -0
  280. package/dist/src/research/sync.d.ts +10 -32
  281. package/dist/src/research/sync.d.ts.map +1 -1
  282. package/dist/src/research/sync.js +28 -316
  283. package/dist/src/research/sync.js.map +1 -1
  284. package/package.json +4 -3
  285. package/src/algorithms/centrality/betweenness.ts +51 -1
  286. package/src/algorithms/centrality/closeness.ts +112 -12
  287. package/src/algorithms/centrality/degree.ts +19 -0
  288. package/src/algorithms/centrality/eigenvector.ts +34 -0
  289. package/src/algorithms/centrality/hits.ts +24 -0
  290. package/src/algorithms/centrality/katz.ts +32 -0
  291. package/src/algorithms/centrality/pagerank.ts +53 -13
  292. package/src/algorithms/community/girvan-newman-legacy.ts +414 -0
  293. package/src/algorithms/community/girvan-newman.ts +36 -386
  294. package/src/algorithms/community/label-propagation.ts +24 -133
  295. package/src/algorithms/components/connected.ts +28 -95
  296. package/src/algorithms/mst/kruskal.ts +65 -4
  297. package/src/algorithms/shortest-path/bellman-ford.ts +49 -0
  298. package/src/algorithms/shortest-path/dijkstra.ts +29 -0
  299. package/src/algorithms/shortest-path/floyd-warshall.ts +106 -136
  300. package/src/algorithms/traversal/bfs-unified.ts +52 -366
  301. package/src/algorithms/traversal/bfs-variants.ts +5 -28
  302. package/src/algorithms/traversal/bfs.ts +0 -4
  303. package/src/algorithms/traversal/dfs.ts +39 -182
  304. package/src/clustering/hierarchical-legacy.ts +551 -0
  305. package/src/clustering/hierarchical.ts +64 -520
  306. package/src/clustering/k-core-legacy.ts +229 -0
  307. package/src/clustering/k-core.ts +38 -209
  308. package/src/clustering/mcl-legacy.ts +498 -0
  309. package/src/clustering/mcl.ts +46 -479
  310. package/src/index.ts +90 -3
  311. package/src/indexed/accelerator.ts +502 -16
  312. package/src/indexed/all-pairs.ts +342 -0
  313. package/src/indexed/bellman-ford.ts +72 -0
  314. package/src/indexed/betweenness.ts +258 -0
  315. package/src/indexed/bfs.ts +189 -3
  316. package/src/indexed/bipartite.ts +77 -0
  317. package/src/indexed/closeness.ts +160 -0
  318. package/src/indexed/common-neighbors.ts +31 -14
  319. package/src/indexed/degree.ts +55 -0
  320. package/src/indexed/delta-pagerank.ts +513 -0
  321. package/src/indexed/dfs.ts +189 -0
  322. package/src/indexed/eigenvector.ts +202 -0
  323. package/src/indexed/facade.ts +229 -0
  324. package/src/indexed/flow.ts +500 -0
  325. package/src/indexed/girvan-newman.ts +120 -0
  326. package/src/indexed/grsbm.ts +390 -0
  327. package/src/indexed/hierarchical.ts +295 -0
  328. package/src/indexed/index.ts +94 -3
  329. package/src/indexed/isomorphism.ts +305 -0
  330. package/src/indexed/label-propagation.ts +582 -0
  331. package/src/indexed/leiden.ts +436 -0
  332. package/src/indexed/link-prediction.ts +399 -0
  333. package/src/indexed/markov.ts +324 -0
  334. package/src/indexed/matching.ts +192 -0
  335. package/src/indexed/min-cut.ts +201 -0
  336. package/src/indexed/modularity.ts +81 -0
  337. package/src/indexed/mst.ts +79 -1
  338. package/src/indexed/pagerank.ts +128 -15
  339. package/src/indexed/point-to-point.ts +220 -0
  340. package/src/indexed/scc.ts +132 -0
  341. package/src/indexed/spectral.ts +550 -0
  342. package/src/indexed/structures/bit-set.ts +65 -0
  343. package/src/indexed/structures/max-heap.ts +74 -0
  344. package/src/indexed/structures/min-heap.ts +51 -6
  345. package/src/indexed/structures/ring-queue.ts +70 -0
  346. package/src/indexed/sync.ts +229 -0
  347. package/src/indexed/terahac.ts +290 -0
  348. package/src/indexed/to-snapshot.ts +156 -6
  349. package/src/link-prediction/common-neighbors-legacy.ts +252 -0
  350. package/src/link-prediction/common-neighbors.ts +65 -168
  351. package/src/optimized/direction-optimized-bfs.ts +6 -1
  352. package/src/research/grsbm-legacy.ts +586 -0
  353. package/src/research/grsbm.ts +76 -555
  354. package/src/research/sync-legacy.ts +456 -0
  355. package/src/research/sync.ts +30 -431
@@ -11,13 +11,18 @@ export class IndexedMinHeap {
11
11
  private readonly heap: U32; // slot -> node
12
12
  private readonly slot: U32; // node -> slot, INVALID_INDEX when absent
13
13
  private readonly key: F64; // node -> key
14
+ private readonly tieBreakByIndex: boolean;
14
15
  private size = 0;
15
16
 
16
17
  /**
17
18
  * Create an empty heap over node indices in `[0, capacity)`.
18
19
  * @param capacity - The number of distinct node indices the heap may hold
20
+ * @param tieBreakByIndex - When true, of two equal keys the lower node index pops first, so
21
+ * the pop order is fully determined by the keys; when false (the default) equal keys pop in
22
+ * an order that depends on the push history
19
23
  */
20
- constructor(capacity: number) {
24
+ constructor(capacity: number, tieBreakByIndex = false) {
25
+ this.tieBreakByIndex = tieBreakByIndex;
21
26
  this.heap = new Uint32Array(capacity);
22
27
  this.slot = new Uint32Array(capacity).fill(INVALID_INDEX);
23
28
  this.key = new Float64Array(capacity);
@@ -31,12 +36,42 @@ export class IndexedMinHeap {
31
36
  return this.size === 0;
32
37
  }
33
38
 
39
+ /**
40
+ * Whether a node is currently in the heap.
41
+ * @param node - The node index
42
+ * @returns True when the node was pushed and has not been popped since
43
+ */
44
+ has(node: number): boolean {
45
+ return this.slot[node] !== INVALID_INDEX;
46
+ }
47
+
48
+ /**
49
+ * The key a node was last given. Meaningful only while `has(node)` is true.
50
+ * @param node - The node index
51
+ * @returns Its key
52
+ */
53
+ keyOf(node: number): number {
54
+ return this.key[node];
55
+ }
56
+
57
+ /**
58
+ * The smallest key in the heap, without removing it.
59
+ * @returns The minimum key, or Infinity when the heap is empty
60
+ */
61
+ peekKey(): number {
62
+ return this.size === 0 ? Infinity : this.key[this.heap[0]];
63
+ }
64
+
34
65
  /**
35
66
  * Insert a node that is not in the heap.
36
67
  * @param node - The node index
37
68
  * @param key - Its key
69
+ * @throws RangeError when the node index is outside `[0, capacity)`
38
70
  */
39
71
  push(node: number, key: number): void {
72
+ if (!(node >= 0 && node < this.slot.length)) {
73
+ throw new RangeError(`Node index ${String(node)} is outside the heap's ${String(this.slot.length)} nodes`);
74
+ }
40
75
  this.key[node] = key;
41
76
  this.heap[this.size] = node;
42
77
  this.slot[node] = this.size;
@@ -79,14 +114,25 @@ export class IndexedMinHeap {
79
114
  return top;
80
115
  }
81
116
 
117
+ /**
118
+ * Whether node `a` pops before node `b`.
119
+ * @param a - A node index in the heap
120
+ * @param b - A node index in the heap
121
+ * @returns True when a's key is smaller, or equal with the tie-break on and a lower index
122
+ */
123
+ private before(a: number, b: number): boolean {
124
+ const ka = this.key[a];
125
+ const kb = this.key[b];
126
+ return ka < kb || (this.tieBreakByIndex && ka === kb && a < b);
127
+ }
128
+
82
129
  private siftUp(from: number): void {
83
130
  let at = from;
84
131
  const node = this.heap[at];
85
- const key = this.key[node];
86
132
  while (at > 0) {
87
133
  const parent = (at - 1) >> 1;
88
134
  const parentNode = this.heap[parent];
89
- if (this.key[parentNode] <= key) {
135
+ if (!this.before(node, parentNode)) {
90
136
  break;
91
137
  }
92
138
  this.heap[at] = parentNode;
@@ -100,16 +146,15 @@ export class IndexedMinHeap {
100
146
  private siftDown(from: number): void {
101
147
  let at = from;
102
148
  const node = this.heap[at];
103
- const key = this.key[node];
104
149
  for (;;) {
105
150
  const left = 2 * at + 1;
106
151
  if (left >= this.size) {
107
152
  break;
108
153
  }
109
154
  const right = left + 1;
110
- const child = right < this.size && this.key[this.heap[right]] < this.key[this.heap[left]] ? right : left;
155
+ const child = right < this.size && this.before(this.heap[right], this.heap[left]) ? right : left;
111
156
  const childNode = this.heap[child];
112
- if (key <= this.key[childNode]) {
157
+ if (!this.before(childNode, node)) {
113
158
  break;
114
159
  }
115
160
  this.heap[at] = childNode;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A fixed-capacity FIFO queue of node indices over one `Uint32Array`, for the frontier of a
3
+ * breadth-first sweep that may re-enqueue a node (label propagation, SPFA-style relaxation) and so
4
+ * cannot use the one-pass `order` array plain BFS does. Overflow and underflow throw rather than
5
+ * silently overwrite: a wrong capacity is a bug in the caller.
6
+ */
7
+ export class RingQueue {
8
+ private readonly buffer: Uint32Array;
9
+ private head = 0;
10
+ private count = 0;
11
+
12
+ /**
13
+ * Create an empty queue.
14
+ * @param capacity - The most entries the queue holds at once
15
+ */
16
+ constructor(capacity: number) {
17
+ this.buffer = new Uint32Array(capacity);
18
+ }
19
+
20
+ /**
21
+ * The number of queued entries.
22
+ * @returns The entry count
23
+ */
24
+ get size(): number {
25
+ return this.count;
26
+ }
27
+
28
+ /**
29
+ * Whether the queue holds no entries.
30
+ * @returns True when empty
31
+ */
32
+ isEmpty(): boolean {
33
+ return this.count === 0;
34
+ }
35
+
36
+ /**
37
+ * Append a value at the tail.
38
+ * @param value - A node index
39
+ * @throws RangeError when the queue is full
40
+ */
41
+ push(value: number): void {
42
+ const capacity = this.buffer.length;
43
+ if (this.count === capacity) {
44
+ throw new RangeError(`RingQueue: full at capacity ${String(capacity)}`);
45
+ }
46
+ this.buffer[(this.head + this.count) % capacity] = value;
47
+ this.count++;
48
+ }
49
+
50
+ /**
51
+ * Remove and return the value at the head.
52
+ * @returns The oldest queued node index
53
+ * @throws RangeError when the queue is empty
54
+ */
55
+ shift(): number {
56
+ if (this.count === 0) {
57
+ throw new RangeError("RingQueue: shift on an empty queue");
58
+ }
59
+ const value = this.buffer[this.head];
60
+ this.head = (this.head + 1) % this.buffer.length;
61
+ this.count--;
62
+ return value;
63
+ }
64
+
65
+ /** Drop every entry, keeping the buffer for reuse. */
66
+ clear(): void {
67
+ this.head = 0;
68
+ this.count = 0;
69
+ }
70
+ }
@@ -0,0 +1,229 @@
1
+ import type { F64, GraphSnapshot, U32 } from "@graphty/graph-format";
2
+
3
+ import { SeededRandom } from "../utils/math-utilities.js";
4
+
5
+ /** Options of the index-based SynC clustering, matching the legacy `syncClustering`. @public */
6
+ export interface SyncClusteringOptions {
7
+ /** Number of cluster centres: an integer in `[1, nodeCount]` (legacy rounds a fraction up; the port throws). */
8
+ readonly numClusters: number;
9
+ /** Iteration cap; default 100. */
10
+ readonly maxIterations?: number | undefined;
11
+ /** Stop when the loss changes by less than this between iterations; default 1e-6. */
12
+ readonly tolerance?: number | undefined;
13
+ /** Seed of the embedding initialisation and the centre draws; default 42. */
14
+ readonly seed?: number | undefined;
15
+ /** Gradient step; default 0.01. */
16
+ readonly learningRate?: number | undefined;
17
+ /** Weight of the neighbour-reconstruction and regularisation terms; default 0.1. */
18
+ readonly lambda?: number | undefined;
19
+ }
20
+
21
+ /** Result of the index-based SynC clustering. @public */
22
+ export interface SyncClusteringResult {
23
+ /** The centre each node is closest to, in `[0, count)`. A centre may end up with no node. */
24
+ readonly labels: U32;
25
+ /** Number of cluster centres: `numClusters`. */
26
+ readonly count: number;
27
+ /** Node embeddings, row-major: node i is `embeddings.subarray(i * dimensions, (i + 1) * dimensions)`. */
28
+ readonly embeddings: F64;
29
+ /** Embedding length: `min(64, nodeCount)`. */
30
+ readonly dimensions: number;
31
+ /** Loss of the last iteration run (Infinity when none ran). */
32
+ readonly loss: number;
33
+ /** Loss of the iteration before the last (Infinity when fewer than two ran). */
34
+ readonly previousLoss: number;
35
+ /** Iterations run. */
36
+ readonly iterations: number;
37
+ /** Whether the loss settled within the tolerance before the cap. */
38
+ readonly converged: boolean;
39
+ }
40
+
41
+ /**
42
+ * Euclidean distance between two rows of a row-major matrix.
43
+ * @param a - First matrix
44
+ * @param i - Row of a
45
+ * @param b - Second matrix
46
+ * @param j - Row of b
47
+ * @param dim - Row length
48
+ * @returns The distance
49
+ */
50
+ function distance(a: F64, i: number, b: F64, j: number, dim: number): number {
51
+ let sum = 0;
52
+ for (let k = 0; k < dim; k++) {
53
+ const diff = a[i * dim + k] - b[j * dim + k];
54
+ sum += diff * diff;
55
+ }
56
+ return Math.sqrt(sum);
57
+ }
58
+
59
+ /**
60
+ * SynC over a snapshot: node embeddings seeded from degree and a seeded generator, pulled toward
61
+ * their out-neighbours by gradient steps, and clustered by k-means with k-means++ centres. The
62
+ * arithmetic, and the order of every random draw, are the legacy `syncClustering`'s, so for the same
63
+ * seed the labels and embeddings agree with it to rounding; the port draws from its own generator
64
+ * instead of replacing `Math.random` for the duration of the call.
65
+ *
66
+ * Weights are ignored and a parallel arc pulls once per arc. The seeding degree is legacy's: in plus
67
+ * out when directed, the stored arc count when undirected, so an undirected self-loop counts once.
68
+ * @param s - The snapshot
69
+ * @param options - {@link SyncClusteringOptions}
70
+ * @returns Assignments, embeddings and convergence
71
+ */
72
+ export function syncClustering(s: GraphSnapshot, options: SyncClusteringOptions): SyncClusteringResult {
73
+ const {
74
+ numClusters,
75
+ maxIterations = 100,
76
+ tolerance = 1e-6,
77
+ seed = 42,
78
+ learningRate = 0.01,
79
+ lambda = 0.1,
80
+ } = options;
81
+ const n = s.nodeCount;
82
+ if (n === 0) {
83
+ return {
84
+ labels: new Uint32Array(0),
85
+ count: numClusters,
86
+ embeddings: new Float64Array(0),
87
+ dimensions: 0,
88
+ loss: 0,
89
+ previousLoss: Infinity,
90
+ iterations: 0,
91
+ converged: true,
92
+ };
93
+ }
94
+ if (!Number.isInteger(numClusters) || numClusters <= 0 || numClusters > n) {
95
+ throw new Error(`Invalid number of clusters: ${String(numClusters)}. Must be between 1 and ${String(n)}`);
96
+ }
97
+ const random = SeededRandom.createGenerator(seed);
98
+ const dim = Math.min(64, n);
99
+ const { rowPtr, colIdx } = s;
100
+
101
+ const emb = new Float64Array(n * dim);
102
+ // Legacy's degree: an undirected self-loop counts once (its single stored arc), not twice.
103
+ const degree = s.directed ? s.degree() : s.outDegree();
104
+ for (let u = 0; u < n; u++) {
105
+ const normalized = degree[u] / Math.max(1, n - 1);
106
+ for (let k = 0; k < dim; k++) {
107
+ emb[u * dim + k] = (random() - 0.5) * 0.1 + normalized * 0.1;
108
+ }
109
+ }
110
+
111
+ // k-means++: the first centre uniformly, each next one with probability proportional to the
112
+ // squared distance to the nearest centre so far. A draw that rounding carries past the last
113
+ // node adds no centre, as in legacy, so there may be fewer than numClusters.
114
+ const centres = new Float64Array(numClusters * dim);
115
+ const pick = Math.floor(random() * n);
116
+ centres.set(emb.subarray(pick * dim, (pick + 1) * dim), 0);
117
+ let centreCount = 1;
118
+ const weight = new Float64Array(n);
119
+ for (let k = 1; k < numClusters; k++) {
120
+ let total = 0;
121
+ for (let u = 0; u < n; u++) {
122
+ let nearest = Infinity;
123
+ for (let c = 0; c < centreCount; c++) {
124
+ nearest = Math.min(nearest, distance(emb, u, centres, c, dim));
125
+ }
126
+ weight[u] = nearest * nearest;
127
+ total += nearest * nearest;
128
+ }
129
+ let draw = random() * total;
130
+ for (let u = 0; u < n; u++) {
131
+ draw -= weight[u];
132
+ if (draw <= 0) {
133
+ centres.set(emb.subarray(u * dim, (u + 1) * dim), centreCount * dim);
134
+ centreCount++;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+
140
+ const labels = new Uint32Array(n);
141
+ const assign = (): void => {
142
+ for (let u = 0; u < n; u++) {
143
+ let bestCentre = 0;
144
+ let bestDistance = Infinity;
145
+ for (let c = 0; c < centreCount; c++) {
146
+ const d = distance(emb, u, centres, c, dim);
147
+ if (d < bestDistance) {
148
+ bestDistance = d;
149
+ bestCentre = c;
150
+ }
151
+ }
152
+ labels[u] = bestCentre;
153
+ }
154
+ };
155
+
156
+ const gradient = new Float64Array(n * dim);
157
+ const sums = new Float64Array(numClusters * dim);
158
+ const sizes = new Uint32Array(numClusters);
159
+ let previousLoss = Infinity;
160
+ let loss = Infinity;
161
+ let iterations = 0;
162
+ let converged = false;
163
+ while (iterations < maxIterations) {
164
+ iterations++;
165
+ assign();
166
+
167
+ // Every gradient from the embeddings before this step, then one step for all.
168
+ gradient.fill(0);
169
+ for (let u = 0; u < n; u++) {
170
+ const row = u * dim;
171
+ for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
172
+ const other = colIdx[a] * dim;
173
+ for (let k = 0; k < dim; k++) {
174
+ gradient[row + k] += lambda * (emb[row + k] - emb[other + k]);
175
+ }
176
+ }
177
+ for (let k = 0; k < dim; k++) {
178
+ gradient[row + k] += lambda * emb[row + k];
179
+ }
180
+ }
181
+ for (let i = 0; i < emb.length; i++) {
182
+ emb[i] -= learningRate * gradient[i];
183
+ }
184
+
185
+ sums.fill(0);
186
+ sizes.fill(0);
187
+ for (let u = 0; u < n; u++) {
188
+ const c = labels[u];
189
+ for (let k = 0; k < dim; k++) {
190
+ sums[c * dim + k] += emb[u * dim + k];
191
+ }
192
+ sizes[c]++;
193
+ }
194
+ for (let c = 0; c < centreCount; c++) {
195
+ if (sizes[c] > 0) {
196
+ for (let k = 0; k < dim; k++) {
197
+ centres[c * dim + k] = sums[c * dim + k] / sizes[c];
198
+ }
199
+ }
200
+ }
201
+
202
+ let clustering = 0;
203
+ let reconstruction = 0;
204
+ let regularization = 0;
205
+ for (let u = 0; u < n; u++) {
206
+ clustering += distance(emb, u, centres, labels[u], dim) ** 2;
207
+ }
208
+ for (let u = 0; u < n; u++) {
209
+ for (let a = rowPtr[u]; a < rowPtr[u + 1]; a++) {
210
+ reconstruction += distance(emb, u, emb, colIdx[a], dim) ** 2;
211
+ }
212
+ }
213
+ for (const value of emb) {
214
+ regularization += value ** 2;
215
+ }
216
+ const current = clustering + lambda * reconstruction + lambda * regularization;
217
+ if (iterations > 1) {
218
+ previousLoss = loss;
219
+ }
220
+ loss = current;
221
+ if (Math.abs(previousLoss - loss) < tolerance) {
222
+ converged = true;
223
+ break;
224
+ }
225
+ }
226
+ assign();
227
+
228
+ return { labels, count: numClusters, embeddings: emb, dimensions: dim, loss, previousLoss, iterations, converged };
229
+ }
@@ -0,0 +1,290 @@
1
+ import { type F64, type GraphSnapshot, renumberPartition, type U32 } from "@graphty/graph-format";
2
+
3
+ import { type LabelResult, withGroups } from "./components.js";
4
+
5
+ /** Options of the index-based TeraHAC, matching the legacy `teraHAC`. @public */
6
+ export interface TeraHacOptions {
7
+ /**
8
+ * How the distance between two clusters is read from their members' pairwise distances: the
9
+ * minimum, the maximum, the mean, or (`"ward"`) the mean of the squares; default `"average"`.
10
+ */
11
+ readonly linkage?: "single" | "complete" | "average" | "ward" | undefined;
12
+ /** Stop merging at this many clusters; a positive integer. Default: merge until one is left. */
13
+ readonly numClusters?: number | undefined;
14
+ /** Stop at the first merge whose linkage distance is above this. Default: no threshold. */
15
+ readonly distanceThreshold?: number | undefined;
16
+ /**
17
+ * Pairwise distance: hop count along out-arcs (true, the default), or 1 for an arc from the
18
+ * lower to the higher node index and 2 for none (false).
19
+ */
20
+ readonly useGraphDistance?: boolean | undefined;
21
+ }
22
+
23
+ /**
24
+ * Result of the index-based TeraHAC: the flat clustering plus the dendrogram. Dendrogram node
25
+ * `i < n` is node index i; node `n + k` is the k-th join, with children `left[k]` and `right[k]`.
26
+ * @public
27
+ */
28
+ export interface TeraHacResult extends LabelResult {
29
+ /** Left child of each join (the legacy `ClusterNode.left`). */
30
+ readonly left: U32;
31
+ /** Right child of each join. */
32
+ readonly right: U32;
33
+ /** Linkage distance of each join; Infinity for the joins that tie the remaining clusters under one root. */
34
+ readonly distance: F64;
35
+ /** Node count under each join. */
36
+ readonly size: U32;
37
+ /** Joins made by distance: `distance.subarray(0, merges)` is the legacy `distances` list. */
38
+ readonly merges: number;
39
+ /** The dendrogram root: `2n - 2`, or 0 for a single node. */
40
+ readonly root: number;
41
+ }
42
+
43
+ /** The distance legacy gives an unreachable pair, so that disconnected clusters still merge last. */
44
+ const UNREACHABLE = 100;
45
+
46
+ /**
47
+ * All-pairs distances into a row-major n x n matrix: BFS hop counts along out-arcs (Infinity when
48
+ * unreachable) or the 1 / 2 adjacency distance.
49
+ * @param s - The snapshot
50
+ * @param useGraphDistance - Which distance
51
+ * @returns The matrix
52
+ */
53
+ function pairwiseDistances(s: GraphSnapshot, useGraphDistance: boolean): Float64Array {
54
+ const n = s.nodeCount;
55
+ const d = new Float64Array(n * n);
56
+ if (!useGraphDistance) {
57
+ for (let i = 0; i < n; i++) {
58
+ for (let j = i + 1; j < n; j++) {
59
+ d[i * n + j] = d[j * n + i] = s.hasArc(i, j) ? 1 : 2;
60
+ }
61
+ }
62
+ return d;
63
+ }
64
+ d.fill(Infinity);
65
+ const queue = new Uint32Array(n);
66
+ for (let source = 0; source < n; source++) {
67
+ const row = source * n;
68
+ d[row + source] = 0;
69
+ queue[0] = source;
70
+ let head = 0;
71
+ let tail = 1;
72
+ while (head < tail) {
73
+ const u = queue[head++];
74
+ const next = d[row + u] + 1;
75
+ for (let a = s.rowPtr[u]; a < s.rowPtr[u + 1]; a++) {
76
+ const v = s.colIdx[a];
77
+ if (d[row + v] === Infinity) {
78
+ d[row + v] = next;
79
+ queue[tail++] = v;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ return d;
85
+ }
86
+
87
+ /**
88
+ * TeraHAC over a snapshot: agglomerative clustering that repeatedly merges the closest pair of
89
+ * clusters, with the same distances, merge order and tie-breaks as the legacy `teraHAC` -- but
90
+ * over node indices, so the result no longer depends on what the node ids are.
91
+ *
92
+ * The linkage is kept per ordered cluster pair as an aggregate (minimum, maximum, sum of distances
93
+ * or sum of squared distances) and updated on each merge, instead of rescanning every member pair;
94
+ * the pairwise distances are integers, so the sums, and the means divided out of them, are exact.
95
+ * A pair's distance reads the aggregate in the orientation legacy reads it, which matters on a
96
+ * directed snapshot, and ties go to the pair legacy's stable sort puts first: two original nodes
97
+ * before any pair with a merged cluster, and among those the most recent merge last.
98
+ *
99
+ * Weights are ignored (legacy counts hops). O(n^2) memory and O(n (n + m)) for the distances; the
100
+ * merging keeps each cluster's closest partner and rescans a row only when that partner merges.
101
+ * @param s - The snapshot
102
+ * @param options - {@link TeraHacOptions}
103
+ * @returns The flat clustering and the dendrogram
104
+ */
105
+ export function teraHAC(s: GraphSnapshot, options: TeraHacOptions = {}): TeraHacResult {
106
+ const { linkage = "average", numClusters, distanceThreshold, useGraphDistance = true } = options;
107
+ if (numClusters !== undefined && (numClusters < 1 || !Number.isInteger(numClusters))) {
108
+ throw new Error("numClusters must be a positive integer");
109
+ }
110
+ const n = s.nodeCount;
111
+ if (n === 0) {
112
+ throw new Error("Cannot cluster empty graph");
113
+ }
114
+
115
+ // ponytail: dense n x n aggregate matrix, the same memory as legacy's distance matrix; a sparse
116
+ // candidate structure is the upgrade if graphs past ~10k nodes need this.
117
+ const agg = pairwiseDistances(s, useGraphDistance);
118
+ if (linkage === "ward") {
119
+ for (let i = 0; i < agg.length; i++) {
120
+ agg[i] *= agg[i];
121
+ }
122
+ }
123
+ const extremes = { single: Math.min, complete: Math.max, average: null, ward: null };
124
+ const extreme = extremes[linkage];
125
+
126
+ const joins = Math.max(0, n - 1);
127
+ const left = new Uint32Array(joins);
128
+ const right = new Uint32Array(joins);
129
+ const distance = new Float64Array(joins);
130
+ const size = new Uint32Array(joins);
131
+
132
+ // Slot i holds one active cluster: its dendrogram id, its size, and its closest partner.
133
+ const clusterId = new Uint32Array(n);
134
+ const members = new Uint32Array(n).fill(1);
135
+ const active = new Uint8Array(n).fill(1);
136
+ for (let i = 0; i < n; i++) {
137
+ clusterId[i] = i;
138
+ }
139
+ const orient = (x: number, y: number): boolean => {
140
+ // true when x is the pair's first cluster: the lower id of two originals, else the newer one
141
+ const a = clusterId[x];
142
+ const b = clusterId[y];
143
+ return a < n && b < n ? a < b : a > b;
144
+ };
145
+ const pairDistance = (x: number, y: number): number => {
146
+ const [p, q] = orient(x, y) ? [x, y] : [y, x];
147
+ let value = agg[p * n + q];
148
+ if (clusterId[p] < n && clusterId[q] < n) {
149
+ // Legacy reads two original nodes' distance straight from the matrix, unsquared even for ward.
150
+ value = linkage === "ward" ? Math.sqrt(value) : value;
151
+ } else if (!extreme) {
152
+ value /= members[p] * members[q];
153
+ }
154
+ return value === Infinity ? UNREACHABLE : value;
155
+ };
156
+ const pairOrder = (x: number, y: number): number => {
157
+ const a = clusterId[x];
158
+ const b = clusterId[y];
159
+ const lo = Math.min(a, b);
160
+ const hi = Math.max(a, b);
161
+ return hi < n ? lo * n + hi : n * n + hi * 2 * n + lo;
162
+ };
163
+ const best = new Int32Array(n).fill(-1);
164
+ const bestDistance = new Float64Array(n);
165
+ const bestOrder = new Float64Array(n);
166
+ const offer = (x: number, y: number): void => {
167
+ const d = pairDistance(x, y);
168
+ const o = pairOrder(x, y);
169
+ if (best[x] === -1 || d < bestDistance[x] || (d === bestDistance[x] && o < bestOrder[x])) {
170
+ best[x] = y;
171
+ bestDistance[x] = d;
172
+ bestOrder[x] = o;
173
+ }
174
+ };
175
+ const rescan = (x: number): void => {
176
+ best[x] = -1;
177
+ for (let y = 0; y < n; y++) {
178
+ if (active[y] && y !== x) {
179
+ offer(x, y);
180
+ }
181
+ }
182
+ };
183
+ for (let x = 0; x < n; x++) {
184
+ rescan(x);
185
+ }
186
+
187
+ let remaining = n;
188
+ let merges = 0;
189
+ while (remaining > 1 && (numClusters === undefined || remaining > numClusters)) {
190
+ let x = -1;
191
+ for (let i = 0; i < n; i++) {
192
+ if (
193
+ active[i] &&
194
+ (x === -1 ||
195
+ bestDistance[i] < bestDistance[x] ||
196
+ (bestDistance[i] === bestDistance[x] && bestOrder[i] < bestOrder[x]))
197
+ ) {
198
+ x = i;
199
+ }
200
+ }
201
+ const d = bestDistance[x];
202
+ if (distanceThreshold !== undefined && d > distanceThreshold) {
203
+ break;
204
+ }
205
+ const [keep, drop] = orient(x, best[x]) ? [x, best[x]] : [best[x], x];
206
+ left[merges] = clusterId[keep];
207
+ right[merges] = clusterId[drop];
208
+ distance[merges] = d;
209
+ size[merges] = members[keep] + members[drop];
210
+ for (let c = 0; c < n; c++) {
211
+ if (active[c] && c !== keep && c !== drop) {
212
+ const kc = keep * n + c;
213
+ const dc = drop * n + c;
214
+ const ck = c * n + keep;
215
+ const cd = c * n + drop;
216
+ agg[kc] = extreme ? extreme(agg[kc], agg[dc]) : agg[kc] + agg[dc];
217
+ agg[ck] = extreme ? extreme(agg[ck], agg[cd]) : agg[ck] + agg[cd];
218
+ }
219
+ }
220
+ clusterId[keep] = n + merges;
221
+ members[keep] = size[merges];
222
+ active[drop] = 0;
223
+ merges++;
224
+ remaining--;
225
+ rescan(keep);
226
+ for (let c = 0; c < n; c++) {
227
+ if (active[c] && c !== keep) {
228
+ if (best[c] === keep || best[c] === drop) {
229
+ rescan(c);
230
+ } else {
231
+ offer(c, keep);
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ // Tie what is left under one root, in ascending cluster id, as legacy does.
238
+ const rest = Array.from(clusterId.filter((_, i) => active[i] === 1)).sort((a, b) => a - b);
239
+ let root = rest[0];
240
+ const sizeOf = (c: number): number => (c < n ? 1 : size[c - n]);
241
+ for (let k = 1; k < rest.length; k++) {
242
+ const j = merges + k - 1;
243
+ left[j] = root;
244
+ right[j] = rest[k];
245
+ distance[j] = Infinity;
246
+ size[j] = sizeOf(root) + sizeOf(rest[k]);
247
+ root = n + j;
248
+ }
249
+
250
+ const cut = cutDendrogram(n, left, right, root, numClusters ?? remaining);
251
+ const { labels, count } = renumberPartition(cut);
252
+ return { ...withGroups(labels, count), left, right, distance, size, merges, root };
253
+ }
254
+
255
+ /**
256
+ * Legacy's flat clustering: split dendrogram nodes breadth-first from the root until splitting the
257
+ * next one would exceed `k` clusters, then label every node by the cluster above it.
258
+ * @param n - Node count
259
+ * @param left - Left child of each join
260
+ * @param right - Right child of each join
261
+ * @param root - The root
262
+ * @param k - The cluster count asked for
263
+ * @returns A cluster number per node index
264
+ */
265
+ function cutDendrogram(n: number, left: U32, right: U32, root: number, k: number): U32 {
266
+ const kept: number[] = [];
267
+ const queue = [root];
268
+ let head = 0;
269
+ while (head < queue.length && kept.length < k) {
270
+ const c = queue[head++];
271
+ if (c < n || k === 1 || kept.length + (queue.length - head) + 1 >= k) {
272
+ kept.push(c);
273
+ } else {
274
+ queue.push(left[c - n], right[c - n]);
275
+ }
276
+ }
277
+ const labels = new Uint32Array(n);
278
+ kept.forEach((top, label) => {
279
+ const stack = [top];
280
+ while (stack.length > 0) {
281
+ const c = stack.pop() ?? 0;
282
+ if (c < n) {
283
+ labels[c] = label;
284
+ } else {
285
+ stack.push(left[c - n], right[c - n]);
286
+ }
287
+ }
288
+ });
289
+ return labels;
290
+ }