@graphty/algorithms 1.0.1 → 1.2.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 +1259 -61
- 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 +551 -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 +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- 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/research/grsbm.d.ts +83 -0
- package/dist/src/research/grsbm.d.ts.map +1 -0
- package/dist/src/research/grsbm.js +411 -0
- package/dist/src/research/grsbm.js.map +1 -0
- package/dist/src/research/index.d.ts +12 -0
- package/dist/src/research/index.d.ts.map +1 -0
- package/dist/src/research/index.js +14 -0
- package/dist/src/research/index.js.map +1 -0
- package/dist/src/research/sync.d.ts +49 -0
- package/dist/src/research/sync.d.ts.map +1 -0
- package/dist/src/research/sync.js +342 -0
- package/dist/src/research/sync.js.map +1 -0
- package/dist/src/research/terahac.d.ts +63 -0
- package/dist/src/research/terahac.d.ts.map +1 -0
- package/dist/src/research/terahac.js +375 -0
- package/dist/src/research/terahac.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 +13 -2
- 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 +672 -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 +5 -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/research/grsbm.ts +589 -0
- package/src/research/index.ts +17 -0
- package/src/research/sync.ts +469 -0
- package/src/research/terahac.ts +506 -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,454 @@
|
|
|
1
|
+
import type {Graph} from "../core/graph.js";
|
|
2
|
+
import type {NodeId} from "../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Markov Clustering (MCL) algorithm implementation
|
|
6
|
+
*
|
|
7
|
+
* MCL simulates flow in graphs and finds clusters based on the notion that
|
|
8
|
+
* random walks stay within clusters and rarely move between clusters.
|
|
9
|
+
* The algorithm alternates between expansion (matrix squaring) and
|
|
10
|
+
* inflation (element-wise powering and normalization).
|
|
11
|
+
*
|
|
12
|
+
* Time complexity: O(V³) per iteration
|
|
13
|
+
* Space complexity: O(V²)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface MCLOptions {
|
|
17
|
+
expansion?: number; // Expansion parameter (default: 2)
|
|
18
|
+
inflation?: number; // Inflation parameter (default: 2)
|
|
19
|
+
maxIterations?: number; // Maximum iterations (default: 100)
|
|
20
|
+
tolerance?: number; // Convergence tolerance (default: 1e-6)
|
|
21
|
+
pruningThreshold?: number; // Pruning threshold (default: 1e-5)
|
|
22
|
+
selfLoops?: boolean; // Add self-loops (default: true)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface MCLResult {
|
|
26
|
+
communities: NodeId[][];
|
|
27
|
+
attractors: Set<NodeId>; // Attractor nodes (cluster representatives)
|
|
28
|
+
iterations: number;
|
|
29
|
+
converged: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Perform Markov Clustering on a graph
|
|
34
|
+
*/
|
|
35
|
+
export function markovClustering(
|
|
36
|
+
graph: Graph,
|
|
37
|
+
options: MCLOptions = {},
|
|
38
|
+
): MCLResult {
|
|
39
|
+
const {
|
|
40
|
+
expansion = 2,
|
|
41
|
+
inflation = 2,
|
|
42
|
+
maxIterations = 100,
|
|
43
|
+
tolerance = 1e-6,
|
|
44
|
+
pruningThreshold = 1e-5,
|
|
45
|
+
selfLoops = true,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
const nodes = Array.from(graph.nodes());
|
|
49
|
+
const nodeIds = nodes.map((node) => node.id);
|
|
50
|
+
const n = nodeIds.length;
|
|
51
|
+
|
|
52
|
+
if (n === 0) {
|
|
53
|
+
return {
|
|
54
|
+
communities: [],
|
|
55
|
+
attractors: new Set(),
|
|
56
|
+
iterations: 0,
|
|
57
|
+
converged: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build initial transition matrix
|
|
62
|
+
let matrix = buildTransitionMatrix(graph, nodeIds, selfLoops);
|
|
63
|
+
|
|
64
|
+
let converged = false;
|
|
65
|
+
let iteration = 0;
|
|
66
|
+
|
|
67
|
+
for (iteration = 0; iteration < maxIterations; iteration++) {
|
|
68
|
+
const oldMatrix = matrix.map((row) => [... row]);
|
|
69
|
+
|
|
70
|
+
// Expansion step (matrix multiplication)
|
|
71
|
+
matrix = matrixPower(matrix, expansion);
|
|
72
|
+
|
|
73
|
+
// Inflation step (element-wise powering and column normalization)
|
|
74
|
+
matrix = inflate(matrix, inflation);
|
|
75
|
+
|
|
76
|
+
// Pruning step (remove small values)
|
|
77
|
+
matrix = prune(matrix, pruningThreshold);
|
|
78
|
+
|
|
79
|
+
// Check for convergence
|
|
80
|
+
if (hasConverged(oldMatrix, matrix, tolerance)) {
|
|
81
|
+
converged = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Extract clusters from final matrix
|
|
87
|
+
const {communities, attractors} = extractClusters(matrix, nodeIds);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
communities,
|
|
91
|
+
attractors,
|
|
92
|
+
iterations: iteration + 1,
|
|
93
|
+
converged,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build initial transition matrix from graph
|
|
99
|
+
*/
|
|
100
|
+
function buildTransitionMatrix(graph: Graph, nodeIds: NodeId[], selfLoops: boolean): number[][] {
|
|
101
|
+
const n = nodeIds.length;
|
|
102
|
+
const matrix = Array.from({length: n}, (): number[] => Array(n).fill(0) as number[]);
|
|
103
|
+
const nodeToIndex = new Map<NodeId, number>();
|
|
104
|
+
|
|
105
|
+
nodeIds.forEach((id, index) => nodeToIndex.set(id, index));
|
|
106
|
+
|
|
107
|
+
// Fill adjacency values
|
|
108
|
+
for (let i = 0; i < n; i++) {
|
|
109
|
+
const nodeId = nodeIds[i];
|
|
110
|
+
if (!nodeId) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const neighbors = graph.neighbors(nodeId);
|
|
115
|
+
|
|
116
|
+
for (const neighbor of neighbors) {
|
|
117
|
+
const j = nodeToIndex.get(neighbor);
|
|
118
|
+
if (j !== undefined) {
|
|
119
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
120
|
+
const weight = edge?.weight ?? 1;
|
|
121
|
+
const row = matrix[i];
|
|
122
|
+
if (!row) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
row[j] = weight;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add self-loops
|
|
131
|
+
if (selfLoops) {
|
|
132
|
+
const row = matrix[i];
|
|
133
|
+
if (!row) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
row[i] = 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Column-normalize the matrix
|
|
142
|
+
for (let j = 0; j < n; j++) {
|
|
143
|
+
let colSum = 0;
|
|
144
|
+
for (let i = 0; i < n; i++) {
|
|
145
|
+
const val = matrix[i]?.[j];
|
|
146
|
+
if (val !== undefined) {
|
|
147
|
+
colSum += val;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (colSum > 0) {
|
|
152
|
+
for (let i = 0; i < n; i++) {
|
|
153
|
+
const row = matrix[i];
|
|
154
|
+
if (!row) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const val = row[j];
|
|
159
|
+
if (val !== undefined) {
|
|
160
|
+
row[j] = val / colSum;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return matrix;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Raise matrix to a power (for expansion step)
|
|
171
|
+
*/
|
|
172
|
+
function matrixPower(matrix: number[][], power: number): number[][] {
|
|
173
|
+
if (power === 1) {
|
|
174
|
+
return matrix;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (power === 2) {
|
|
178
|
+
return matrixMultiply(matrix, matrix);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let result = matrix;
|
|
182
|
+
for (let i = 1; i < power; i++) {
|
|
183
|
+
result = matrixMultiply(result, matrix);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Multiply two matrices
|
|
190
|
+
*/
|
|
191
|
+
function matrixMultiply(a: number[][], b: number[][]): number[][] {
|
|
192
|
+
const n = a.length;
|
|
193
|
+
const m = b[0]?.length ?? 0;
|
|
194
|
+
const p = b.length;
|
|
195
|
+
|
|
196
|
+
const result: number[][] = Array.from({length: n}, () => Array(m).fill(0) as number[]);
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < n; i++) {
|
|
199
|
+
for (let j = 0; j < m; j++) {
|
|
200
|
+
for (let k = 0; k < p; k++) {
|
|
201
|
+
const aVal = a[i]?.[k] ?? 0;
|
|
202
|
+
const bVal = b[k]?.[j] ?? 0;
|
|
203
|
+
const resultRow = result[i];
|
|
204
|
+
if (!resultRow) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const prevVal = resultRow[j];
|
|
209
|
+
if (prevVal !== undefined) {
|
|
210
|
+
resultRow[j] = prevVal + (aVal * bVal);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Inflation step: element-wise powering and column normalization
|
|
221
|
+
*/
|
|
222
|
+
function inflate(matrix: number[][], inflation: number): number[][] {
|
|
223
|
+
const n = matrix.length;
|
|
224
|
+
const result: number[][] = Array.from({length: n}, () => Array(n).fill(0) as number[]);
|
|
225
|
+
|
|
226
|
+
// Element-wise powering
|
|
227
|
+
for (let i = 0; i < n; i++) {
|
|
228
|
+
for (let j = 0; j < n; j++) {
|
|
229
|
+
const val = matrix[i]?.[j];
|
|
230
|
+
if (val !== undefined) {
|
|
231
|
+
const resultRow = result[i];
|
|
232
|
+
if (resultRow) {
|
|
233
|
+
resultRow[j] = Math.pow(val, inflation);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Column normalization
|
|
240
|
+
for (let j = 0; j < n; j++) {
|
|
241
|
+
let colSum = 0;
|
|
242
|
+
for (let i = 0; i < n; i++) {
|
|
243
|
+
const val = result[i]?.[j];
|
|
244
|
+
if (val !== undefined) {
|
|
245
|
+
colSum += val;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (colSum > 0) {
|
|
250
|
+
for (let i = 0; i < n; i++) {
|
|
251
|
+
const row = result[i];
|
|
252
|
+
if (!row) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const val = row[j];
|
|
257
|
+
if (val !== undefined) {
|
|
258
|
+
row[j] = val / colSum;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pruning step: remove small values
|
|
269
|
+
*/
|
|
270
|
+
function prune(matrix: number[][], threshold: number): number[][] {
|
|
271
|
+
const n = matrix.length;
|
|
272
|
+
const result: number[][] = Array.from({length: n}, () => Array(n).fill(0) as number[]);
|
|
273
|
+
|
|
274
|
+
for (let i = 0; i < n; i++) {
|
|
275
|
+
for (let j = 0; j < n; j++) {
|
|
276
|
+
const matrixRow = matrix[i];
|
|
277
|
+
if (!matrixRow) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const matrixVal = matrixRow[j];
|
|
282
|
+
if (matrixVal !== undefined && matrixVal >= threshold) {
|
|
283
|
+
const resultRow = result[i];
|
|
284
|
+
if (resultRow) {
|
|
285
|
+
resultRow[j] = matrixVal;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Re-normalize columns after pruning
|
|
292
|
+
for (let j = 0; j < n; j++) {
|
|
293
|
+
let colSum = 0;
|
|
294
|
+
for (let i = 0; i < n; i++) {
|
|
295
|
+
const resultRow = result[i];
|
|
296
|
+
if (!resultRow) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const val = resultRow[j];
|
|
301
|
+
if (val !== undefined) {
|
|
302
|
+
colSum += val;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (colSum > 0) {
|
|
307
|
+
for (let i = 0; i < n; i++) {
|
|
308
|
+
const resultRow = result[i];
|
|
309
|
+
if (!resultRow) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const val = resultRow[j];
|
|
314
|
+
if (val !== undefined) {
|
|
315
|
+
resultRow[j] = val / colSum;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Check if the algorithm has converged
|
|
326
|
+
*/
|
|
327
|
+
function hasConverged(oldMatrix: number[][], newMatrix: number[][], tolerance: number): boolean {
|
|
328
|
+
const n = oldMatrix.length;
|
|
329
|
+
|
|
330
|
+
for (let i = 0; i < n; i++) {
|
|
331
|
+
for (let j = 0; j < n; j++) {
|
|
332
|
+
const oldRow = oldMatrix[i];
|
|
333
|
+
const newRow = newMatrix[i];
|
|
334
|
+
if (!oldRow || !newRow) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const oldVal = oldRow[j];
|
|
339
|
+
const newVal = newRow[j];
|
|
340
|
+
if (oldVal !== undefined && newVal !== undefined && Math.abs(oldVal - newVal) > tolerance) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Extract clusters from the final matrix
|
|
351
|
+
*/
|
|
352
|
+
function extractClusters(matrix: number[][], nodeIds: NodeId[]): {
|
|
353
|
+
communities: NodeId[][];
|
|
354
|
+
attractors: Set<NodeId>;
|
|
355
|
+
} {
|
|
356
|
+
const n = matrix.length;
|
|
357
|
+
const attractors = new Set<NodeId>();
|
|
358
|
+
const communities: NodeId[][] = [];
|
|
359
|
+
const nodeToCluster = new Map<number, number>();
|
|
360
|
+
|
|
361
|
+
// Find attractors (columns with non-zero diagonal elements)
|
|
362
|
+
for (let i = 0; i < n; i++) {
|
|
363
|
+
const matrixRow = matrix[i];
|
|
364
|
+
if (!matrixRow) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const diagonalVal = matrixRow[i];
|
|
369
|
+
const nodeId = nodeIds[i];
|
|
370
|
+
if (diagonalVal !== undefined && diagonalVal > 0 && nodeId !== undefined) {
|
|
371
|
+
attractors.add(nodeId);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Assign nodes to clusters based on columns
|
|
376
|
+
let clusterIndex = 0;
|
|
377
|
+
for (let j = 0; j < n; j++) {
|
|
378
|
+
// Find nodes that belong to this cluster (column)
|
|
379
|
+
const clusterNodes: number[] = [];
|
|
380
|
+
for (let i = 0; i < n; i++) {
|
|
381
|
+
const matrixRow = matrix[i];
|
|
382
|
+
if (!matrixRow) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const val = matrixRow[j];
|
|
387
|
+
if (val !== undefined && val > 0 && !nodeToCluster.has(i)) {
|
|
388
|
+
clusterNodes.push(i);
|
|
389
|
+
nodeToCluster.set(i, clusterIndex);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (clusterNodes.length > 0) {
|
|
394
|
+
communities.push(clusterNodes.map((idx) => {
|
|
395
|
+
const nodeId = nodeIds[idx];
|
|
396
|
+
return nodeId;
|
|
397
|
+
}).filter((node): node is NodeId => node !== undefined));
|
|
398
|
+
clusterIndex++;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Handle isolated nodes
|
|
403
|
+
for (let i = 0; i < n; i++) {
|
|
404
|
+
if (!nodeToCluster.has(i)) {
|
|
405
|
+
const nodeId = nodeIds[i];
|
|
406
|
+
if (nodeId !== undefined) {
|
|
407
|
+
communities.push([nodeId]);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
nodeToCluster.set(i, clusterIndex++);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {communities, attractors};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Calculate modularity of MCL clustering result
|
|
419
|
+
*/
|
|
420
|
+
export function calculateMCLModularity(
|
|
421
|
+
graph: Graph,
|
|
422
|
+
communities: NodeId[][],
|
|
423
|
+
): number {
|
|
424
|
+
const m = graph.totalEdgeCount;
|
|
425
|
+
if (m === 0) {
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let modularity = 0;
|
|
430
|
+
const communityMap = new Map<NodeId, number>();
|
|
431
|
+
|
|
432
|
+
// Build community map
|
|
433
|
+
communities.forEach((community, index) => {
|
|
434
|
+
community.forEach((nodeId) => {
|
|
435
|
+
communityMap.set(nodeId, index);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Calculate modularity
|
|
440
|
+
for (const edge of graph.edges()) {
|
|
441
|
+
const sourceCommunity = communityMap.get(edge.source);
|
|
442
|
+
const targetCommunity = communityMap.get(edge.target);
|
|
443
|
+
|
|
444
|
+
if (sourceCommunity !== undefined && targetCommunity !== undefined && sourceCommunity === targetCommunity) {
|
|
445
|
+
modularity += 1;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const sourceDegree = graph.degree(edge.source);
|
|
449
|
+
const targetDegree = graph.degree(edge.target);
|
|
450
|
+
modularity -= (sourceDegree * targetDegree) / (2 * m);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return modularity / (2 * m);
|
|
454
|
+
}
|