@graphty/algorithms 1.0.1 → 1.1.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.
- package/README.md +827 -60
- package/dist/algorithms.d.ts +2 -0
- package/dist/algorithms.d.ts.map +1 -0
- package/dist/algorithms.js +2 -0
- package/dist/algorithms.js.map +1 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +22 -7
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +7 -7
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +1 -1
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +27 -0
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/eigenvector.js +113 -0
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -0
- package/dist/src/algorithms/centrality/hits.d.ts +35 -0
- package/dist/src/algorithms/centrality/hits.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/hits.js +146 -0
- package/dist/src/algorithms/centrality/hits.js.map +1 -0
- package/dist/src/algorithms/centrality/index.d.ts +6 -0
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/index.js +3 -0
- package/dist/src/algorithms/centrality/index.js.map +1 -1
- package/dist/src/algorithms/centrality/katz.d.ts +30 -0
- package/dist/src/algorithms/centrality/katz.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/katz.js +85 -0
- package/dist/src/algorithms/centrality/katz.js.map +1 -0
- package/dist/src/algorithms/centrality/pagerank.js +3 -3
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.d.ts +23 -0
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -0
- package/dist/src/algorithms/community/girvan-newman.js +297 -0
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -0
- package/dist/src/algorithms/community/index.d.ts +12 -0
- package/dist/src/algorithms/community/index.d.ts.map +1 -0
- package/dist/src/algorithms/community/index.js +12 -0
- package/dist/src/algorithms/community/index.js.map +1 -0
- package/dist/src/algorithms/community/label-propagation.d.ts +42 -0
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -0
- package/dist/src/algorithms/community/label-propagation.js +307 -0
- package/dist/src/algorithms/community/label-propagation.js.map +1 -0
- package/dist/src/algorithms/community/leiden.d.ts +33 -0
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -0
- package/dist/src/algorithms/community/leiden.js +434 -0
- package/dist/src/algorithms/community/leiden.js.map +1 -0
- package/dist/src/algorithms/community/louvain.d.ts +19 -0
- package/dist/src/algorithms/community/louvain.d.ts.map +1 -0
- package/dist/src/algorithms/community/louvain.js +219 -0
- package/dist/src/algorithms/community/louvain.js.map +1 -0
- package/dist/src/algorithms/components/connected.js +7 -7
- package/dist/src/algorithms/components/connected.js.map +1 -1
- package/dist/src/algorithms/index.d.ts +7 -0
- package/dist/src/algorithms/index.d.ts.map +1 -1
- package/dist/src/algorithms/index.js +14 -0
- package/dist/src/algorithms/index.js.map +1 -1
- package/dist/src/algorithms/matching/bipartite.d.ts +37 -0
- package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -0
- package/dist/src/algorithms/matching/bipartite.js +127 -0
- package/dist/src/algorithms/matching/bipartite.js.map +1 -0
- package/dist/src/algorithms/matching/index.d.ts +8 -0
- package/dist/src/algorithms/matching/index.d.ts.map +1 -0
- package/dist/src/algorithms/matching/index.js +6 -0
- package/dist/src/algorithms/matching/index.js.map +1 -0
- package/dist/src/algorithms/matching/isomorphism.d.ts +32 -0
- package/dist/src/algorithms/matching/isomorphism.d.ts.map +1 -0
- package/dist/src/algorithms/matching/isomorphism.js +291 -0
- package/dist/src/algorithms/matching/isomorphism.js.map +1 -0
- package/dist/src/algorithms/mst/index.d.ts +4 -0
- package/dist/src/algorithms/mst/index.d.ts.map +1 -0
- package/dist/src/algorithms/mst/index.js +3 -0
- package/dist/src/algorithms/mst/index.js.map +1 -0
- package/dist/src/algorithms/mst/kruskal.d.ts +9 -0
- package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -0
- package/dist/src/algorithms/mst/kruskal.js +43 -0
- package/dist/src/algorithms/mst/kruskal.js.map +1 -0
- package/dist/src/algorithms/mst/prim.d.ts +5 -0
- package/dist/src/algorithms/mst/prim.d.ts.map +1 -0
- package/dist/src/algorithms/mst/prim.js +63 -0
- package/dist/src/algorithms/mst/prim.js.map +1 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js +2 -2
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +5 -5
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +14 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.js +129 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -0
- package/dist/src/algorithms/shortest-path/index.d.ts +2 -0
- package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/index.js +1 -0
- package/dist/src/algorithms/shortest-path/index.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +2 -2
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/algorithms/traversal/dfs.js +11 -11
- package/dist/src/algorithms/traversal/dfs.js.map +1 -1
- package/dist/src/clustering/hierarchical.d.ts +48 -0
- package/dist/src/clustering/hierarchical.d.ts.map +1 -0
- package/dist/src/clustering/hierarchical.js +439 -0
- package/dist/src/clustering/hierarchical.js.map +1 -0
- package/dist/src/clustering/index.d.ts +7 -0
- package/dist/src/clustering/index.d.ts.map +1 -0
- package/dist/src/clustering/index.js +5 -0
- package/dist/src/clustering/index.js.map +1 -0
- package/dist/src/clustering/k-core.d.ts +63 -0
- package/dist/src/clustering/k-core.d.ts.map +1 -0
- package/dist/src/clustering/k-core.js +377 -0
- package/dist/src/clustering/k-core.js.map +1 -0
- package/dist/src/clustering/mcl.d.ts +36 -0
- package/dist/src/clustering/mcl.d.ts.map +1 -0
- package/dist/src/clustering/mcl.js +343 -0
- package/dist/src/clustering/mcl.js.map +1 -0
- package/dist/src/clustering/spectral.d.ts +28 -0
- package/dist/src/clustering/spectral.d.ts.map +1 -0
- package/dist/src/clustering/spectral.js +447 -0
- package/dist/src/clustering/spectral.js.map +1 -0
- package/dist/src/core/graph.d.ts +9 -0
- package/dist/src/core/graph.d.ts.map +1 -1
- package/dist/src/core/graph.js +31 -11
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/data-structures/priority-queue.js +0 -2
- package/dist/src/data-structures/priority-queue.js.map +1 -1
- package/dist/src/data-structures/union-find.js +2 -5
- package/dist/src/data-structures/union-find.js.map +1 -1
- package/dist/src/flow/ford-fulkerson.d.ts +53 -0
- package/dist/src/flow/ford-fulkerson.d.ts.map +1 -0
- package/dist/src/flow/ford-fulkerson.js +355 -0
- package/dist/src/flow/ford-fulkerson.js.map +1 -0
- package/dist/src/flow/index.d.ts +3 -0
- package/dist/src/flow/index.d.ts.map +1 -0
- package/dist/src/flow/index.js +3 -0
- package/dist/src/flow/index.js.map +1 -0
- package/dist/src/flow/min-cut.d.ts +49 -0
- package/dist/src/flow/min-cut.d.ts.map +1 -0
- package/dist/src/flow/min-cut.js +383 -0
- package/dist/src/flow/min-cut.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/link-prediction/adamic-adar.d.ts +56 -0
- package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -0
- package/dist/src/link-prediction/adamic-adar.js +256 -0
- package/dist/src/link-prediction/adamic-adar.js.map +1 -0
- package/dist/src/link-prediction/common-neighbors.d.ts +50 -0
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -0
- package/dist/src/link-prediction/common-neighbors.js +155 -0
- package/dist/src/link-prediction/common-neighbors.js.map +1 -0
- package/dist/src/link-prediction/index.d.ts +7 -0
- package/dist/src/link-prediction/index.d.ts.map +1 -0
- package/dist/src/link-prediction/index.js +6 -0
- package/dist/src/link-prediction/index.js.map +1 -0
- package/dist/src/pathfinding/astar.d.ts +50 -0
- package/dist/src/pathfinding/astar.d.ts.map +1 -0
- package/dist/src/pathfinding/astar.js +180 -0
- package/dist/src/pathfinding/astar.js.map +1 -0
- package/dist/src/pathfinding/index.d.ts +3 -0
- package/dist/src/pathfinding/index.d.ts.map +1 -0
- package/dist/src/pathfinding/index.js +3 -0
- package/dist/src/pathfinding/index.js.map +1 -0
- package/dist/src/pathfinding/utils.d.ts +18 -0
- package/dist/src/pathfinding/utils.d.ts.map +1 -0
- package/dist/src/pathfinding/utils.js +68 -0
- package/dist/src/pathfinding/utils.js.map +1 -0
- package/dist/src/types/index.d.ts +9 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/utils/graphNode.d.ts +8 -0
- package/dist/src/utils/graphNode.d.ts.map +1 -0
- package/dist/src/utils/graphNode.js +2 -0
- package/dist/src/utils/graphNode.js.map +1 -0
- package/dist/src/utils/priorityQueue.d.ts +21 -0
- package/dist/src/utils/priorityQueue.d.ts.map +1 -0
- package/dist/src/utils/priorityQueue.js +101 -0
- package/dist/src/utils/priorityQueue.js.map +1 -0
- package/package.json +3 -1
- package/src/algorithms/centrality/betweenness.ts +25 -7
- package/src/algorithms/centrality/closeness.ts +7 -7
- package/src/algorithms/centrality/degree.ts +1 -1
- package/src/algorithms/centrality/eigenvector.ts +162 -0
- package/src/algorithms/centrality/hits.ts +210 -0
- package/src/algorithms/centrality/index.ts +6 -0
- package/src/algorithms/centrality/katz.ts +139 -0
- package/src/algorithms/centrality/pagerank.ts +3 -3
- package/src/algorithms/community/girvan-newman.ts +372 -0
- package/src/algorithms/community/index.ts +13 -0
- package/src/algorithms/community/label-propagation.ts +392 -0
- package/src/algorithms/community/leiden.ts +572 -0
- package/src/algorithms/community/louvain.ts +298 -0
- package/src/algorithms/components/connected.ts +7 -7
- package/src/algorithms/index.ts +21 -0
- package/src/algorithms/matching/bipartite.ts +179 -0
- package/src/algorithms/matching/index.ts +8 -0
- package/src/algorithms/matching/isomorphism.ts +392 -0
- package/src/algorithms/mst/index.ts +4 -0
- package/src/algorithms/mst/kruskal.ts +61 -0
- package/src/algorithms/mst/prim.ts +80 -0
- package/src/algorithms/shortest-path/bellman-ford.ts +2 -2
- package/src/algorithms/shortest-path/dijkstra.ts +5 -5
- package/src/algorithms/shortest-path/floyd-warshall.ts +170 -0
- package/src/algorithms/shortest-path/index.ts +2 -0
- package/src/algorithms/traversal/bfs.ts +2 -2
- package/src/algorithms/traversal/dfs.ts +11 -11
- package/src/clustering/hierarchical.ts +555 -0
- package/src/clustering/index.ts +6 -0
- package/src/clustering/k-core.ts +460 -0
- package/src/clustering/mcl.ts +454 -0
- package/src/clustering/spectral.ts +545 -0
- package/src/core/graph.ts +34 -6
- package/src/data-structures/union-find.ts +2 -2
- package/src/flow/ford-fulkerson.ts +459 -0
- package/src/flow/index.ts +2 -0
- package/src/flow/min-cut.ts +482 -0
- package/src/index.ts +2 -0
- package/src/link-prediction/adamic-adar.ts +353 -0
- package/src/link-prediction/common-neighbors.ts +252 -0
- package/src/link-prediction/index.ts +20 -0
- package/src/pathfinding/astar.ts +229 -0
- package/src/pathfinding/index.ts +2 -0
- package/src/pathfinding/utils.ts +87 -0
- package/src/types/index.ts +11 -0
- package/src/utils/graphNode.ts +7 -0
- package/src/utils/priorityQueue.ts +120 -0
- package/dist/test/browser/basic.test.d.ts +0 -2
- package/dist/test/browser/basic.test.d.ts.map +0 -1
- package/dist/test/browser/basic.test.js +0 -70
- package/dist/test/browser/basic.test.js.map +0 -1
- package/dist/test/unit/bellman-ford.test.d.ts +0 -2
- package/dist/test/unit/bellman-ford.test.d.ts.map +0 -1
- package/dist/test/unit/bellman-ford.test.js +0 -231
- package/dist/test/unit/bellman-ford.test.js.map +0 -1
- package/dist/test/unit/betweenness-centrality.test.d.ts +0 -2
- package/dist/test/unit/betweenness-centrality.test.d.ts.map +0 -1
- package/dist/test/unit/betweenness-centrality.test.js +0 -311
- package/dist/test/unit/betweenness-centrality.test.js.map +0 -1
- package/dist/test/unit/bfs.test.d.ts +0 -2
- package/dist/test/unit/bfs.test.d.ts.map +0 -1
- package/dist/test/unit/bfs.test.js +0 -277
- package/dist/test/unit/bfs.test.js.map +0 -1
- package/dist/test/unit/closeness.test.d.ts +0 -2
- package/dist/test/unit/closeness.test.d.ts.map +0 -1
- package/dist/test/unit/closeness.test.js +0 -266
- package/dist/test/unit/closeness.test.js.map +0 -1
- package/dist/test/unit/connected-components.test.d.ts +0 -2
- package/dist/test/unit/connected-components.test.d.ts.map +0 -1
- package/dist/test/unit/connected-components.test.js +0 -355
- package/dist/test/unit/connected-components.test.js.map +0 -1
- package/dist/test/unit/degree-centrality.test.d.ts +0 -2
- package/dist/test/unit/degree-centrality.test.d.ts.map +0 -1
- package/dist/test/unit/degree-centrality.test.js +0 -260
- package/dist/test/unit/degree-centrality.test.js.map +0 -1
- package/dist/test/unit/dfs.test.d.ts +0 -2
- package/dist/test/unit/dfs.test.d.ts.map +0 -1
- package/dist/test/unit/dfs.test.js +0 -350
- package/dist/test/unit/dfs.test.js.map +0 -1
- package/dist/test/unit/dijkstra.test.d.ts +0 -2
- package/dist/test/unit/dijkstra.test.d.ts.map +0 -1
- package/dist/test/unit/dijkstra.test.js +0 -311
- package/dist/test/unit/dijkstra.test.js.map +0 -1
- package/dist/test/unit/graph.test.d.ts +0 -2
- package/dist/test/unit/graph.test.d.ts.map +0 -1
- package/dist/test/unit/graph.test.js +0 -175
- package/dist/test/unit/graph.test.js.map +0 -1
- package/dist/test/unit/pagerank.test.d.ts +0 -2
- package/dist/test/unit/pagerank.test.d.ts.map +0 -1
- package/dist/test/unit/pagerank.test.js +0 -281
- package/dist/test/unit/pagerank.test.js.map +0 -1
- package/dist/test/unit/priority-queue.test.d.ts +0 -2
- package/dist/test/unit/priority-queue.test.d.ts.map +0 -1
- package/dist/test/unit/priority-queue.test.js +0 -189
- package/dist/test/unit/priority-queue.test.js.map +0 -1
- package/dist/test/unit/union-find.test.d.ts +0 -2
- package/dist/test/unit/union-find.test.d.ts.map +0 -1
- package/dist/test/unit/union-find.test.js +0 -245
- package/dist/test/unit/union-find.test.js.map +0 -1
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hierarchical Clustering Algorithm
|
|
3
|
+
*
|
|
4
|
+
* Builds a hierarchy of clusters through agglomeration (bottom-up)
|
|
5
|
+
* or division (top-down). Useful for understanding multi-scale
|
|
6
|
+
* structure in graphs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ClusterNode<T> {
|
|
10
|
+
id: string;
|
|
11
|
+
members: Set<T>;
|
|
12
|
+
left?: ClusterNode<T> | undefined;
|
|
13
|
+
right?: ClusterNode<T> | undefined;
|
|
14
|
+
distance: number;
|
|
15
|
+
height: number;
|
|
16
|
+
// For forest roots, store the individual trees
|
|
17
|
+
trees?: ClusterNode<T>[] | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HierarchicalClusteringResult<T> {
|
|
21
|
+
root: ClusterNode<T>;
|
|
22
|
+
dendrogram: ClusterNode<T>[];
|
|
23
|
+
clusters: Map<number, Set<T>[]>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LinkageMethod = "single" | "complete" | "average" | "ward";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compute distance between two nodes based on graph structure
|
|
30
|
+
*/
|
|
31
|
+
function computeGraphDistance<T>(
|
|
32
|
+
graph: Map<T, Set<T>>,
|
|
33
|
+
distances: Map<T, Map<T, number>>,
|
|
34
|
+
): void {
|
|
35
|
+
// Use BFS to compute shortest path distances
|
|
36
|
+
for (const start of graph.keys()) {
|
|
37
|
+
const dist = new Map<T, number>();
|
|
38
|
+
const queue: T[] = [start];
|
|
39
|
+
dist.set(start, 0);
|
|
40
|
+
|
|
41
|
+
while (queue.length > 0) {
|
|
42
|
+
const node = queue.shift();
|
|
43
|
+
if (!node) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const neighbors = graph.get(node);
|
|
48
|
+
|
|
49
|
+
if (neighbors) {
|
|
50
|
+
for (const neighbor of neighbors) {
|
|
51
|
+
if (!dist.has(neighbor)) {
|
|
52
|
+
const nodeDistance = dist.get(node);
|
|
53
|
+
if (nodeDistance !== undefined) {
|
|
54
|
+
dist.set(neighbor, nodeDistance + 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
queue.push(neighbor);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
distances.set(start, dist);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Compute distance between clusters based on linkage method
|
|
69
|
+
*/
|
|
70
|
+
function clusterDistance<T>(
|
|
71
|
+
cluster1: Set<T>,
|
|
72
|
+
cluster2: Set<T>,
|
|
73
|
+
distances: Map<T, Map<T, number>>,
|
|
74
|
+
method: LinkageMethod,
|
|
75
|
+
): number {
|
|
76
|
+
if (cluster1.size === 0 || cluster2.size === 0) {
|
|
77
|
+
return Infinity;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const allDistances: number[] = [];
|
|
81
|
+
|
|
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);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (method) {
|
|
90
|
+
case "single":
|
|
91
|
+
return Math.min(... allDistances);
|
|
92
|
+
case "complete":
|
|
93
|
+
return Math.max(... allDistances);
|
|
94
|
+
case "average":
|
|
95
|
+
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;
|
|
100
|
+
default:
|
|
101
|
+
return Math.min(... allDistances);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
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²)
|
|
115
|
+
*/
|
|
116
|
+
export function hierarchicalClustering<T>(
|
|
117
|
+
graph: Map<T, Set<T>>,
|
|
118
|
+
linkage: LinkageMethod = "single",
|
|
119
|
+
): HierarchicalClusteringResult<T> {
|
|
120
|
+
const nodes = Array.from(graph.keys());
|
|
121
|
+
const n = nodes.length;
|
|
122
|
+
|
|
123
|
+
if (n === 0) {
|
|
124
|
+
return {
|
|
125
|
+
root: {id: "empty", members: new Set(), distance: 0, height: 0},
|
|
126
|
+
dendrogram: [],
|
|
127
|
+
clusters: new Map(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Compute pairwise distances
|
|
132
|
+
const distances = new Map<T, Map<T, number>>();
|
|
133
|
+
computeGraphDistance(graph, distances);
|
|
134
|
+
|
|
135
|
+
// Initialize clusters - each node is its own cluster
|
|
136
|
+
const clusters: ClusterNode<T>[] = nodes.map((node, i) => ({
|
|
137
|
+
id: `leaf-${String(i)}`,
|
|
138
|
+
members: new Set([node]),
|
|
139
|
+
distance: 0,
|
|
140
|
+
height: 0,
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
const dendrogram: ClusterNode<T>[] = [... clusters];
|
|
144
|
+
let clusterCount = n;
|
|
145
|
+
|
|
146
|
+
// Distance matrix between clusters
|
|
147
|
+
const clusterDistances = new Map<string, Map<string, number>>();
|
|
148
|
+
|
|
149
|
+
// Initialize cluster distances
|
|
150
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
151
|
+
const clusterI = clusters[i];
|
|
152
|
+
if (!clusterI) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
clusterDistances.set(clusterI.id, new Map());
|
|
157
|
+
for (let j = i + 1; j < clusters.length; j++) {
|
|
158
|
+
const clusterJ = clusters[j];
|
|
159
|
+
if (!clusterJ) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
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
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Merge clusters until only one remains
|
|
177
|
+
while (clusters.length > 1) {
|
|
178
|
+
// Find closest pair of clusters
|
|
179
|
+
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) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const dist = clusterDistances.get(clusterI.id)?.get(clusterJ.id) ??
|
|
192
|
+
clusterDistances.get(clusterJ.id)?.get(clusterI.id) ??
|
|
193
|
+
Infinity;
|
|
194
|
+
|
|
195
|
+
if (dist < minDist) {
|
|
196
|
+
minDist = dist;
|
|
197
|
+
merge1 = i;
|
|
198
|
+
merge2 = j;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if we found a valid merge
|
|
204
|
+
if (minDist === Infinity || merge1 === merge2) {
|
|
205
|
+
// No more clusters can be merged (disconnected components)
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Create new cluster
|
|
210
|
+
const cluster1 = clusters[merge1];
|
|
211
|
+
const cluster2 = clusters[merge2];
|
|
212
|
+
if (!cluster1 || !cluster2) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const newCluster: ClusterNode<T> = {
|
|
217
|
+
id: `cluster-${String(clusterCount++)}`,
|
|
218
|
+
members: new Set([... cluster1.members, ... cluster2.members]),
|
|
219
|
+
left: cluster1,
|
|
220
|
+
right: cluster2,
|
|
221
|
+
distance: minDist,
|
|
222
|
+
height: Math.max(cluster1.height, cluster2.height) + 1,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
dendrogram.push(newCluster);
|
|
226
|
+
|
|
227
|
+
// Update cluster distances for new cluster
|
|
228
|
+
clusterDistances.set(newCluster.id, new Map());
|
|
229
|
+
|
|
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
|
+
}
|
|
236
|
+
|
|
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
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
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);
|
|
256
|
+
}
|
|
257
|
+
|
|
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);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Generate clusters at different levels
|
|
265
|
+
const clustersByLevel = new Map<number, Set<T>[]>();
|
|
266
|
+
|
|
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
|
+
}
|
|
275
|
+
|
|
276
|
+
// Handle the case where we have multiple disconnected components
|
|
277
|
+
let root: ClusterNode<T>;
|
|
278
|
+
if (clusters.length === 0) {
|
|
279
|
+
return {
|
|
280
|
+
root: {id: "empty", members: new Set(), distance: 0, height: 0},
|
|
281
|
+
dendrogram: [],
|
|
282
|
+
clusters: new Map(),
|
|
283
|
+
};
|
|
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
|
+
|
|
309
|
+
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);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
root,
|
|
329
|
+
dendrogram,
|
|
330
|
+
clusters: clustersByLevel,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Cut dendrogram at specified height to get clusters
|
|
336
|
+
*/
|
|
337
|
+
export function cutDendrogram<T>(
|
|
338
|
+
root: ClusterNode<T>,
|
|
339
|
+
height: number,
|
|
340
|
+
): Set<T>[] {
|
|
341
|
+
const clusters: Set<T>[] = [];
|
|
342
|
+
|
|
343
|
+
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
|
|
351
|
+
for (const tree of node.trees) {
|
|
352
|
+
traverse(tree);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
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);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
traverse(root);
|
|
369
|
+
return clusters;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Cut dendrogram to get k clusters
|
|
374
|
+
*/
|
|
375
|
+
export function cutDendrogramKClusters<T>(
|
|
376
|
+
root: ClusterNode<T>,
|
|
377
|
+
k: number,
|
|
378
|
+
): Set<T>[] {
|
|
379
|
+
if (k <= 0) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (k === 1) {
|
|
384
|
+
return [root.members];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Find the height that gives us k clusters
|
|
388
|
+
let low = 0;
|
|
389
|
+
let high = root.height;
|
|
390
|
+
|
|
391
|
+
while (low < high) {
|
|
392
|
+
const mid = Math.floor((low + high) / 2);
|
|
393
|
+
const clusters = cutDendrogram(root, mid);
|
|
394
|
+
|
|
395
|
+
if (clusters.length === k) {
|
|
396
|
+
return clusters;
|
|
397
|
+
} else if (clusters.length < k) {
|
|
398
|
+
high = mid;
|
|
399
|
+
} else {
|
|
400
|
+
low = mid + 1;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return cutDendrogram(root, low);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Compute modularity-based hierarchical clustering
|
|
409
|
+
* Uses modularity gain to decide merges
|
|
410
|
+
*/
|
|
411
|
+
export function modularityHierarchicalClustering<T>(
|
|
412
|
+
graph: Map<T, Set<T>>,
|
|
413
|
+
): HierarchicalClusteringResult<T> {
|
|
414
|
+
const nodes = Array.from(graph.keys());
|
|
415
|
+
const n = nodes.length;
|
|
416
|
+
|
|
417
|
+
if (n === 0) {
|
|
418
|
+
return {
|
|
419
|
+
root: {id: "empty", members: new Set(), distance: 0, height: 0},
|
|
420
|
+
dendrogram: [],
|
|
421
|
+
clusters: new Map(),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Calculate total edges
|
|
426
|
+
let m = 0;
|
|
427
|
+
for (const neighbors of graph.values()) {
|
|
428
|
+
m += neighbors.size;
|
|
429
|
+
}
|
|
430
|
+
m = m / 2; // Each edge counted twice
|
|
431
|
+
|
|
432
|
+
// Initialize clusters
|
|
433
|
+
const clusters: ClusterNode<T>[] = nodes.map((node, i) => ({
|
|
434
|
+
id: `leaf-${String(i)}`,
|
|
435
|
+
members: new Set([node]),
|
|
436
|
+
distance: 0,
|
|
437
|
+
height: 0,
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
const dendrogram: ClusterNode<T>[] = [... clusters];
|
|
441
|
+
let clusterCount = n;
|
|
442
|
+
|
|
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;
|
|
452
|
+
|
|
453
|
+
for (const node1 of comm1) {
|
|
454
|
+
const neighbors = graph.get(node1);
|
|
455
|
+
if (!neighbors) {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
degree1 += neighbors.size;
|
|
460
|
+
|
|
461
|
+
for (const neighbor of neighbors) {
|
|
462
|
+
if (comm2.has(neighbor)) {
|
|
463
|
+
edgesBetween++;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (const node2 of comm2) {
|
|
469
|
+
const node2Neighbors = graph.get(node2);
|
|
470
|
+
if (node2Neighbors) {
|
|
471
|
+
degree2 += node2Neighbors.size;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Modularity gain formula
|
|
476
|
+
const gain = (edgesBetween / m) - ((degree1 * degree2) / (4 * m * m));
|
|
477
|
+
return gain;
|
|
478
|
+
}
|
|
479
|
+
|
|
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;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const gain = modularityGain(clusterI.members, clusterJ.members);
|
|
495
|
+
|
|
496
|
+
if (gain > maxGain) {
|
|
497
|
+
maxGain = gain;
|
|
498
|
+
merge1 = i;
|
|
499
|
+
merge2 = j;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Create new cluster
|
|
505
|
+
const cluster1 = clusters[merge1];
|
|
506
|
+
const cluster2 = clusters[merge2];
|
|
507
|
+
if (!cluster1 || !cluster2) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const newCluster: ClusterNode<T> = {
|
|
512
|
+
id: `cluster-${String(clusterCount++)}`,
|
|
513
|
+
members: new Set([... cluster1.members, ... cluster2.members]),
|
|
514
|
+
left: cluster1,
|
|
515
|
+
right: cluster2,
|
|
516
|
+
distance: -maxGain, // Use negative gain as distance
|
|
517
|
+
height: Math.max(cluster1.height, cluster2.height) + 1,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
dendrogram.push(newCluster);
|
|
521
|
+
|
|
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);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Generate clusters at different levels
|
|
534
|
+
const clustersByLevel = new Map<number, Set<T>[]>();
|
|
535
|
+
const root = clusters[0];
|
|
536
|
+
|
|
537
|
+
if (!root) {
|
|
538
|
+
return {
|
|
539
|
+
root: {id: "empty", members: new Set(), distance: 0, height: 0},
|
|
540
|
+
dendrogram: [],
|
|
541
|
+
clusters: new Map(),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
for (let h = 0; h <= root.height; h++) {
|
|
546
|
+
clustersByLevel.set(h, cutDendrogram(root, h));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
root,
|
|
551
|
+
dendrogram,
|
|
552
|
+
clusters: clustersByLevel,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "./hierarchical.js";
|
|
2
|
+
export * from "./k-core.js";
|
|
3
|
+
export type {MCLOptions, MCLResult} from "./mcl.js";
|
|
4
|
+
export {calculateMCLModularity, markovClustering} from "./mcl.js";
|
|
5
|
+
export type {SpectralClusteringOptions, SpectralClusteringResult} from "./spectral.js";
|
|
6
|
+
export {spectralClustering} from "./spectral.js";
|