@graphty/algorithms 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +1259 -61
  2. package/dist/algorithms.d.ts +2 -0
  3. package/dist/algorithms.d.ts.map +1 -0
  4. package/dist/algorithms.js +2 -0
  5. package/dist/algorithms.js.map +1 -0
  6. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  7. package/dist/src/algorithms/centrality/betweenness.js +22 -7
  8. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  9. package/dist/src/algorithms/centrality/closeness.js +7 -7
  10. package/dist/src/algorithms/centrality/closeness.js.map +1 -1
  11. package/dist/src/algorithms/centrality/degree.js +1 -1
  12. package/dist/src/algorithms/centrality/degree.js.map +1 -1
  13. package/dist/src/algorithms/centrality/eigenvector.d.ts +27 -0
  14. package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -0
  15. package/dist/src/algorithms/centrality/eigenvector.js +113 -0
  16. package/dist/src/algorithms/centrality/eigenvector.js.map +1 -0
  17. package/dist/src/algorithms/centrality/hits.d.ts +35 -0
  18. package/dist/src/algorithms/centrality/hits.d.ts.map +1 -0
  19. package/dist/src/algorithms/centrality/hits.js +146 -0
  20. package/dist/src/algorithms/centrality/hits.js.map +1 -0
  21. package/dist/src/algorithms/centrality/index.d.ts +6 -0
  22. package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
  23. package/dist/src/algorithms/centrality/index.js +3 -0
  24. package/dist/src/algorithms/centrality/index.js.map +1 -1
  25. package/dist/src/algorithms/centrality/katz.d.ts +30 -0
  26. package/dist/src/algorithms/centrality/katz.d.ts.map +1 -0
  27. package/dist/src/algorithms/centrality/katz.js +85 -0
  28. package/dist/src/algorithms/centrality/katz.js.map +1 -0
  29. package/dist/src/algorithms/centrality/pagerank.js +3 -3
  30. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  31. package/dist/src/algorithms/community/girvan-newman.d.ts +23 -0
  32. package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -0
  33. package/dist/src/algorithms/community/girvan-newman.js +297 -0
  34. package/dist/src/algorithms/community/girvan-newman.js.map +1 -0
  35. package/dist/src/algorithms/community/index.d.ts +12 -0
  36. package/dist/src/algorithms/community/index.d.ts.map +1 -0
  37. package/dist/src/algorithms/community/index.js +12 -0
  38. package/dist/src/algorithms/community/index.js.map +1 -0
  39. package/dist/src/algorithms/community/label-propagation.d.ts +42 -0
  40. package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -0
  41. package/dist/src/algorithms/community/label-propagation.js +307 -0
  42. package/dist/src/algorithms/community/label-propagation.js.map +1 -0
  43. package/dist/src/algorithms/community/leiden.d.ts +33 -0
  44. package/dist/src/algorithms/community/leiden.d.ts.map +1 -0
  45. package/dist/src/algorithms/community/leiden.js +434 -0
  46. package/dist/src/algorithms/community/leiden.js.map +1 -0
  47. package/dist/src/algorithms/community/louvain.d.ts +19 -0
  48. package/dist/src/algorithms/community/louvain.d.ts.map +1 -0
  49. package/dist/src/algorithms/community/louvain.js +219 -0
  50. package/dist/src/algorithms/community/louvain.js.map +1 -0
  51. package/dist/src/algorithms/components/connected.js +7 -7
  52. package/dist/src/algorithms/components/connected.js.map +1 -1
  53. package/dist/src/algorithms/index.d.ts +7 -0
  54. package/dist/src/algorithms/index.d.ts.map +1 -1
  55. package/dist/src/algorithms/index.js +14 -0
  56. package/dist/src/algorithms/index.js.map +1 -1
  57. package/dist/src/algorithms/matching/bipartite.d.ts +37 -0
  58. package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -0
  59. package/dist/src/algorithms/matching/bipartite.js +127 -0
  60. package/dist/src/algorithms/matching/bipartite.js.map +1 -0
  61. package/dist/src/algorithms/matching/index.d.ts +8 -0
  62. package/dist/src/algorithms/matching/index.d.ts.map +1 -0
  63. package/dist/src/algorithms/matching/index.js +6 -0
  64. package/dist/src/algorithms/matching/index.js.map +1 -0
  65. package/dist/src/algorithms/matching/isomorphism.d.ts +32 -0
  66. package/dist/src/algorithms/matching/isomorphism.d.ts.map +1 -0
  67. package/dist/src/algorithms/matching/isomorphism.js +291 -0
  68. package/dist/src/algorithms/matching/isomorphism.js.map +1 -0
  69. package/dist/src/algorithms/mst/index.d.ts +4 -0
  70. package/dist/src/algorithms/mst/index.d.ts.map +1 -0
  71. package/dist/src/algorithms/mst/index.js +3 -0
  72. package/dist/src/algorithms/mst/index.js.map +1 -0
  73. package/dist/src/algorithms/mst/kruskal.d.ts +9 -0
  74. package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -0
  75. package/dist/src/algorithms/mst/kruskal.js +43 -0
  76. package/dist/src/algorithms/mst/kruskal.js.map +1 -0
  77. package/dist/src/algorithms/mst/prim.d.ts +5 -0
  78. package/dist/src/algorithms/mst/prim.d.ts.map +1 -0
  79. package/dist/src/algorithms/mst/prim.js +63 -0
  80. package/dist/src/algorithms/mst/prim.js.map +1 -0
  81. package/dist/src/algorithms/shortest-path/bellman-ford.js +2 -2
  82. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  83. package/dist/src/algorithms/shortest-path/dijkstra.js +5 -5
  84. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  85. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +14 -0
  86. package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -0
  87. package/dist/src/algorithms/shortest-path/floyd-warshall.js +129 -0
  88. package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -0
  89. package/dist/src/algorithms/shortest-path/index.d.ts +2 -0
  90. package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -1
  91. package/dist/src/algorithms/shortest-path/index.js +1 -0
  92. package/dist/src/algorithms/shortest-path/index.js.map +1 -1
  93. package/dist/src/algorithms/traversal/bfs.js +2 -2
  94. package/dist/src/algorithms/traversal/bfs.js.map +1 -1
  95. package/dist/src/algorithms/traversal/dfs.js +11 -11
  96. package/dist/src/algorithms/traversal/dfs.js.map +1 -1
  97. package/dist/src/clustering/hierarchical.d.ts +48 -0
  98. package/dist/src/clustering/hierarchical.d.ts.map +1 -0
  99. package/dist/src/clustering/hierarchical.js +439 -0
  100. package/dist/src/clustering/hierarchical.js.map +1 -0
  101. package/dist/src/clustering/index.d.ts +7 -0
  102. package/dist/src/clustering/index.d.ts.map +1 -0
  103. package/dist/src/clustering/index.js +5 -0
  104. package/dist/src/clustering/index.js.map +1 -0
  105. package/dist/src/clustering/k-core.d.ts +63 -0
  106. package/dist/src/clustering/k-core.d.ts.map +1 -0
  107. package/dist/src/clustering/k-core.js +377 -0
  108. package/dist/src/clustering/k-core.js.map +1 -0
  109. package/dist/src/clustering/mcl.d.ts +36 -0
  110. package/dist/src/clustering/mcl.d.ts.map +1 -0
  111. package/dist/src/clustering/mcl.js +343 -0
  112. package/dist/src/clustering/mcl.js.map +1 -0
  113. package/dist/src/clustering/spectral.d.ts +28 -0
  114. package/dist/src/clustering/spectral.d.ts.map +1 -0
  115. package/dist/src/clustering/spectral.js +551 -0
  116. package/dist/src/clustering/spectral.js.map +1 -0
  117. package/dist/src/core/graph.d.ts +9 -0
  118. package/dist/src/core/graph.d.ts.map +1 -1
  119. package/dist/src/core/graph.js +31 -11
  120. package/dist/src/core/graph.js.map +1 -1
  121. package/dist/src/data-structures/priority-queue.js +0 -2
  122. package/dist/src/data-structures/priority-queue.js.map +1 -1
  123. package/dist/src/data-structures/union-find.js +2 -5
  124. package/dist/src/data-structures/union-find.js.map +1 -1
  125. package/dist/src/flow/ford-fulkerson.d.ts +53 -0
  126. package/dist/src/flow/ford-fulkerson.d.ts.map +1 -0
  127. package/dist/src/flow/ford-fulkerson.js +355 -0
  128. package/dist/src/flow/ford-fulkerson.js.map +1 -0
  129. package/dist/src/flow/index.d.ts +3 -0
  130. package/dist/src/flow/index.d.ts.map +1 -0
  131. package/dist/src/flow/index.js +3 -0
  132. package/dist/src/flow/index.js.map +1 -0
  133. package/dist/src/flow/min-cut.d.ts +49 -0
  134. package/dist/src/flow/min-cut.d.ts.map +1 -0
  135. package/dist/src/flow/min-cut.js +383 -0
  136. package/dist/src/flow/min-cut.js.map +1 -0
  137. package/dist/src/index.d.ts +2 -1
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +2 -0
  140. package/dist/src/index.js.map +1 -1
  141. package/dist/src/link-prediction/adamic-adar.d.ts +56 -0
  142. package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -0
  143. package/dist/src/link-prediction/adamic-adar.js +256 -0
  144. package/dist/src/link-prediction/adamic-adar.js.map +1 -0
  145. package/dist/src/link-prediction/common-neighbors.d.ts +50 -0
  146. package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -0
  147. package/dist/src/link-prediction/common-neighbors.js +155 -0
  148. package/dist/src/link-prediction/common-neighbors.js.map +1 -0
  149. package/dist/src/link-prediction/index.d.ts +7 -0
  150. package/dist/src/link-prediction/index.d.ts.map +1 -0
  151. package/dist/src/link-prediction/index.js +6 -0
  152. package/dist/src/link-prediction/index.js.map +1 -0
  153. package/dist/src/pathfinding/astar.d.ts +50 -0
  154. package/dist/src/pathfinding/astar.d.ts.map +1 -0
  155. package/dist/src/pathfinding/astar.js +180 -0
  156. package/dist/src/pathfinding/astar.js.map +1 -0
  157. package/dist/src/pathfinding/index.d.ts +3 -0
  158. package/dist/src/pathfinding/index.d.ts.map +1 -0
  159. package/dist/src/pathfinding/index.js +3 -0
  160. package/dist/src/pathfinding/index.js.map +1 -0
  161. package/dist/src/pathfinding/utils.d.ts +18 -0
  162. package/dist/src/pathfinding/utils.d.ts.map +1 -0
  163. package/dist/src/pathfinding/utils.js +68 -0
  164. package/dist/src/pathfinding/utils.js.map +1 -0
  165. package/dist/src/research/grsbm.d.ts +83 -0
  166. package/dist/src/research/grsbm.d.ts.map +1 -0
  167. package/dist/src/research/grsbm.js +411 -0
  168. package/dist/src/research/grsbm.js.map +1 -0
  169. package/dist/src/research/index.d.ts +12 -0
  170. package/dist/src/research/index.d.ts.map +1 -0
  171. package/dist/src/research/index.js +14 -0
  172. package/dist/src/research/index.js.map +1 -0
  173. package/dist/src/research/sync.d.ts +49 -0
  174. package/dist/src/research/sync.d.ts.map +1 -0
  175. package/dist/src/research/sync.js +342 -0
  176. package/dist/src/research/sync.js.map +1 -0
  177. package/dist/src/research/terahac.d.ts +63 -0
  178. package/dist/src/research/terahac.d.ts.map +1 -0
  179. package/dist/src/research/terahac.js +375 -0
  180. package/dist/src/research/terahac.js.map +1 -0
  181. package/dist/src/types/index.d.ts +9 -0
  182. package/dist/src/types/index.d.ts.map +1 -1
  183. package/dist/src/utils/graphNode.d.ts +8 -0
  184. package/dist/src/utils/graphNode.d.ts.map +1 -0
  185. package/dist/src/utils/graphNode.js +2 -0
  186. package/dist/src/utils/graphNode.js.map +1 -0
  187. package/dist/src/utils/priorityQueue.d.ts +21 -0
  188. package/dist/src/utils/priorityQueue.d.ts.map +1 -0
  189. package/dist/src/utils/priorityQueue.js +101 -0
  190. package/dist/src/utils/priorityQueue.js.map +1 -0
  191. package/package.json +13 -2
  192. package/src/algorithms/centrality/betweenness.ts +25 -7
  193. package/src/algorithms/centrality/closeness.ts +7 -7
  194. package/src/algorithms/centrality/degree.ts +1 -1
  195. package/src/algorithms/centrality/eigenvector.ts +162 -0
  196. package/src/algorithms/centrality/hits.ts +210 -0
  197. package/src/algorithms/centrality/index.ts +6 -0
  198. package/src/algorithms/centrality/katz.ts +139 -0
  199. package/src/algorithms/centrality/pagerank.ts +3 -3
  200. package/src/algorithms/community/girvan-newman.ts +372 -0
  201. package/src/algorithms/community/index.ts +13 -0
  202. package/src/algorithms/community/label-propagation.ts +392 -0
  203. package/src/algorithms/community/leiden.ts +572 -0
  204. package/src/algorithms/community/louvain.ts +298 -0
  205. package/src/algorithms/components/connected.ts +7 -7
  206. package/src/algorithms/index.ts +21 -0
  207. package/src/algorithms/matching/bipartite.ts +179 -0
  208. package/src/algorithms/matching/index.ts +8 -0
  209. package/src/algorithms/matching/isomorphism.ts +392 -0
  210. package/src/algorithms/mst/index.ts +4 -0
  211. package/src/algorithms/mst/kruskal.ts +61 -0
  212. package/src/algorithms/mst/prim.ts +80 -0
  213. package/src/algorithms/shortest-path/bellman-ford.ts +2 -2
  214. package/src/algorithms/shortest-path/dijkstra.ts +5 -5
  215. package/src/algorithms/shortest-path/floyd-warshall.ts +170 -0
  216. package/src/algorithms/shortest-path/index.ts +2 -0
  217. package/src/algorithms/traversal/bfs.ts +2 -2
  218. package/src/algorithms/traversal/dfs.ts +11 -11
  219. package/src/clustering/hierarchical.ts +555 -0
  220. package/src/clustering/index.ts +6 -0
  221. package/src/clustering/k-core.ts +460 -0
  222. package/src/clustering/mcl.ts +454 -0
  223. package/src/clustering/spectral.ts +672 -0
  224. package/src/core/graph.ts +34 -6
  225. package/src/data-structures/union-find.ts +2 -2
  226. package/src/flow/ford-fulkerson.ts +459 -0
  227. package/src/flow/index.ts +2 -0
  228. package/src/flow/min-cut.ts +482 -0
  229. package/src/index.ts +5 -0
  230. package/src/link-prediction/adamic-adar.ts +353 -0
  231. package/src/link-prediction/common-neighbors.ts +252 -0
  232. package/src/link-prediction/index.ts +20 -0
  233. package/src/pathfinding/astar.ts +229 -0
  234. package/src/pathfinding/index.ts +2 -0
  235. package/src/pathfinding/utils.ts +87 -0
  236. package/src/research/grsbm.ts +589 -0
  237. package/src/research/index.ts +17 -0
  238. package/src/research/sync.ts +469 -0
  239. package/src/research/terahac.ts +506 -0
  240. package/src/types/index.ts +11 -0
  241. package/src/utils/graphNode.ts +7 -0
  242. package/src/utils/priorityQueue.ts +120 -0
  243. package/dist/test/browser/basic.test.d.ts +0 -2
  244. package/dist/test/browser/basic.test.d.ts.map +0 -1
  245. package/dist/test/browser/basic.test.js +0 -70
  246. package/dist/test/browser/basic.test.js.map +0 -1
  247. package/dist/test/unit/bellman-ford.test.d.ts +0 -2
  248. package/dist/test/unit/bellman-ford.test.d.ts.map +0 -1
  249. package/dist/test/unit/bellman-ford.test.js +0 -231
  250. package/dist/test/unit/bellman-ford.test.js.map +0 -1
  251. package/dist/test/unit/betweenness-centrality.test.d.ts +0 -2
  252. package/dist/test/unit/betweenness-centrality.test.d.ts.map +0 -1
  253. package/dist/test/unit/betweenness-centrality.test.js +0 -311
  254. package/dist/test/unit/betweenness-centrality.test.js.map +0 -1
  255. package/dist/test/unit/bfs.test.d.ts +0 -2
  256. package/dist/test/unit/bfs.test.d.ts.map +0 -1
  257. package/dist/test/unit/bfs.test.js +0 -277
  258. package/dist/test/unit/bfs.test.js.map +0 -1
  259. package/dist/test/unit/closeness.test.d.ts +0 -2
  260. package/dist/test/unit/closeness.test.d.ts.map +0 -1
  261. package/dist/test/unit/closeness.test.js +0 -266
  262. package/dist/test/unit/closeness.test.js.map +0 -1
  263. package/dist/test/unit/connected-components.test.d.ts +0 -2
  264. package/dist/test/unit/connected-components.test.d.ts.map +0 -1
  265. package/dist/test/unit/connected-components.test.js +0 -355
  266. package/dist/test/unit/connected-components.test.js.map +0 -1
  267. package/dist/test/unit/degree-centrality.test.d.ts +0 -2
  268. package/dist/test/unit/degree-centrality.test.d.ts.map +0 -1
  269. package/dist/test/unit/degree-centrality.test.js +0 -260
  270. package/dist/test/unit/degree-centrality.test.js.map +0 -1
  271. package/dist/test/unit/dfs.test.d.ts +0 -2
  272. package/dist/test/unit/dfs.test.d.ts.map +0 -1
  273. package/dist/test/unit/dfs.test.js +0 -350
  274. package/dist/test/unit/dfs.test.js.map +0 -1
  275. package/dist/test/unit/dijkstra.test.d.ts +0 -2
  276. package/dist/test/unit/dijkstra.test.d.ts.map +0 -1
  277. package/dist/test/unit/dijkstra.test.js +0 -311
  278. package/dist/test/unit/dijkstra.test.js.map +0 -1
  279. package/dist/test/unit/graph.test.d.ts +0 -2
  280. package/dist/test/unit/graph.test.d.ts.map +0 -1
  281. package/dist/test/unit/graph.test.js +0 -175
  282. package/dist/test/unit/graph.test.js.map +0 -1
  283. package/dist/test/unit/pagerank.test.d.ts +0 -2
  284. package/dist/test/unit/pagerank.test.d.ts.map +0 -1
  285. package/dist/test/unit/pagerank.test.js +0 -281
  286. package/dist/test/unit/pagerank.test.js.map +0 -1
  287. package/dist/test/unit/priority-queue.test.d.ts +0 -2
  288. package/dist/test/unit/priority-queue.test.d.ts.map +0 -1
  289. package/dist/test/unit/priority-queue.test.js +0 -189
  290. package/dist/test/unit/priority-queue.test.js.map +0 -1
  291. package/dist/test/unit/union-find.test.d.ts +0 -2
  292. package/dist/test/unit/union-find.test.d.ts.map +0 -1
  293. package/dist/test/unit/union-find.test.js +0 -245
  294. package/dist/test/unit/union-find.test.js.map +0 -1
