@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.
- package/README.md +820 -235
- package/dist/algorithms.d.ts +42 -2
- package/dist/algorithms.js +11099 -2
- package/dist/algorithms.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +4 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +124 -170
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +26 -1
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +56 -149
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +3 -3
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +39 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js +286 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts +80 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js +294 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
- package/dist/src/algorithms/centrality/index.d.ts +2 -0
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/index.js +1 -0
- package/dist/src/algorithms/centrality/index.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +17 -0
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +45 -0
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +47 -54
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/index.d.ts +2 -0
- package/dist/src/algorithms/community/index.d.ts.map +1 -1
- package/dist/src/algorithms/community/index.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +13 -3
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +50 -35
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/community/leiden.d.ts +3 -2
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
- package/dist/src/algorithms/community/leiden.js +34 -39
- package/dist/src/algorithms/community/leiden.js.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.d.ts +112 -0
- package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
- package/dist/src/algorithms/community/louvain-optimized.js +303 -0
- package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
- package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
- package/dist/src/algorithms/community/louvain.js +17 -76
- package/dist/src/algorithms/community/louvain.js.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.d.ts +59 -0
- package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
- package/dist/src/algorithms/community/modularity-utils.js +112 -0
- package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
- package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
- package/dist/src/algorithms/matching/bipartite.js +8 -40
- package/dist/src/algorithms/matching/bipartite.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +1 -12
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +37 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +193 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts +5 -2
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +15 -14
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +29 -0
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-unified.js +333 -0
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts +54 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.js +368 -0
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +5 -182
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/benchmark-all-algorithms.d.ts +23 -0
- package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
- package/dist/src/benchmark-all-algorithms.js +462 -0
- package/dist/src/benchmark-all-algorithms.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +16 -15
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +209 -223
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/index.d.ts +4 -2
- package/dist/src/clustering/index.d.ts.map +1 -1
- package/dist/src/clustering/index.js +2 -2
- package/dist/src/clustering/index.js.map +1 -1
- package/dist/src/clustering/k-core.d.ts +16 -15
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +57 -21
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/spectral.d.ts +8 -0
- package/dist/src/clustering/spectral.d.ts.map +1 -1
- package/dist/src/clustering/spectral.js +158 -35
- package/dist/src/clustering/spectral.js.map +1 -1
- package/dist/src/core/graph.js +4 -4
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/flow/ford-fulkerson.d.ts +17 -11
- package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
- package/dist/src/flow/ford-fulkerson.js +141 -217
- package/dist/src/flow/ford-fulkerson.js.map +1 -1
- package/dist/src/flow/min-cut.d.ts +6 -5
- package/dist/src/flow/min-cut.d.ts.map +1 -1
- package/dist/src/flow/min-cut.js +17 -8
- package/dist/src/flow/min-cut.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
- package/dist/src/link-prediction/adamic-adar.js +17 -26
- package/dist/src/link-prediction/adamic-adar.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +7 -11
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/optimized/bit-packed.d.ts +143 -0
- package/dist/src/optimized/bit-packed.d.ts.map +1 -0
- package/dist/src/optimized/bit-packed.js +292 -0
- package/dist/src/optimized/bit-packed.js.map +1 -0
- package/dist/src/optimized/csr-graph.d.ts +86 -0
- package/dist/src/optimized/csr-graph.d.ts.map +1 -0
- package/dist/src/optimized/csr-graph.js +341 -0
- package/dist/src/optimized/csr-graph.js.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts +74 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.js +223 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
- package/dist/src/optimized/graph-adapter.d.ts +62 -0
- package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
- package/dist/src/optimized/graph-adapter.js +146 -0
- package/dist/src/optimized/graph-adapter.js.map +1 -0
- package/dist/src/optimized/index.d.ts +14 -0
- package/dist/src/optimized/index.d.ts.map +1 -0
- package/dist/src/optimized/index.js +28 -0
- package/dist/src/optimized/index.js.map +1 -0
- package/dist/src/research/grsbm.d.ts +83 -0
- package/dist/src/research/grsbm.d.ts.map +1 -0
- package/dist/src/research/grsbm.js +403 -0
- package/dist/src/research/grsbm.js.map +1 -0
- package/dist/src/research/index.d.ts +12 -0
- package/dist/src/research/index.d.ts.map +1 -0
- package/dist/src/research/index.js +14 -0
- package/dist/src/research/index.js.map +1 -0
- package/dist/src/research/sync.d.ts +49 -0
- package/dist/src/research/sync.d.ts.map +1 -0
- package/dist/src/research/sync.js +316 -0
- package/dist/src/research/sync.js.map +1 -0
- package/dist/src/research/terahac.d.ts +65 -0
- package/dist/src/research/terahac.d.ts.map +1 -0
- package/dist/src/research/terahac.js +379 -0
- package/dist/src/research/terahac.js.map +1 -0
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/utils/algorithm-utilities.d.ts +2 -0
- package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
- package/dist/src/utils/algorithm-utilities.js +2 -0
- package/dist/src/utils/algorithm-utilities.js.map +1 -0
- package/dist/src/utils/graph-converters.d.ts +79 -0
- package/dist/src/utils/graph-converters.d.ts.map +1 -0
- package/dist/src/utils/graph-converters.js +196 -0
- package/dist/src/utils/graph-converters.js.map +1 -0
- package/dist/src/utils/graph-utilities.d.ts +62 -0
- package/dist/src/utils/graph-utilities.d.ts.map +1 -0
- package/dist/src/utils/graph-utilities.js +148 -0
- package/dist/src/utils/graph-utilities.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/math-utilities.d.ts +52 -0
- package/dist/src/utils/math-utilities.d.ts.map +1 -0
- package/dist/src/utils/math-utilities.js +123 -0
- package/dist/src/utils/math-utilities.js.map +1 -0
- package/dist/src/utils/matrix-utilities.d.ts +2 -0
- package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
- package/dist/src/utils/matrix-utilities.js +2 -0
- package/dist/src/utils/matrix-utilities.js.map +1 -0
- package/dist/src/utils/optimization-helpers.d.ts +33 -0
- package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
- package/dist/src/utils/optimization-helpers.js +47 -0
- package/dist/src/utils/optimization-helpers.js.map +1 -0
- package/package.json +28 -4
- package/src/algorithms/centrality/betweenness.ts +167 -199
- package/src/algorithms/centrality/closeness.ts +73 -191
- package/src/algorithms/centrality/degree.ts +3 -3
- package/src/algorithms/centrality/delta-pagerank-simple.ts +366 -0
- package/src/algorithms/centrality/delta-pagerank.ts +435 -0
- package/src/algorithms/centrality/index.ts +2 -0
- package/src/algorithms/centrality/pagerank.ts +66 -0
- package/src/algorithms/community/girvan-newman.ts +52 -59
- package/src/algorithms/community/index.ts +2 -0
- package/src/algorithms/community/label-propagation.ts +64 -35
- package/src/algorithms/community/leiden.ts +40 -41
- package/src/algorithms/community/louvain-optimized.ts +445 -0
- package/src/algorithms/community/louvain.ts +24 -104
- package/src/algorithms/community/modularity-utils.ts +146 -0
- package/src/algorithms/matching/bipartite.ts +8 -44
- package/src/algorithms/shortest-path/bellman-ford.ts +1 -14
- package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
- package/src/algorithms/shortest-path/dijkstra.ts +17 -15
- package/src/algorithms/traversal/bfs-unified.ts +433 -0
- package/src/algorithms/traversal/bfs-variants.ts +496 -0
- package/src/algorithms/traversal/bfs.ts +10 -228
- package/src/benchmark-all-algorithms.ts +548 -0
- package/src/clustering/hierarchical.ts +241 -251
- package/src/clustering/index.ts +4 -2
- package/src/clustering/k-core.ts +87 -39
- package/src/clustering/spectral.ts +185 -34
- package/src/core/graph.ts +4 -4
- package/src/flow/ford-fulkerson.ts +186 -254
- package/src/flow/min-cut.ts +21 -11
- package/src/index.ts +9 -0
- package/src/link-prediction/adamic-adar.ts +17 -30
- package/src/link-prediction/common-neighbors.ts +7 -16
- package/src/optimized/bit-packed.ts +340 -0
- package/src/optimized/csr-graph.ts +455 -0
- package/src/optimized/direction-optimized-bfs.ts +291 -0
- package/src/optimized/graph-adapter.ts +200 -0
- package/src/optimized/index.ts +36 -0
- package/src/research/grsbm.ts +580 -0
- package/src/research/index.ts +17 -0
- package/src/research/sync.ts +440 -0
- package/src/research/terahac.ts +514 -0
- package/src/types/index.ts +8 -0
- package/src/utils/algorithm-utilities.ts +2 -0
- package/src/utils/graph-converters.ts +241 -0
- package/src/utils/graph-utilities.ts +199 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/math-utilities.ts +140 -0
- package/src/utils/matrix-utilities.ts +2 -0
- 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
|
-
|
|
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:
|
|
72
|
-
cluster2:
|
|
102
|
+
cluster1: ClusterNode<T>,
|
|
103
|
+
cluster2: ClusterNode<T>,
|
|
73
104
|
distances: Map<T, Map<T, number>>,
|
|
74
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
152
|
-
if (!
|
|
184
|
+
const cluster1 = clusters[i];
|
|
185
|
+
if (!cluster1) {
|
|
153
186
|
continue;
|
|
154
187
|
}
|
|
155
188
|
|
|
156
|
-
|
|
189
|
+
const distMap = new Map<string, number>();
|
|
157
190
|
for (let j = i + 1; j < clusters.length; j++) {
|
|
158
|
-
const
|
|
159
|
-
if (!
|
|
191
|
+
const cluster2 = clusters[j];
|
|
192
|
+
if (!cluster2) {
|
|
160
193
|
continue;
|
|
161
194
|
}
|
|
162
195
|
|
|
163
|
-
const dist = clusterDistance(
|
|
164
|
-
|
|
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 (
|
|
178
|
-
|
|
206
|
+
while (activeClusters.size > 1) {
|
|
207
|
+
// Find closest pair of clusters
|
|
179
208
|
let minDist = Infinity;
|
|
180
|
-
let
|
|
181
|
-
let
|
|
182
|
-
|
|
183
|
-
for (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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 =
|
|
192
|
-
clusterDistances.get(
|
|
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
|
-
|
|
198
|
-
|
|
229
|
+
mergeCluster1 = cluster1;
|
|
230
|
+
mergeCluster2 = cluster2;
|
|
199
231
|
}
|
|
200
232
|
}
|
|
201
233
|
}
|
|
202
234
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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([...
|
|
219
|
-
left:
|
|
220
|
-
right:
|
|
254
|
+
members: new Set([... mergeCluster1.members, ... mergeCluster2.members]),
|
|
255
|
+
left: mergeCluster1,
|
|
256
|
+
right: mergeCluster2,
|
|
221
257
|
distance: minDist,
|
|
222
|
-
height: Math.max(
|
|
258
|
+
height: Math.max(mergeCluster1.height, mergeCluster2.height) + 1,
|
|
223
259
|
};
|
|
224
260
|
|
|
225
261
|
dendrogram.push(newCluster);
|
|
226
262
|
|
|
227
|
-
// Update
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
265
|
-
const
|
|
296
|
+
// Root is the last remaining cluster
|
|
297
|
+
const root = activeClusters.size > 0 ? Array.from(activeClusters)[0] : dendrogram[dendrogram.length - 1];
|
|
266
298
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
357
|
-
|
|
339
|
+
} else {
|
|
340
|
+
if (node.left) {
|
|
341
|
+
traverse(node.left);
|
|
342
|
+
}
|
|
358
343
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
//
|
|
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
|
|
412
|
-
graph:
|
|
413
|
-
): HierarchicalClusteringResult<
|
|
414
|
-
const
|
|
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
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
417
|
+
|
|
418
|
+
// For undirected graphs
|
|
419
|
+
totalEdges = totalEdges / 2;
|
|
431
420
|
|
|
432
421
|
// Initialize clusters
|
|
433
|
-
const clusters: ClusterNode<
|
|
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<
|
|
429
|
+
const dendrogram: ClusterNode<string>[] = [... clusters];
|
|
441
430
|
let clusterCount = n;
|
|
442
431
|
|
|
443
|
-
//
|
|
444
|
-
const
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
degree2 += node2Neighbors.size;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
451
|
+
// Calculate modularity gain
|
|
452
|
+
let edgesBetween = 0;
|
|
453
|
+
let degreeProduct = 0;
|
|
474
454
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
455
|
+
for (const node1 of cluster1.members) {
|
|
456
|
+
const neighbors = adjacencySet.get(node1);
|
|
457
|
+
const degree1 = degrees.get(node1) ?? 0;
|
|
479
458
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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 =
|
|
469
|
+
const gain = (edgesBetween / totalEdges) -
|
|
470
|
+
(degreeProduct / (4 * totalEdges * totalEdges));
|
|
495
471
|
|
|
496
472
|
if (gain > maxGain) {
|
|
497
473
|
maxGain = gain;
|
|
498
|
-
|
|
499
|
-
|
|
474
|
+
mergeCluster1 = cluster1;
|
|
475
|
+
mergeCluster2 = cluster2;
|
|
500
476
|
}
|
|
501
477
|
}
|
|
502
478
|
}
|
|
503
479
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const cluster2 = clusters[merge2];
|
|
507
|
-
if (!cluster1 || !cluster2) {
|
|
508
|
-
continue;
|
|
480
|
+
if (!mergeCluster1 || !mergeCluster2) {
|
|
481
|
+
break;
|
|
509
482
|
}
|
|
510
483
|
|
|
511
|
-
|
|
484
|
+
// Create new cluster
|
|
485
|
+
const newCluster: ClusterNode<string> = {
|
|
512
486
|
id: `cluster-${String(clusterCount++)}`,
|
|
513
|
-
members: new Set([...
|
|
514
|
-
left:
|
|
515
|
-
right:
|
|
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(
|
|
491
|
+
height: Math.max(mergeCluster1.height, mergeCluster2.height) + 1,
|
|
518
492
|
};
|
|
519
493
|
|
|
520
494
|
dendrogram.push(newCluster);
|
|
521
495
|
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
//
|
|
534
|
-
const
|
|
535
|
-
|
|
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
|
+
}
|