@graphty/algorithms 1.1.0 → 1.3.1

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