@@ -0,0 +1,589 @@
1
+ import type {Graph} from "../core/graph.js";
2
+ import type {NodeId} from "../types/index.js";
3
+
4
+ /**
5
+ * Configuration options for the GRSBM (Greedy Recursive Spectral Bisection) algorithm
6
+ */
7
+ export interface GRSBMConfig {
8
+ /** Maximum depth of recursive bisection */
9
+ maxDepth?: number;
10
+ /** Minimum cluster size to continue splitting */
11
+ minClusterSize?: number;
12
+ /** Number of eigenvectors to use for spectral embedding */
13
+ numEigenvectors?: number;
14
+ /** Convergence tolerance for eigenvalue computation */
15
+ tolerance?: number;
16
+ /** Maximum number of power iterations for eigenvalue computation */
17
+ maxIterations?: number;
18
+ /** Random seed for reproducibility */
19
+ seed?: number;
20
+ }
21
+
22
+ /**
23
+ * Represents a cluster in the hierarchical structure
24
+ */
25
+ export interface GRSBMCluster {
26
+ /** Unique identifier for this cluster */
27
+ id: string;
28
+ /** Node IDs in this cluster */
29
+ members: Set<NodeId>;
30
+ /** Left child cluster (if split) */
31
+ left?: GRSBMCluster;
32
+ /** Right child cluster (if split) */
33
+ right?: GRSBMCluster;
34
+ /** Modularity score of this cluster */
35
+ modularity: number;
36
+ /** Depth in the hierarchy */
37
+ depth: number;
38
+ /** Spectral embedding quality score */
39
+ spectralScore: number;
40
+ }
41
+
42
+ /**
43
+ * Result of the GRSBM clustering algorithm
44
+ */
45
+ export interface GRSBMResult {
46
+ /** Root of the cluster hierarchy */
47
+ root: GRSBMCluster;
48
+ /** Flat clustering at leaf level */
49
+ clusters: Map<NodeId, number>;
50
+ /** Number of final clusters */
51
+ numClusters: number;
52
+ /** Modularity scores for each level */
53
+ modularityScores: number[];
54
+ /** Explanation of the clustering structure */
55
+ explanation: ClusterExplanation[];
56
+ }
57
+
58
+ /**
59
+ * Explanation for a cluster split decision
60
+ */
61
+ export interface ClusterExplanation {
62
+ /** Cluster that was split */
63
+ clusterId: string;
64
+ /** Reason for the split */
65
+ reason: string;
66
+ /** Modularity improvement from the split */
67
+ modularityImprovement: number;
68
+ /** Key nodes that influenced the split */
69
+ keyNodes: NodeId[];
70
+ /** Spectral embedding values */
71
+ spectralValues: number[];
72
+ }
73
+
74
+ /**
75
+ * GRSBM - Greedy Recursive Spectral Bisection with Modularity
76
+ *
77
+ * This algorithm performs hierarchical community detection using spectral
78
+ * bisection guided by modularity optimization. It provides explainable
79
+ * community structure by tracking the reasoning behind each split.
80
+ *
81
+ * Based on: "Explainable Community Detection via Hierarchical Spectral Clustering" (2024)
82
+ *
83
+ * @param graph - Input graph to cluster
84
+ * @param config - Configuration options
85
+ * @returns Hierarchical clustering result with explanations
86
+ */
87
+ export function grsbm(graph: Graph, config: GRSBMConfig = {}): GRSBMResult {
88
+ const {
89
+ maxDepth = 10,
90
+ minClusterSize = 2, // Reduced default to allow more splits
91
+ numEigenvectors = 2,
92
+ tolerance = 1e-6,
93
+ maxIterations = 100,
94
+ seed = 42,
95
+ } = config;
96
+
97
+ // Set random seed for reproducibility
98
+ Math.random = seedRandom(seed);
99
+
100
+ const nodes = Array.from(graph.nodes());
101
+ const nodeCount = nodes.length;
102
+
103
+ if (nodeCount === 0) {
104
+ throw new Error("Cannot cluster empty graph");
105
+ }
106
+
107
+ // Initialize root cluster with all nodes
108
+ const rootMembers = new Set(nodes.map((node) => node.id));
109
+ const initialModularity = calculateModularity(graph, new Map([... rootMembers].map((id) => [id, 0])));
110
+
111
+ const root: GRSBMCluster = {
112
+ id: "root",
113
+ members: rootMembers,
114
+ modularity: initialModularity,
115
+ depth: 0,
116
+ spectralScore: 0,
117
+ };
118
+
119
+ const explanations: ClusterExplanation[] = [];
120
+ const modularityScores: number[] = [initialModularity];
121
+
122
+ // Perform recursive bisection
123
+ const clusterQueue: GRSBMCluster[] = [root];
124
+ let nextClusterId = 1;
125
+
126
+ while (clusterQueue.length > 0) {
127
+ const currentCluster = clusterQueue.shift();
128
+ if (!currentCluster) {
129
+ continue;
130
+ }
131
+
132
+ // Check stopping criteria
133
+ if (currentCluster.depth >= maxDepth ||
134
+ currentCluster.members.size < minClusterSize * 2) {
135
+ continue;
136
+ }
137
+
138
+ // Attempt spectral bisection
139
+ const bisectionResult = spectralBisection(
140
+ graph,
141
+ currentCluster,
142
+ numEigenvectors,
143
+ tolerance,
144
+ maxIterations,
145
+ );
146
+
147
+ if (!bisectionResult) {
148
+ continue; // Could not split this cluster
149
+ }
150
+
151
+ const {leftMembers, rightMembers, spectralScore, keyNodes, spectralValues, reason} = bisectionResult;
152
+
153
+ // Create child clusters
154
+ const leftCluster: GRSBMCluster = {
155
+ id: `cluster_${String(nextClusterId++)}`,
156
+ members: leftMembers,
157
+ modularity: 0, // Will be calculated below
158
+ depth: currentCluster.depth + 1,
159
+ spectralScore,
160
+ };
161
+
162
+ const rightCluster: GRSBMCluster = {
163
+ id: `cluster_${String(nextClusterId++)}`,
164
+ members: rightMembers,
165
+ modularity: 0, // Will be calculated below
166
+ depth: currentCluster.depth + 1,
167
+ spectralScore,
168
+ };
169
+
170
+ // Calculate modularity improvement
171
+ const newAssignment = new Map<NodeId, number>();
172
+ for (const nodeId of leftMembers) {
173
+ newAssignment.set(nodeId, 0);
174
+ }
175
+ for (const nodeId of rightMembers) {
176
+ newAssignment.set(nodeId, 1);
177
+ }
178
+
179
+ const newModularity = calculateModularity(graph, newAssignment);
180
+ const modularityImprovement = newModularity - currentCluster.modularity;
181
+
182
+ // Only split if modularity improves or is neutral (allow small splits)
183
+ if (modularityImprovement >= -0.01) { // Small tolerance for neutral splits
184
+ leftCluster.modularity = newModularity;
185
+ rightCluster.modularity = newModularity;
186
+
187
+ currentCluster.left = leftCluster;
188
+ currentCluster.right = rightCluster;
189
+
190
+ // Add explanation
191
+ explanations.push({
192
+ clusterId: currentCluster.id,
193
+ reason,
194
+ modularityImprovement,
195
+ keyNodes,
196
+ spectralValues,
197
+ });
198
+
199
+ modularityScores.push(newModularity);
200
+
201
+ // Add children to queue for further processing
202
+ clusterQueue.push(leftCluster, rightCluster);
203
+ }
204
+ }
205
+
206
+ // Extract flat clustering from leaf nodes
207
+ const clusters = new Map<NodeId, number>();
208
+ let clusterId = 0;
209
+
210
+ function assignClusterIds(cluster: GRSBMCluster): void {
211
+ if (!cluster.left && !cluster.right) {
212
+ // Leaf node
213
+ for (const nodeId of cluster.members) {
214
+ clusters.set(nodeId, clusterId);
215
+ }
216
+ clusterId++;
217
+ } else {
218
+ // Internal node - traverse children
219
+ if (cluster.left) {
220
+ assignClusterIds(cluster.left);
221
+ }
222
+
223
+ if (cluster.right) {
224
+ assignClusterIds(cluster.right);
225
+ }
226
+ }
227
+ }
228
+
229
+ assignClusterIds(root);
230
+
231
+ return {
232
+ root,
233
+ clusters,
234
+ numClusters: clusterId,
235
+ modularityScores,
236
+ explanation: explanations,
237
+ };
238
+ }
239
+
240
+ /**
241
+ * Perform spectral bisection on a cluster
242
+ */
243
+ function spectralBisection(
244
+ graph: Graph,
245
+ cluster: GRSBMCluster,
246
+ numEigenvectors: number,
247
+ tolerance: number,
248
+ maxIterations: number,
249
+ ): {
250
+ leftMembers: Set<NodeId>;
251
+ rightMembers: Set<NodeId>;
252
+ spectralScore: number;
253
+ keyNodes: NodeId[];
254
+ spectralValues: number[];
255
+ reason: string;
256
+ } | null {
257
+ const members = Array.from(cluster.members);
258
+ const n = members.length;
259
+
260
+ if (n < 4) {
261
+ return null; // Too small to split meaningfully
262
+ }
263
+
264
+ // Create subgraph Laplacian matrix
265
+ const laplacian = createLaplacianMatrix(graph, members);
266
+
267
+ // Compute the second smallest eigenvector (Fiedler vector)
268
+ const eigenvector = computeFiedlerVector(laplacian, tolerance, maxIterations);
269
+
270
+ if (!eigenvector) {
271
+ return null; // Could not compute eigenvector
272
+ }
273
+
274
+ // Find optimal split point
275
+ const sortedIndices = eigenvector
276
+ .map((value, index) => ({value, index}))
277
+ .sort((a, b) => a.value - b.value);
278
+
279
+ let bestSplitIndex = Math.floor(n / 2);
280
+ let bestModularity = -Infinity;
281
+
282
+ // Try different split points
283
+ for (let splitIndex = Math.floor(n * 0.2); splitIndex <= Math.floor(n * 0.8); splitIndex++) {
284
+ const leftIndices = sortedIndices.slice(0, splitIndex).map((item) => item.index);
285
+ const rightIndices = sortedIndices.slice(splitIndex).map((item) => item.index);
286
+
287
+ const assignment = new Map<NodeId, number>();
288
+ for (const idx of leftIndices) {
289
+ const nodeId = members[idx];
290
+ if (nodeId !== undefined) {
291
+ assignment.set(nodeId, 0);
292
+ }
293
+ }
294
+ for (const idx of rightIndices) {
295
+ const nodeId = members[idx];
296
+ if (nodeId !== undefined) {
297
+ assignment.set(nodeId, 1);
298
+ }
299
+ }
300
+
301
+ const modularity = calculateModularity(graph, assignment);
302
+
303
+ if (modularity > bestModularity) {
304
+ bestModularity = modularity;
305
+ bestSplitIndex = splitIndex;
306
+ }
307
+ }
308
+
309
+ // Create final split
310
+ const leftIndices = sortedIndices.slice(0, bestSplitIndex).map((item) => item.index);
311
+ const rightIndices = sortedIndices.slice(bestSplitIndex).map((item) => item.index);
312
+
313
+ const leftMembers = new Set<NodeId>();
314
+ const rightMembers = new Set<NodeId>();
315
+
316
+ for (const idx of leftIndices) {
317
+ const nodeId = members[idx];
318
+ if (nodeId !== undefined) {
319
+ leftMembers.add(nodeId);
320
+ }
321
+ }
322
+
323
+ for (const idx of rightIndices) {
324
+ const nodeId = members[idx];
325
+ if (nodeId !== undefined) {
326
+ rightMembers.add(nodeId);
327
+ }
328
+ }
329
+
330
+ // Identify key nodes (those with extreme spectral values)
331
+ const extremeThreshold = 0.1;
332
+ const sortedValues = [... eigenvector].sort((a, b) => Math.abs(b) - Math.abs(a));
333
+ const threshold = sortedValues[Math.floor(sortedValues.length * extremeThreshold)] ?? 0;
334
+
335
+ const keyNodes: NodeId[] = [];
336
+ for (let i = 0; i < eigenvector.length; i++) {
337
+ const eigenValue = eigenvector[i];
338
+ if (eigenValue !== undefined && Math.abs(eigenValue) >= Math.abs(threshold)) {
339
+ const nodeId = members[i];
340
+ if (nodeId !== undefined) {
341
+ keyNodes.push(nodeId);
342
+ }
343
+ }
344
+ }
345
+
346
+ // Calculate spectral score (based on eigenvalue gap)
347
+ const firstValue = sortedValues[0] ?? 0;
348
+ const lastValue = sortedValues[sortedValues.length - 1] ?? 0;
349
+ const spectralScore = Math.abs(firstValue - lastValue);
350
+
351
+ // Generate explanation
352
+ const reason = `Spectral bisection based on Fiedler vector with modularity ${bestModularity.toFixed(3)}. Split creates clusters of sizes ${String(leftMembers.size)} and ${String(rightMembers.size)}.`;
353
+
354
+ return {
355
+ leftMembers,
356
+ rightMembers,
357
+ spectralScore,
358
+ keyNodes: keyNodes.slice(0, 5), // Top 5 key nodes
359
+ spectralValues: eigenvector,
360
+ reason,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Create Laplacian matrix for a subgraph
366
+ */
367
+ function createLaplacianMatrix(graph: Graph, nodes: NodeId[]): number[][] {
368
+ const n = nodes.length;
369
+ const nodeIndex = new Map<NodeId, number>();
370
+
371
+ // Create node index mapping
372
+ for (let i = 0; i < n; i++) {
373
+ const node = nodes[i];
374
+ if (node !== undefined) {
375
+ nodeIndex.set(node, i);
376
+ }
377
+ }
378
+
379
+ // Initialize Laplacian matrix
380
+ const laplacian = Array.from({length: n}, () => new Array<number>(n).fill(0));
381
+
382
+ // Fill adjacency part and calculate degrees
383
+ const degrees = new Array<number>(n).fill(0);
384
+
385
+ for (let i = 0; i < n; i++) {
386
+ const nodeId = nodes[i];
387
+ if (nodeId === undefined) {
388
+ continue;
389
+ }
390
+
391
+ for (const neighbor of graph.neighbors(nodeId)) {
392
+ const neighborIdx = nodeIndex.get(neighbor);
393
+ if (neighborIdx !== undefined && i < laplacian.length && i < degrees.length) {
394
+ const row = laplacian[i];
395
+ if (row) {
396
+ row[neighborIdx] = -1;
397
+ degrees[i] = (degrees[i] ?? 0) + 1;
398
+ }
399
+ }
400
+ }
401
+ }
402
+
403
+ // Set diagonal (degree) values
404
+ for (let i = 0; i < n; i++) {
405
+ const row = laplacian[i];
406
+ const degree = degrees[i];
407
+ if (row && degree !== undefined) {
408
+ row[i] = degree;
409
+ }
410
+ }
411
+
412
+ return laplacian;
413
+ }
414
+
415
+ /**
416
+ * Compute the Fiedler vector (second smallest eigenvector) using power iteration
417
+ */
418
+ function computeFiedlerVector(laplacian: number[][], tolerance: number, maxIterations: number): number[] | null {
419
+ const n = laplacian.length;
420
+ if (n < 2) {
421
+ return null;
422
+ }
423
+
424
+ // Initialize random vector orthogonal to the all-ones vector
425
+ let vector = new Array<number>(n).fill(0);
426
+ for (let i = 0; i < n; i++) {
427
+ vector[i] = Math.random() - 0.5;
428
+ }
429
+
430
+ // Make orthogonal to all-ones vector
431
+ const mean = vector.reduce((sum: number, val: number) => sum + val, 0) / n;
432
+ for (let i = 0; i < n; i++) {
433
+ const val = vector[i];
434
+ if (val !== undefined) {
435
+ vector[i] = val - mean;
436
+ }
437
+ }
438
+
439
+ // Normalize
440
+ const norm = Math.sqrt(vector.reduce((sum: number, val: number) => sum + (val * val), 0));
441
+ if (norm > 0) {
442
+ for (let i = 0; i < n; i++) {
443
+ const val = vector[i];
444
+ if (val !== undefined) {
445
+ vector[i] = val / norm;
446
+ }
447
+ }
448
+ }
449
+
450
+ // Power iteration with deflation for second smallest eigenvalue
451
+ for (let iter = 0; iter < maxIterations; iter++) {
452
+ const newVector = new Array<number>(n).fill(0);
453
+
454
+ // Matrix-vector multiplication: newVector = L * vector
455
+ for (let i = 0; i < n; i++) {
456
+ let sum = 0;
457
+ const row = laplacian[i];
458
+ if (row) {
459
+ for (let j = 0; j < n; j++) {
460
+ const matrixVal = row[j];
461
+ const vectorVal = vector[j];
462
+ if (matrixVal !== undefined && vectorVal !== undefined) {
463
+ sum += matrixVal * vectorVal;
464
+ }
465
+ }
466
+ }
467
+
468
+ newVector[i] = sum;
469
+ }
470
+
471
+ // For smallest eigenvalue, we actually want inverse iteration
472
+ // This is simplified - in practice would solve (L - shift*I)x = vector
473
+ // For now, use power iteration on -L (approximately)
474
+ for (let i = 0; i < n; i++) {
475
+ const val = newVector[i];
476
+ if (val !== undefined) {
477
+ newVector[i] = -val;
478
+ }
479
+ }
480
+
481
+ // Orthogonalize against all-ones vector
482
+ const newMean = newVector.reduce((sum, val) => sum + val, 0) / n;
483
+ for (let i = 0; i < n; i++) {
484
+ const val = newVector[i];
485
+ if (val !== undefined) {
486
+ newVector[i] = val - newMean;
487
+ }
488
+ }
489
+
490
+ // Normalize
491
+ const newNorm = Math.sqrt(newVector.reduce((sum, val) => sum + (val * val), 0));
492
+ if (newNorm < tolerance) {
493
+ break; // Converged to zero vector
494
+ }
495
+
496
+ for (let i = 0; i < n; i++) {
497
+ const val = newVector[i];
498
+ if (val !== undefined) {
499
+ newVector[i] = val / newNorm;
500
+ }
501
+ }
502
+
503
+ // Check convergence
504
+ let diff = 0;
505
+ for (let i = 0; i < n; i++) {
506
+ const newVal = newVector[i];
507
+ const oldVal = vector[i];
508
+ if (newVal !== undefined && oldVal !== undefined) {
509
+ diff += Math.abs(newVal - oldVal);
510
+ }
511
+ }
512
+
513
+ vector = newVector;
514
+
515
+ if (diff < tolerance) {
516
+ break;
517
+ }
518
+ }
519
+
520
+ return vector;
521
+ }
522
+
523
+ /**
524
+ * Calculate modularity of a graph partitioning
525
+ */
526
+ function calculateModularity(graph: Graph, assignment: Map<NodeId, number>): number {
527
+ const totalEdges = graph.uniqueEdgeCount;
528
+ if (totalEdges === 0) {
529
+ return 0;
530
+ }
531
+
532
+ let modularity = 0;
533
+ const communities = new Map<number, Set<NodeId>>();
534
+
535
+ // Group nodes by community
536
+ for (const [nodeId, communityId] of assignment) {
537
+ if (!communities.has(communityId)) {
538
+ communities.set(communityId, new Set());
539
+ }
540
+
541
+ const community = communities.get(communityId);
542
+ if (community) {
543
+ community.add(nodeId);
544
+ }
545
+ }
546
+
547
+ // Calculate modularity for each community
548
+ for (const [, members] of communities) {
549
+ let internalEdges = 0;
550
+ let totalDegree = 0;
551
+
552
+ for (const nodeId of members) {
553
+ const nodeDegree = graph.degree(nodeId);
554
+ totalDegree += nodeDegree;
555
+
556
+ // Count internal edges
557
+ for (const neighbor of graph.neighbors(nodeId)) {
558
+ if (members.has(neighbor)) {
559
+ internalEdges++;
560
+ }
561
+ }
562
+ }
563
+
564
+ // Each edge is counted twice in undirected graphs
565
+ internalEdges /= 2;
566
+
567
+ // Modularity contribution for this community
568
+ const expectedEdges = (totalDegree * totalDegree) / (4 * totalEdges);
569
+ modularity += (internalEdges - expectedEdges) / totalEdges;
570
+ }
571
+
572
+ return modularity;
573
+ }
574
+
575
+ /**
576
+ * Simple seeded random number generator for reproducibility
577
+ */
578
+ function seedRandom(seed: number): () => number {
579
+ const m = 0x80000000; // 2**31
580
+ const a = 1103515245;
581
+ const c = 12345;
582
+
583
+ seed = seed % m;
584
+
585
+ return function() {
586
+ seed = ((a * seed) + c) % m;
587
+ return seed / (m - 1);
588
+ };
589
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Priority 4 Research Algorithms (2023-2025)
3
+ *
4
+ * This module contains cutting-edge graph algorithms based on recent research
5
+ * from 2023-2025, focusing on deep learning integration, scalable algorithms,
6
+ * and explainable community detection.
7
+ */
8
+
9
+ // Synergistic Deep Graph Clustering
10
+ export {syncClustering, type SynCConfig, type SynCResult} from "./sync.js";
11
+
12
+ // TeraHAC - Hierarchical Agglomerative Clustering
13
+ export {teraHAC, type TeraHACConfig, type TeraHACResult} from "./terahac.js";
14
+ export {type ClusterNode as TeraHACClusterNode} from "./terahac.js";
15
+
16
+ // GRSBM - Greedy Recursive Spectral Bisection with Modularity
17
+ export {type ClusterExplanation, grsbm, type GRSBMCluster, type GRSBMConfig, type GRSBMResult} from "./grsbm.js";