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