@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
|
@@ -5,8 +5,39 @@
|
|
|
5
5
|
* or division (top-down). Useful for understanding multi-scale
|
|
6
6
|
* structure in graphs.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Convert Graph to adjacency set representation for clustering
|
|
10
|
+
* @param graph - The input graph to convert
|
|
11
|
+
* @returns Map of node IDs to sets of neighbor node IDs
|
|
12
|
+
*/
|
|
13
|
+
function graphToAdjacencySet(graph) {
|
|
14
|
+
const adjacency = new Map();
|
|
15
|
+
// Initialize all nodes
|
|
16
|
+
for (const node of graph.nodes()) {
|
|
17
|
+
adjacency.set(String(node.id), new Set());
|
|
18
|
+
}
|
|
19
|
+
// Add edges
|
|
20
|
+
for (const edge of graph.edges()) {
|
|
21
|
+
const source = String(edge.source);
|
|
22
|
+
const target = String(edge.target);
|
|
23
|
+
const sourceSet = adjacency.get(source);
|
|
24
|
+
if (sourceSet) {
|
|
25
|
+
sourceSet.add(target);
|
|
26
|
+
}
|
|
27
|
+
// For undirected graphs, add reverse edge
|
|
28
|
+
if (!graph.isDirected) {
|
|
29
|
+
const targetSet = adjacency.get(target);
|
|
30
|
+
if (targetSet) {
|
|
31
|
+
targetSet.add(source);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return adjacency;
|
|
36
|
+
}
|
|
8
37
|
/**
|
|
9
38
|
* Compute distance between two nodes based on graph structure
|
|
39
|
+
* @param graph - Adjacency map representation of the graph
|
|
40
|
+
* @param distances - Map to store computed distances between nodes
|
|
10
41
|
*/
|
|
11
42
|
function computeGraphDistance(graph, distances) {
|
|
12
43
|
// Use BFS to compute shortest path distances
|
|
@@ -23,10 +54,7 @@ function computeGraphDistance(graph, distances) {
|
|
|
23
54
|
if (neighbors) {
|
|
24
55
|
for (const neighbor of neighbors) {
|
|
25
56
|
if (!dist.has(neighbor)) {
|
|
26
|
-
|
|
27
|
-
if (nodeDistance !== undefined) {
|
|
28
|
-
dist.set(neighbor, nodeDistance + 1);
|
|
29
|
-
}
|
|
57
|
+
dist.set(neighbor, (dist.get(node) ?? 0) + 1);
|
|
30
58
|
queue.push(neighbor);
|
|
31
59
|
}
|
|
32
60
|
}
|
|
@@ -36,46 +64,55 @@ function computeGraphDistance(graph, distances) {
|
|
|
36
64
|
}
|
|
37
65
|
}
|
|
38
66
|
/**
|
|
39
|
-
* Compute distance between clusters based on linkage method
|
|
67
|
+
* Compute distance between two clusters based on linkage method
|
|
68
|
+
* @param cluster1 - First cluster node
|
|
69
|
+
* @param cluster2 - Second cluster node
|
|
70
|
+
* @param distances - Precomputed distance matrix between nodes
|
|
71
|
+
* @param linkage - Linkage method to use for computing cluster distance
|
|
72
|
+
* @returns The distance between the two clusters
|
|
40
73
|
*/
|
|
41
|
-
function clusterDistance(cluster1, cluster2, distances,
|
|
42
|
-
if (cluster1.size === 0 || cluster2.size === 0) {
|
|
43
|
-
return Infinity;
|
|
44
|
-
}
|
|
74
|
+
function clusterDistance(cluster1, cluster2, distances, linkage) {
|
|
45
75
|
const allDistances = [];
|
|
46
|
-
for (const node1 of cluster1) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
for (const node1 of cluster1.members) {
|
|
77
|
+
const node1Distances = distances.get(node1);
|
|
78
|
+
if (!node1Distances) {
|
|
79
|
+
continue;
|
|
50
80
|
}
|
|
81
|
+
for (const node2 of cluster2.members) {
|
|
82
|
+
const dist = node1Distances.get(node2);
|
|
83
|
+
if (dist !== undefined) {
|
|
84
|
+
allDistances.push(dist);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (allDistances.length === 0) {
|
|
89
|
+
return Infinity;
|
|
51
90
|
}
|
|
52
|
-
switch (
|
|
91
|
+
switch (linkage) {
|
|
53
92
|
case "single":
|
|
54
93
|
return Math.min(...allDistances);
|
|
55
94
|
case "complete":
|
|
56
95
|
return Math.max(...allDistances);
|
|
57
96
|
case "average":
|
|
58
97
|
return allDistances.reduce((a, b) => a + b, 0) / allDistances.length;
|
|
59
|
-
case "ward":
|
|
60
|
-
// Ward's method
|
|
61
|
-
|
|
62
|
-
|
|
98
|
+
case "ward": {
|
|
99
|
+
// Simplified Ward's method - minimize within-cluster variance
|
|
100
|
+
const size1 = cluster1.members.size;
|
|
101
|
+
const size2 = cluster2.members.size;
|
|
102
|
+
const avgDist = allDistances.reduce((a, b) => a + b, 0) / allDistances.length;
|
|
103
|
+
return avgDist * ((size1 * size2) / (size1 + size2));
|
|
104
|
+
}
|
|
63
105
|
default:
|
|
64
106
|
return Math.min(...allDistances);
|
|
65
107
|
}
|
|
66
108
|
}
|
|
67
109
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* @
|
|
72
|
-
* @param linkage - Linkage method for cluster distance
|
|
73
|
-
* @returns Hierarchical clustering result
|
|
74
|
-
*
|
|
75
|
-
* Time Complexity: O(n³) naive, O(n² log n) with heap
|
|
76
|
-
* Space Complexity: O(n²)
|
|
110
|
+
* Internal implementation of agglomerative hierarchical clustering
|
|
111
|
+
* @param graph - Adjacency map representation of the graph
|
|
112
|
+
* @param linkage - Linkage method for cluster distance computation
|
|
113
|
+
* @returns Hierarchical clustering result with dendrogram and cluster assignments
|
|
77
114
|
*/
|
|
78
|
-
|
|
115
|
+
function hierarchicalClusteringImpl(graph, linkage = "single") {
|
|
79
116
|
const nodes = Array.from(graph.keys());
|
|
80
117
|
const n = nodes.length;
|
|
81
118
|
if (n === 0) {
|
|
@@ -101,152 +138,111 @@ export function hierarchicalClustering(graph, linkage = "single") {
|
|
|
101
138
|
const clusterDistances = new Map();
|
|
102
139
|
// Initialize cluster distances
|
|
103
140
|
for (let i = 0; i < clusters.length; i++) {
|
|
104
|
-
const
|
|
105
|
-
if (!
|
|
141
|
+
const cluster1 = clusters[i];
|
|
142
|
+
if (!cluster1) {
|
|
106
143
|
continue;
|
|
107
144
|
}
|
|
108
|
-
|
|
145
|
+
const distMap = new Map();
|
|
109
146
|
for (let j = i + 1; j < clusters.length; j++) {
|
|
110
|
-
const
|
|
111
|
-
if (!
|
|
147
|
+
const cluster2 = clusters[j];
|
|
148
|
+
if (!cluster2) {
|
|
112
149
|
continue;
|
|
113
150
|
}
|
|
114
|
-
const dist = clusterDistance(
|
|
115
|
-
|
|
116
|
-
if (clusterIDistances) {
|
|
117
|
-
clusterIDistances.set(clusterJ.id, dist);
|
|
118
|
-
}
|
|
151
|
+
const dist = clusterDistance(cluster1, cluster2, distances, linkage);
|
|
152
|
+
distMap.set(cluster2.id, dist);
|
|
119
153
|
}
|
|
154
|
+
clusterDistances.set(cluster1.id, distMap);
|
|
120
155
|
}
|
|
156
|
+
// Active clusters
|
|
157
|
+
const activeClusters = new Set(clusters);
|
|
121
158
|
// Merge clusters until only one remains
|
|
122
|
-
while (
|
|
159
|
+
while (activeClusters.size > 1) {
|
|
123
160
|
// Find closest pair of clusters
|
|
124
161
|
let minDist = Infinity;
|
|
125
|
-
let
|
|
126
|
-
let
|
|
127
|
-
for (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
162
|
+
let mergeCluster1 = null;
|
|
163
|
+
let mergeCluster2 = null;
|
|
164
|
+
for (const cluster1 of activeClusters) {
|
|
165
|
+
const distMap = clusterDistances.get(cluster1.id);
|
|
166
|
+
if (!distMap) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const cluster2 of activeClusters) {
|
|
170
|
+
if (cluster1.id >= cluster2.id) {
|
|
132
171
|
continue;
|
|
133
172
|
}
|
|
134
|
-
const dist = clusterDistances.get(
|
|
135
|
-
clusterDistances.get(clusterJ.id)?.get(clusterI.id) ??
|
|
136
|
-
Infinity;
|
|
173
|
+
const dist = distMap.get(cluster2.id) ?? clusterDistances.get(cluster2.id)?.get(cluster1.id) ?? Infinity;
|
|
137
174
|
if (dist < minDist) {
|
|
138
175
|
minDist = dist;
|
|
139
|
-
|
|
140
|
-
|
|
176
|
+
mergeCluster1 = cluster1;
|
|
177
|
+
mergeCluster2 = cluster2;
|
|
141
178
|
}
|
|
142
179
|
}
|
|
143
180
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
181
|
+
if (!mergeCluster1 || !mergeCluster2 || minDist === Infinity) {
|
|
182
|
+
// No valid merge found - create forest
|
|
183
|
+
const trees = Array.from(activeClusters);
|
|
184
|
+
const forestRoot = {
|
|
185
|
+
id: `forest-${String(clusterCount++)}`,
|
|
186
|
+
members: new Set(nodes),
|
|
187
|
+
distance: Infinity,
|
|
188
|
+
height: Math.max(...trees.map((t) => t.height)) + 1,
|
|
189
|
+
trees,
|
|
190
|
+
};
|
|
191
|
+
dendrogram.push(forestRoot);
|
|
192
|
+
activeClusters.clear();
|
|
193
|
+
activeClusters.add(forestRoot);
|
|
147
194
|
break;
|
|
148
195
|
}
|
|
149
196
|
// Create new cluster
|
|
150
|
-
const cluster1 = clusters[merge1];
|
|
151
|
-
const cluster2 = clusters[merge2];
|
|
152
|
-
if (!cluster1 || !cluster2) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
197
|
const newCluster = {
|
|
156
198
|
id: `cluster-${String(clusterCount++)}`,
|
|
157
|
-
members: new Set([...
|
|
158
|
-
left:
|
|
159
|
-
right:
|
|
199
|
+
members: new Set([...mergeCluster1.members, ...mergeCluster2.members]),
|
|
200
|
+
left: mergeCluster1,
|
|
201
|
+
right: mergeCluster2,
|
|
160
202
|
distance: minDist,
|
|
161
|
-
height: Math.max(
|
|
203
|
+
height: Math.max(mergeCluster1.height, mergeCluster2.height) + 1,
|
|
162
204
|
};
|
|
163
205
|
dendrogram.push(newCluster);
|
|
164
|
-
// Update
|
|
165
|
-
|
|
166
|
-
for (
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
206
|
+
// Update distances to new cluster
|
|
207
|
+
const newDistMap = new Map();
|
|
208
|
+
for (const cluster of activeClusters) {
|
|
209
|
+
if (cluster.id === mergeCluster1.id || cluster.id === mergeCluster2.id) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const dist = clusterDistance(newCluster, cluster, distances, linkage);
|
|
213
|
+
newDistMap.set(cluster.id, dist);
|
|
214
|
+
// Update reverse direction
|
|
215
|
+
const clusterDistMap = clusterDistances.get(cluster.id);
|
|
216
|
+
if (clusterDistMap) {
|
|
217
|
+
clusterDistMap.set(newCluster.id, dist);
|
|
177
218
|
}
|
|
178
219
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
220
|
+
clusterDistances.set(newCluster.id, newDistMap);
|
|
221
|
+
// Remove merged clusters
|
|
222
|
+
activeClusters.delete(mergeCluster1);
|
|
223
|
+
activeClusters.delete(mergeCluster2);
|
|
224
|
+
clusterDistances.delete(mergeCluster1.id);
|
|
225
|
+
clusterDistances.delete(mergeCluster2.id);
|
|
226
|
+
// Clean up references
|
|
227
|
+
for (const distMap of clusterDistances.values()) {
|
|
228
|
+
distMap.delete(mergeCluster1.id);
|
|
229
|
+
distMap.delete(mergeCluster2.id);
|
|
185
230
|
}
|
|
186
|
-
|
|
187
|
-
clusters.splice(Math.max(merge1, merge2), 1);
|
|
188
|
-
clusters.splice(Math.min(merge1, merge2), 1);
|
|
189
|
-
clusters.push(newCluster);
|
|
231
|
+
activeClusters.add(newCluster);
|
|
190
232
|
}
|
|
191
|
-
//
|
|
233
|
+
// Root is the last remaining cluster
|
|
234
|
+
const root = activeClusters.size > 0 ? Array.from(activeClusters)[0] : dendrogram[dendrogram.length - 1];
|
|
235
|
+
// Generate clusters at different heights
|
|
192
236
|
const clustersByLevel = new Map();
|
|
193
|
-
|
|
194
|
-
if (node.height <= height || !node.left || !node.right) {
|
|
195
|
-
clusters.push(node.members);
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
extractClustersAtHeight(node.left, height, clusters);
|
|
199
|
-
extractClustersAtHeight(node.right, height, clusters);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Handle the case where we have multiple disconnected components
|
|
203
|
-
let root;
|
|
204
|
-
if (clusters.length === 0) {
|
|
237
|
+
if (!root) {
|
|
205
238
|
return {
|
|
206
239
|
root: { id: "empty", members: new Set(), distance: 0, height: 0 },
|
|
207
240
|
dendrogram: [],
|
|
208
241
|
clusters: new Map(),
|
|
209
242
|
};
|
|
210
243
|
}
|
|
211
|
-
else if (clusters.length === 1 && clusters[0]) {
|
|
212
|
-
root = clusters[0];
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
// Create a root that combines all remaining clusters (forest)
|
|
216
|
-
const allMembers = new Set();
|
|
217
|
-
let maxHeight = 0;
|
|
218
|
-
for (const cluster of clusters) {
|
|
219
|
-
for (const member of cluster.members) {
|
|
220
|
-
allMembers.add(member);
|
|
221
|
-
}
|
|
222
|
-
maxHeight = Math.max(maxHeight, cluster.height);
|
|
223
|
-
}
|
|
224
|
-
root = {
|
|
225
|
-
id: "root-forest",
|
|
226
|
-
members: allMembers,
|
|
227
|
-
distance: Infinity,
|
|
228
|
-
height: maxHeight + 1,
|
|
229
|
-
// Store references to the individual trees
|
|
230
|
-
trees: [...clusters],
|
|
231
|
-
};
|
|
232
|
-
dendrogram.push(root);
|
|
233
|
-
}
|
|
234
244
|
for (let h = 0; h <= root.height; h++) {
|
|
235
|
-
|
|
236
|
-
if (clusters.length > 1 && h === root.height) {
|
|
237
|
-
// At forest root height, return single cluster with all members
|
|
238
|
-
clustersAtHeight.push(root.members);
|
|
239
|
-
}
|
|
240
|
-
else if (clusters.length > 1 && h < root.height) {
|
|
241
|
-
// For forest, extract from each tree separately
|
|
242
|
-
for (const tree of clusters) {
|
|
243
|
-
extractClustersAtHeight(tree, h, clustersAtHeight);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
extractClustersAtHeight(root, h, clustersAtHeight);
|
|
248
|
-
}
|
|
249
|
-
clustersByLevel.set(h, clustersAtHeight);
|
|
245
|
+
clustersByLevel.set(h, cutDendrogram(root, h));
|
|
250
246
|
}
|
|
251
247
|
return {
|
|
252
248
|
root,
|
|
@@ -255,39 +251,42 @@ export function hierarchicalClustering(graph, linkage = "single") {
|
|
|
255
251
|
};
|
|
256
252
|
}
|
|
257
253
|
/**
|
|
258
|
-
* Cut dendrogram at
|
|
254
|
+
* Cut dendrogram at specific height to get clusters
|
|
255
|
+
* @param root - Root node of the dendrogram
|
|
256
|
+
* @param height - Height at which to cut the dendrogram
|
|
257
|
+
* @returns Array of sets representing clusters at the specified height
|
|
259
258
|
*/
|
|
260
259
|
export function cutDendrogram(root, height) {
|
|
261
260
|
const clusters = [];
|
|
262
261
|
function traverse(node) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
else {
|
|
270
|
-
// Otherwise, traverse each tree in the forest
|
|
262
|
+
if (node.height <= height || (!node.left && !node.right)) {
|
|
263
|
+
clusters.push(node.members);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
if (node.trees) {
|
|
267
|
+
// Handle forest nodes
|
|
271
268
|
for (const tree of node.trees) {
|
|
272
269
|
traverse(tree);
|
|
273
270
|
}
|
|
274
271
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
traverse(node.right);
|
|
272
|
+
else {
|
|
273
|
+
if (node.left) {
|
|
274
|
+
traverse(node.left);
|
|
275
|
+
}
|
|
276
|
+
if (node.right) {
|
|
277
|
+
traverse(node.right);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
284
280
|
}
|
|
285
281
|
}
|
|
286
282
|
traverse(root);
|
|
287
283
|
return clusters;
|
|
288
284
|
}
|
|
289
285
|
/**
|
|
290
|
-
* Cut dendrogram to get k clusters
|
|
286
|
+
* Cut dendrogram to get exactly k clusters
|
|
287
|
+
* @param root - Root node of the dendrogram
|
|
288
|
+
* @param k - Target number of clusters
|
|
289
|
+
* @returns Array of sets representing exactly k clusters
|
|
291
290
|
*/
|
|
292
291
|
export function cutDendrogramKClusters(root, k) {
|
|
293
292
|
if (k <= 0) {
|
|
@@ -296,7 +295,7 @@ export function cutDendrogramKClusters(root, k) {
|
|
|
296
295
|
if (k === 1) {
|
|
297
296
|
return [root.members];
|
|
298
297
|
}
|
|
299
|
-
//
|
|
298
|
+
// Binary search for the right height
|
|
300
299
|
let low = 0;
|
|
301
300
|
let high = root.height;
|
|
302
301
|
while (low < high) {
|
|
@@ -317,9 +316,12 @@ export function cutDendrogramKClusters(root, k) {
|
|
|
317
316
|
/**
|
|
318
317
|
* Compute modularity-based hierarchical clustering
|
|
319
318
|
* Uses modularity gain to decide merges
|
|
319
|
+
* @param graph - The input graph to cluster
|
|
320
|
+
* @returns Hierarchical clustering result with dendrogram and cluster assignments
|
|
320
321
|
*/
|
|
321
322
|
export function modularityHierarchicalClustering(graph) {
|
|
322
|
-
const
|
|
323
|
+
const adjacencySet = graphToAdjacencySet(graph);
|
|
324
|
+
const nodes = Array.from(adjacencySet.keys());
|
|
323
325
|
const n = nodes.length;
|
|
324
326
|
if (n === 0) {
|
|
325
327
|
return {
|
|
@@ -328,12 +330,15 @@ export function modularityHierarchicalClustering(graph) {
|
|
|
328
330
|
clusters: new Map(),
|
|
329
331
|
};
|
|
330
332
|
}
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
// Compute degrees
|
|
334
|
+
const degrees = new Map();
|
|
335
|
+
let totalEdges = 0;
|
|
336
|
+
for (const [node, neighbors] of adjacencySet) {
|
|
337
|
+
degrees.set(node, neighbors.size);
|
|
338
|
+
totalEdges += neighbors.size;
|
|
335
339
|
}
|
|
336
|
-
|
|
340
|
+
// For undirected graphs
|
|
341
|
+
totalEdges = totalEdges / 2;
|
|
337
342
|
// Initialize clusters
|
|
338
343
|
const clusters = nodes.map((node, i) => ({
|
|
339
344
|
id: `leaf-${String(i)}`,
|
|
@@ -343,83 +348,65 @@ export function modularityHierarchicalClustering(graph) {
|
|
|
343
348
|
}));
|
|
344
349
|
const dendrogram = [...clusters];
|
|
345
350
|
let clusterCount = n;
|
|
346
|
-
//
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
let degree2 = 0;
|
|
354
|
-
for (const node1 of comm1) {
|
|
355
|
-
const neighbors = graph.get(node1);
|
|
356
|
-
if (!neighbors) {
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
degree1 += neighbors.size;
|
|
360
|
-
for (const neighbor of neighbors) {
|
|
361
|
-
if (comm2.has(neighbor)) {
|
|
362
|
-
edgesBetween++;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
for (const node2 of comm2) {
|
|
367
|
-
const node2Neighbors = graph.get(node2);
|
|
368
|
-
if (node2Neighbors) {
|
|
369
|
-
degree2 += node2Neighbors.size;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
// Modularity gain formula
|
|
373
|
-
const gain = (edgesBetween / m) - ((degree1 * degree2) / (4 * m * m));
|
|
374
|
-
return gain;
|
|
375
|
-
}
|
|
376
|
-
// Merge clusters based on modularity
|
|
377
|
-
while (clusters.length > 1) {
|
|
351
|
+
// Active clusters
|
|
352
|
+
const activeClusters = new Set(clusters);
|
|
353
|
+
// Community assignments
|
|
354
|
+
const communities = new Map();
|
|
355
|
+
nodes.forEach((node, i) => communities.set(node, i));
|
|
356
|
+
// Merge clusters based on modularity gain
|
|
357
|
+
while (activeClusters.size > 1) {
|
|
378
358
|
let maxGain = -Infinity;
|
|
379
|
-
let
|
|
380
|
-
let
|
|
381
|
-
for (
|
|
382
|
-
for (
|
|
383
|
-
|
|
384
|
-
const clusterJ = clusters[j];
|
|
385
|
-
if (!clusterI || !clusterJ) {
|
|
359
|
+
let mergeCluster1 = null;
|
|
360
|
+
let mergeCluster2 = null;
|
|
361
|
+
for (const cluster1 of activeClusters) {
|
|
362
|
+
for (const cluster2 of activeClusters) {
|
|
363
|
+
if (cluster1.id >= cluster2.id) {
|
|
386
364
|
continue;
|
|
387
365
|
}
|
|
388
|
-
|
|
366
|
+
// Calculate modularity gain
|
|
367
|
+
let edgesBetween = 0;
|
|
368
|
+
let degreeProduct = 0;
|
|
369
|
+
for (const node1 of cluster1.members) {
|
|
370
|
+
const neighbors = adjacencySet.get(node1);
|
|
371
|
+
const degree1 = degrees.get(node1) ?? 0;
|
|
372
|
+
for (const node2 of cluster2.members) {
|
|
373
|
+
if (neighbors?.has(node2)) {
|
|
374
|
+
edgesBetween++;
|
|
375
|
+
}
|
|
376
|
+
const degree2 = degrees.get(node2) ?? 0;
|
|
377
|
+
degreeProduct += degree1 * degree2;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const gain = edgesBetween / totalEdges - degreeProduct / (4 * totalEdges * totalEdges);
|
|
389
381
|
if (gain > maxGain) {
|
|
390
382
|
maxGain = gain;
|
|
391
|
-
|
|
392
|
-
|
|
383
|
+
mergeCluster1 = cluster1;
|
|
384
|
+
mergeCluster2 = cluster2;
|
|
393
385
|
}
|
|
394
386
|
}
|
|
395
387
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const cluster2 = clusters[merge2];
|
|
399
|
-
if (!cluster1 || !cluster2) {
|
|
400
|
-
continue;
|
|
388
|
+
if (!mergeCluster1 || !mergeCluster2) {
|
|
389
|
+
break;
|
|
401
390
|
}
|
|
391
|
+
// Create new cluster
|
|
402
392
|
const newCluster = {
|
|
403
393
|
id: `cluster-${String(clusterCount++)}`,
|
|
404
|
-
members: new Set([...
|
|
405
|
-
left:
|
|
406
|
-
right:
|
|
394
|
+
members: new Set([...mergeCluster1.members, ...mergeCluster2.members]),
|
|
395
|
+
left: mergeCluster1,
|
|
396
|
+
right: mergeCluster2,
|
|
407
397
|
distance: -maxGain, // Use negative gain as distance
|
|
408
|
-
height: Math.max(
|
|
398
|
+
height: Math.max(mergeCluster1.height, mergeCluster2.height) + 1,
|
|
409
399
|
};
|
|
410
400
|
dendrogram.push(newCluster);
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Remove merged clusters
|
|
416
|
-
clusters.splice(Math.max(merge1, merge2), 1);
|
|
417
|
-
clusters.splice(Math.min(merge1, merge2), 1);
|
|
418
|
-
clusters.push(newCluster);
|
|
401
|
+
// Remove old and add new
|
|
402
|
+
activeClusters.delete(mergeCluster1);
|
|
403
|
+
activeClusters.delete(mergeCluster2);
|
|
404
|
+
activeClusters.add(newCluster);
|
|
419
405
|
}
|
|
420
|
-
//
|
|
406
|
+
// Root is the last remaining cluster
|
|
407
|
+
const root = Array.from(activeClusters)[0] ?? dendrogram[dendrogram.length - 1];
|
|
408
|
+
// Generate clusters at different heights
|
|
421
409
|
const clustersByLevel = new Map();
|
|
422
|
-
const root = clusters[0];
|
|
423
410
|
if (!root) {
|
|
424
411
|
return {
|
|
425
412
|
root: { id: "empty", members: new Set(), distance: 0, height: 0 },
|
|
@@ -436,4 +423,19 @@ export function modularityHierarchicalClustering(graph) {
|
|
|
436
423
|
clusters: clustersByLevel,
|
|
437
424
|
};
|
|
438
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Agglomerative hierarchical clustering
|
|
428
|
+
* Builds clusters bottom-up by merging closest pairs
|
|
429
|
+
* @param graph - Undirected graph - accepts Graph class or Map<T, Set<T>>
|
|
430
|
+
* @param linkage - Linkage method for cluster distance
|
|
431
|
+
* @returns Hierarchical clustering result
|
|
432
|
+
*
|
|
433
|
+
* Time Complexity: O(n³) naive, O(n² log n) with heap
|
|
434
|
+
* Space Complexity: O(n²)
|
|
435
|
+
*/
|
|
436
|
+
export function hierarchicalClustering(graph, linkage = "single") {
|
|
437
|
+
// Convert Graph to adjacency set representation
|
|
438
|
+
const adjacencySet = graphToAdjacencySet(graph);
|
|
439
|
+
return hierarchicalClusteringImpl(adjacencySet, linkage);
|
|
440
|
+
}
|
|
439
441
|
//# sourceMappingURL=hierarchical.js.map
|