@graphty/algorithms 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +827 -60
- package/dist/algorithms.d.ts +2 -0
- package/dist/algorithms.d.ts.map +1 -0
- package/dist/algorithms.js +2 -0
- package/dist/algorithms.js.map +1 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +22 -7
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +7 -7
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +1 -1
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/eigenvector.d.ts +27 -0
- package/dist/src/algorithms/centrality/eigenvector.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/eigenvector.js +113 -0
- package/dist/src/algorithms/centrality/eigenvector.js.map +1 -0
- package/dist/src/algorithms/centrality/hits.d.ts +35 -0
- package/dist/src/algorithms/centrality/hits.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/hits.js +146 -0
- package/dist/src/algorithms/centrality/hits.js.map +1 -0
- package/dist/src/algorithms/centrality/index.d.ts +6 -0
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/index.js +3 -0
- package/dist/src/algorithms/centrality/index.js.map +1 -1
- package/dist/src/algorithms/centrality/katz.d.ts +30 -0
- package/dist/src/algorithms/centrality/katz.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/katz.js +85 -0
- package/dist/src/algorithms/centrality/katz.js.map +1 -0
- package/dist/src/algorithms/centrality/pagerank.js +3 -3
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.d.ts +23 -0
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -0
- package/dist/src/algorithms/community/girvan-newman.js +297 -0
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -0
- package/dist/src/algorithms/community/index.d.ts +12 -0
- package/dist/src/algorithms/community/index.d.ts.map +1 -0
- package/dist/src/algorithms/community/index.js +12 -0
- package/dist/src/algorithms/community/index.js.map +1 -0
- package/dist/src/algorithms/community/label-propagation.d.ts +42 -0
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -0
- package/dist/src/algorithms/community/label-propagation.js +307 -0
- package/dist/src/algorithms/community/label-propagation.js.map +1 -0
- package/dist/src/algorithms/community/leiden.d.ts +33 -0
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -0
- package/dist/src/algorithms/community/leiden.js +434 -0
- package/dist/src/algorithms/community/leiden.js.map +1 -0
- package/dist/src/algorithms/community/louvain.d.ts +19 -0
- package/dist/src/algorithms/community/louvain.d.ts.map +1 -0
- package/dist/src/algorithms/community/louvain.js +219 -0
- package/dist/src/algorithms/community/louvain.js.map +1 -0
- package/dist/src/algorithms/components/connected.js +7 -7
- package/dist/src/algorithms/components/connected.js.map +1 -1
- package/dist/src/algorithms/index.d.ts +7 -0
- package/dist/src/algorithms/index.d.ts.map +1 -1
- package/dist/src/algorithms/index.js +14 -0
- package/dist/src/algorithms/index.js.map +1 -1
- package/dist/src/algorithms/matching/bipartite.d.ts +37 -0
- package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -0
- package/dist/src/algorithms/matching/bipartite.js +127 -0
- package/dist/src/algorithms/matching/bipartite.js.map +1 -0
- package/dist/src/algorithms/matching/index.d.ts +8 -0
- package/dist/src/algorithms/matching/index.d.ts.map +1 -0
- package/dist/src/algorithms/matching/index.js +6 -0
- package/dist/src/algorithms/matching/index.js.map +1 -0
- package/dist/src/algorithms/matching/isomorphism.d.ts +32 -0
- package/dist/src/algorithms/matching/isomorphism.d.ts.map +1 -0
- package/dist/src/algorithms/matching/isomorphism.js +291 -0
- package/dist/src/algorithms/matching/isomorphism.js.map +1 -0
- package/dist/src/algorithms/mst/index.d.ts +4 -0
- package/dist/src/algorithms/mst/index.d.ts.map +1 -0
- package/dist/src/algorithms/mst/index.js +3 -0
- package/dist/src/algorithms/mst/index.js.map +1 -0
- package/dist/src/algorithms/mst/kruskal.d.ts +9 -0
- package/dist/src/algorithms/mst/kruskal.d.ts.map +1 -0
- package/dist/src/algorithms/mst/kruskal.js +43 -0
- package/dist/src/algorithms/mst/kruskal.js.map +1 -0
- package/dist/src/algorithms/mst/prim.d.ts +5 -0
- package/dist/src/algorithms/mst/prim.d.ts.map +1 -0
- package/dist/src/algorithms/mst/prim.js +63 -0
- package/dist/src/algorithms/mst/prim.js.map +1 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js +2 -2
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +5 -5
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts +14 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.js +129 -0
- package/dist/src/algorithms/shortest-path/floyd-warshall.js.map +1 -0
- package/dist/src/algorithms/shortest-path/index.d.ts +2 -0
- package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/index.js +1 -0
- package/dist/src/algorithms/shortest-path/index.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +2 -2
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/algorithms/traversal/dfs.js +11 -11
- package/dist/src/algorithms/traversal/dfs.js.map +1 -1
- package/dist/src/clustering/hierarchical.d.ts +48 -0
- package/dist/src/clustering/hierarchical.d.ts.map +1 -0
- package/dist/src/clustering/hierarchical.js +439 -0
- package/dist/src/clustering/hierarchical.js.map +1 -0
- package/dist/src/clustering/index.d.ts +7 -0
- package/dist/src/clustering/index.d.ts.map +1 -0
- package/dist/src/clustering/index.js +5 -0
- package/dist/src/clustering/index.js.map +1 -0
- package/dist/src/clustering/k-core.d.ts +63 -0
- package/dist/src/clustering/k-core.d.ts.map +1 -0
- package/dist/src/clustering/k-core.js +377 -0
- package/dist/src/clustering/k-core.js.map +1 -0
- package/dist/src/clustering/mcl.d.ts +36 -0
- package/dist/src/clustering/mcl.d.ts.map +1 -0
- package/dist/src/clustering/mcl.js +343 -0
- package/dist/src/clustering/mcl.js.map +1 -0
- package/dist/src/clustering/spectral.d.ts +28 -0
- package/dist/src/clustering/spectral.d.ts.map +1 -0
- package/dist/src/clustering/spectral.js +447 -0
- package/dist/src/clustering/spectral.js.map +1 -0
- package/dist/src/core/graph.d.ts +9 -0
- package/dist/src/core/graph.d.ts.map +1 -1
- package/dist/src/core/graph.js +31 -11
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/data-structures/priority-queue.js +0 -2
- package/dist/src/data-structures/priority-queue.js.map +1 -1
- package/dist/src/data-structures/union-find.js +2 -5
- package/dist/src/data-structures/union-find.js.map +1 -1
- package/dist/src/flow/ford-fulkerson.d.ts +53 -0
- package/dist/src/flow/ford-fulkerson.d.ts.map +1 -0
- package/dist/src/flow/ford-fulkerson.js +355 -0
- package/dist/src/flow/ford-fulkerson.js.map +1 -0
- package/dist/src/flow/index.d.ts +3 -0
- package/dist/src/flow/index.d.ts.map +1 -0
- package/dist/src/flow/index.js +3 -0
- package/dist/src/flow/index.js.map +1 -0
- package/dist/src/flow/min-cut.d.ts +49 -0
- package/dist/src/flow/min-cut.d.ts.map +1 -0
- package/dist/src/flow/min-cut.js +383 -0
- package/dist/src/flow/min-cut.js.map +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/link-prediction/adamic-adar.d.ts +56 -0
- package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -0
- package/dist/src/link-prediction/adamic-adar.js +256 -0
- package/dist/src/link-prediction/adamic-adar.js.map +1 -0
- package/dist/src/link-prediction/common-neighbors.d.ts +50 -0
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -0
- package/dist/src/link-prediction/common-neighbors.js +155 -0
- package/dist/src/link-prediction/common-neighbors.js.map +1 -0
- package/dist/src/link-prediction/index.d.ts +7 -0
- package/dist/src/link-prediction/index.d.ts.map +1 -0
- package/dist/src/link-prediction/index.js +6 -0
- package/dist/src/link-prediction/index.js.map +1 -0
- package/dist/src/pathfinding/astar.d.ts +50 -0
- package/dist/src/pathfinding/astar.d.ts.map +1 -0
- package/dist/src/pathfinding/astar.js +180 -0
- package/dist/src/pathfinding/astar.js.map +1 -0
- package/dist/src/pathfinding/index.d.ts +3 -0
- package/dist/src/pathfinding/index.d.ts.map +1 -0
- package/dist/src/pathfinding/index.js +3 -0
- package/dist/src/pathfinding/index.js.map +1 -0
- package/dist/src/pathfinding/utils.d.ts +18 -0
- package/dist/src/pathfinding/utils.d.ts.map +1 -0
- package/dist/src/pathfinding/utils.js +68 -0
- package/dist/src/pathfinding/utils.js.map +1 -0
- package/dist/src/types/index.d.ts +9 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/utils/graphNode.d.ts +8 -0
- package/dist/src/utils/graphNode.d.ts.map +1 -0
- package/dist/src/utils/graphNode.js +2 -0
- package/dist/src/utils/graphNode.js.map +1 -0
- package/dist/src/utils/priorityQueue.d.ts +21 -0
- package/dist/src/utils/priorityQueue.d.ts.map +1 -0
- package/dist/src/utils/priorityQueue.js +101 -0
- package/dist/src/utils/priorityQueue.js.map +1 -0
- package/package.json +3 -1
- package/src/algorithms/centrality/betweenness.ts +25 -7
- package/src/algorithms/centrality/closeness.ts +7 -7
- package/src/algorithms/centrality/degree.ts +1 -1
- package/src/algorithms/centrality/eigenvector.ts +162 -0
- package/src/algorithms/centrality/hits.ts +210 -0
- package/src/algorithms/centrality/index.ts +6 -0
- package/src/algorithms/centrality/katz.ts +139 -0
- package/src/algorithms/centrality/pagerank.ts +3 -3
- package/src/algorithms/community/girvan-newman.ts +372 -0
- package/src/algorithms/community/index.ts +13 -0
- package/src/algorithms/community/label-propagation.ts +392 -0
- package/src/algorithms/community/leiden.ts +572 -0
- package/src/algorithms/community/louvain.ts +298 -0
- package/src/algorithms/components/connected.ts +7 -7
- package/src/algorithms/index.ts +21 -0
- package/src/algorithms/matching/bipartite.ts +179 -0
- package/src/algorithms/matching/index.ts +8 -0
- package/src/algorithms/matching/isomorphism.ts +392 -0
- package/src/algorithms/mst/index.ts +4 -0
- package/src/algorithms/mst/kruskal.ts +61 -0
- package/src/algorithms/mst/prim.ts +80 -0
- package/src/algorithms/shortest-path/bellman-ford.ts +2 -2
- package/src/algorithms/shortest-path/dijkstra.ts +5 -5
- package/src/algorithms/shortest-path/floyd-warshall.ts +170 -0
- package/src/algorithms/shortest-path/index.ts +2 -0
- package/src/algorithms/traversal/bfs.ts +2 -2
- package/src/algorithms/traversal/dfs.ts +11 -11
- package/src/clustering/hierarchical.ts +555 -0
- package/src/clustering/index.ts +6 -0
- package/src/clustering/k-core.ts +460 -0
- package/src/clustering/mcl.ts +454 -0
- package/src/clustering/spectral.ts +545 -0
- package/src/core/graph.ts +34 -6
- package/src/data-structures/union-find.ts +2 -2
- package/src/flow/ford-fulkerson.ts +459 -0
- package/src/flow/index.ts +2 -0
- package/src/flow/min-cut.ts +482 -0
- package/src/index.ts +2 -0
- package/src/link-prediction/adamic-adar.ts +353 -0
- package/src/link-prediction/common-neighbors.ts +252 -0
- package/src/link-prediction/index.ts +20 -0
- package/src/pathfinding/astar.ts +229 -0
- package/src/pathfinding/index.ts +2 -0
- package/src/pathfinding/utils.ts +87 -0
- package/src/types/index.ts +11 -0
- package/src/utils/graphNode.ts +7 -0
- package/src/utils/priorityQueue.ts +120 -0
- package/dist/test/browser/basic.test.d.ts +0 -2
- package/dist/test/browser/basic.test.d.ts.map +0 -1
- package/dist/test/browser/basic.test.js +0 -70
- package/dist/test/browser/basic.test.js.map +0 -1
- package/dist/test/unit/bellman-ford.test.d.ts +0 -2
- package/dist/test/unit/bellman-ford.test.d.ts.map +0 -1
- package/dist/test/unit/bellman-ford.test.js +0 -231
- package/dist/test/unit/bellman-ford.test.js.map +0 -1
- package/dist/test/unit/betweenness-centrality.test.d.ts +0 -2
- package/dist/test/unit/betweenness-centrality.test.d.ts.map +0 -1
- package/dist/test/unit/betweenness-centrality.test.js +0 -311
- package/dist/test/unit/betweenness-centrality.test.js.map +0 -1
- package/dist/test/unit/bfs.test.d.ts +0 -2
- package/dist/test/unit/bfs.test.d.ts.map +0 -1
- package/dist/test/unit/bfs.test.js +0 -277
- package/dist/test/unit/bfs.test.js.map +0 -1
- package/dist/test/unit/closeness.test.d.ts +0 -2
- package/dist/test/unit/closeness.test.d.ts.map +0 -1
- package/dist/test/unit/closeness.test.js +0 -266
- package/dist/test/unit/closeness.test.js.map +0 -1
- package/dist/test/unit/connected-components.test.d.ts +0 -2
- package/dist/test/unit/connected-components.test.d.ts.map +0 -1
- package/dist/test/unit/connected-components.test.js +0 -355
- package/dist/test/unit/connected-components.test.js.map +0 -1
- package/dist/test/unit/degree-centrality.test.d.ts +0 -2
- package/dist/test/unit/degree-centrality.test.d.ts.map +0 -1
- package/dist/test/unit/degree-centrality.test.js +0 -260
- package/dist/test/unit/degree-centrality.test.js.map +0 -1
- package/dist/test/unit/dfs.test.d.ts +0 -2
- package/dist/test/unit/dfs.test.d.ts.map +0 -1
- package/dist/test/unit/dfs.test.js +0 -350
- package/dist/test/unit/dfs.test.js.map +0 -1
- package/dist/test/unit/dijkstra.test.d.ts +0 -2
- package/dist/test/unit/dijkstra.test.d.ts.map +0 -1
- package/dist/test/unit/dijkstra.test.js +0 -311
- package/dist/test/unit/dijkstra.test.js.map +0 -1
- package/dist/test/unit/graph.test.d.ts +0 -2
- package/dist/test/unit/graph.test.d.ts.map +0 -1
- package/dist/test/unit/graph.test.js +0 -175
- package/dist/test/unit/graph.test.js.map +0 -1
- package/dist/test/unit/pagerank.test.d.ts +0 -2
- package/dist/test/unit/pagerank.test.d.ts.map +0 -1
- package/dist/test/unit/pagerank.test.js +0 -281
- package/dist/test/unit/pagerank.test.js.map +0 -1
- package/dist/test/unit/priority-queue.test.d.ts +0 -2
- package/dist/test/unit/priority-queue.test.d.ts.map +0 -1
- package/dist/test/unit/priority-queue.test.js +0 -189
- package/dist/test/unit/priority-queue.test.js.map +0 -1
- package/dist/test/unit/union-find.test.d.ts +0 -2
- package/dist/test/unit/union-find.test.d.ts.map +0 -1
- package/dist/test/unit/union-find.test.js +0 -245
- package/dist/test/unit/union-find.test.js.map +0 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {CommunityResult, LouvainOptions, NodeId} from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Louvain community detection algorithm
|
|
6
|
+
*
|
|
7
|
+
* Implements the Louvain method for community detection in graphs.
|
|
8
|
+
* Uses modularity optimization to find community structure.
|
|
9
|
+
*
|
|
10
|
+
* References:
|
|
11
|
+
* - Blondel, V. D., Guillaume, J. L., Lambiotte, R., & Lefebvre, E. (2008).
|
|
12
|
+
* Fast unfolding of communities in large networks.
|
|
13
|
+
* Journal of statistical mechanics: theory and experiment, 2008(10), P10008.
|
|
14
|
+
*
|
|
15
|
+
* @param graph - The input graph
|
|
16
|
+
* @param options - Algorithm options
|
|
17
|
+
* @returns Community detection result with communities, modularity, and iterations
|
|
18
|
+
*/
|
|
19
|
+
export function louvain(
|
|
20
|
+
graph: Graph,
|
|
21
|
+
options: LouvainOptions = {},
|
|
22
|
+
): CommunityResult {
|
|
23
|
+
const resolution = options.resolution ?? 1.0;
|
|
24
|
+
const maxIterations = options.maxIterations ?? 100;
|
|
25
|
+
const tolerance = options.tolerance ?? 1e-6;
|
|
26
|
+
|
|
27
|
+
// Initialize: each node in its own community
|
|
28
|
+
const communities = initializeCommunities(graph);
|
|
29
|
+
let modularity = calculateModularity(graph, communities, resolution);
|
|
30
|
+
let iteration = 0;
|
|
31
|
+
let improved = true;
|
|
32
|
+
|
|
33
|
+
while (iteration < maxIterations && improved) {
|
|
34
|
+
// Phase 1: Local optimization
|
|
35
|
+
improved = louvainPhase1(graph, communities, resolution);
|
|
36
|
+
|
|
37
|
+
if (improved) {
|
|
38
|
+
const newModularity = calculateModularity(graph, communities, resolution);
|
|
39
|
+
|
|
40
|
+
// Check for convergence
|
|
41
|
+
if ((newModularity - modularity) < tolerance) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
modularity = newModularity;
|
|
46
|
+
iteration++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
communities: extractCommunities(communities),
|
|
52
|
+
modularity: calculateModularity(graph, communities, resolution),
|
|
53
|
+
iterations: iteration,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initialize communities: each node in its own community
|
|
59
|
+
*/
|
|
60
|
+
function initializeCommunities(graph: Graph): Map<NodeId, number> {
|
|
61
|
+
const communities = new Map<NodeId, number>();
|
|
62
|
+
let communityId = 0;
|
|
63
|
+
|
|
64
|
+
for (const node of graph.nodes()) {
|
|
65
|
+
communities.set(node.id, communityId++);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return communities;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Phase 1 of Louvain algorithm: Local optimization
|
|
73
|
+
* For each node, try moving to neighboring communities and keep the move
|
|
74
|
+
* that provides the best modularity gain.
|
|
75
|
+
*/
|
|
76
|
+
function louvainPhase1(
|
|
77
|
+
graph: Graph,
|
|
78
|
+
communities: Map<NodeId, number>,
|
|
79
|
+
resolution: number,
|
|
80
|
+
): boolean {
|
|
81
|
+
let globalImprovement = false;
|
|
82
|
+
let localImprovement = true;
|
|
83
|
+
|
|
84
|
+
while (localImprovement) {
|
|
85
|
+
localImprovement = false;
|
|
86
|
+
|
|
87
|
+
for (const node of graph.nodes()) {
|
|
88
|
+
const nodeId = node.id;
|
|
89
|
+
const currentCommunity = communities.get(nodeId);
|
|
90
|
+
|
|
91
|
+
if (currentCommunity === undefined) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Calculate current modularity contribution
|
|
96
|
+
const currentModularity = nodeModularityContribution(
|
|
97
|
+
graph, nodeId, currentCommunity, communities, resolution,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
let bestCommunity = currentCommunity;
|
|
101
|
+
let bestModularity = currentModularity;
|
|
102
|
+
|
|
103
|
+
// Try moving to neighboring communities
|
|
104
|
+
const neighborCommunities = getNeighborCommunities(graph, nodeId, communities);
|
|
105
|
+
|
|
106
|
+
for (const neighborCommunity of neighborCommunities) {
|
|
107
|
+
if (neighborCommunity === currentCommunity) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const newModularity = nodeModularityContribution(
|
|
112
|
+
graph, nodeId, neighborCommunity, communities, resolution,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (newModularity > bestModularity) {
|
|
116
|
+
bestModularity = newModularity;
|
|
117
|
+
bestCommunity = neighborCommunity;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Move node if improvement found
|
|
122
|
+
if (bestCommunity !== currentCommunity) {
|
|
123
|
+
communities.set(nodeId, bestCommunity);
|
|
124
|
+
localImprovement = true;
|
|
125
|
+
globalImprovement = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return globalImprovement;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Calculate modularity contribution of a node to a specific community
|
|
135
|
+
*/
|
|
136
|
+
function nodeModularityContribution(
|
|
137
|
+
graph: Graph,
|
|
138
|
+
nodeId: NodeId,
|
|
139
|
+
community: number,
|
|
140
|
+
communities: Map<NodeId, number>,
|
|
141
|
+
resolution: number,
|
|
142
|
+
): number {
|
|
143
|
+
const totalEdgeWeight = getTotalEdgeWeight(graph);
|
|
144
|
+
if (totalEdgeWeight === 0) {
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let internalLinks = 0;
|
|
149
|
+
let nodeDegree = 0;
|
|
150
|
+
let communityDegree = 0;
|
|
151
|
+
|
|
152
|
+
// Calculate node's connections to the community
|
|
153
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
154
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
155
|
+
const weight = edge?.weight ?? 1;
|
|
156
|
+
nodeDegree += weight;
|
|
157
|
+
|
|
158
|
+
if (communities.get(neighbor) === community) {
|
|
159
|
+
internalLinks += weight;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Calculate total degree of the community (excluding the node if it's currently in it)
|
|
164
|
+
for (const [otherNodeId, otherCommunity] of communities) {
|
|
165
|
+
if (otherCommunity === community && otherNodeId !== nodeId) {
|
|
166
|
+
communityDegree += getNodeDegree(graph, otherNodeId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Modularity formula: Q = (1/2m) * Σ[A_ij - (k_i * k_j)/(2m)] * δ(c_i, c_j)
|
|
171
|
+
const modularityIncrease = (internalLinks - ((resolution * nodeDegree * communityDegree) / (2 * totalEdgeWeight))) / totalEdgeWeight;
|
|
172
|
+
|
|
173
|
+
return modularityIncrease;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get communities of neighboring nodes
|
|
178
|
+
*/
|
|
179
|
+
function getNeighborCommunities(
|
|
180
|
+
graph: Graph,
|
|
181
|
+
nodeId: NodeId,
|
|
182
|
+
communities: Map<NodeId, number>,
|
|
183
|
+
): Set<number> {
|
|
184
|
+
const neighborCommunities = new Set<number>();
|
|
185
|
+
|
|
186
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
187
|
+
const community = communities.get(neighbor);
|
|
188
|
+
if (community !== undefined) {
|
|
189
|
+
neighborCommunities.add(community);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return neighborCommunities;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Calculate total modularity of the current community assignment
|
|
198
|
+
*/
|
|
199
|
+
function calculateModularity(
|
|
200
|
+
graph: Graph,
|
|
201
|
+
communities: Map<NodeId, number>,
|
|
202
|
+
resolution: number,
|
|
203
|
+
): number {
|
|
204
|
+
const totalEdgeWeight = getTotalEdgeWeight(graph);
|
|
205
|
+
if (totalEdgeWeight === 0) {
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let modularity = 0;
|
|
210
|
+
|
|
211
|
+
// For undirected graphs, we need to be careful not to double-count edges
|
|
212
|
+
const countedEdges = new Set<string>();
|
|
213
|
+
|
|
214
|
+
// Calculate modularity: Q = (1/2m) * Σ[A_ij - γ(k_i * k_j)/(2m)] * δ(c_i, c_j)
|
|
215
|
+
for (const nodeI of graph.nodes()) {
|
|
216
|
+
for (const nodeJ of graph.nodes()) {
|
|
217
|
+
// Skip if already counted this pair in undirected graph
|
|
218
|
+
if (!graph.isDirected) {
|
|
219
|
+
const nodeIStr = String(nodeI.id);
|
|
220
|
+
const nodeJStr = String(nodeJ.id);
|
|
221
|
+
const edgeKey = nodeIStr <= nodeJStr ? `${nodeIStr}-${nodeJStr}` : `${nodeJStr}-${nodeIStr}`;
|
|
222
|
+
if (countedEdges.has(edgeKey)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
countedEdges.add(edgeKey);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (communities.get(nodeI.id) === communities.get(nodeJ.id)) {
|
|
230
|
+
const edge = graph.getEdge(nodeI.id, nodeJ.id);
|
|
231
|
+
const reverseEdge = !graph.isDirected ? graph.getEdge(nodeJ.id, nodeI.id) : null;
|
|
232
|
+
|
|
233
|
+
let edgeWeight = 0;
|
|
234
|
+
if (edge) {
|
|
235
|
+
edgeWeight += edge.weight ?? 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (reverseEdge && nodeI.id !== nodeJ.id) {
|
|
239
|
+
edgeWeight += reverseEdge.weight ?? 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const degreeI = getNodeDegree(graph, nodeI.id);
|
|
243
|
+
const degreeJ = getNodeDegree(graph, nodeJ.id);
|
|
244
|
+
|
|
245
|
+
modularity += edgeWeight - ((resolution * degreeI * degreeJ) / (2 * totalEdgeWeight));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return modularity / (2 * totalEdgeWeight);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Extract final community structure
|
|
255
|
+
*/
|
|
256
|
+
function extractCommunities(communities: Map<NodeId, number>): NodeId[][] {
|
|
257
|
+
const communityMap = new Map<number, NodeId[]>();
|
|
258
|
+
|
|
259
|
+
for (const [nodeId, community] of communities) {
|
|
260
|
+
if (!communityMap.has(community)) {
|
|
261
|
+
communityMap.set(community, []);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const communityNodes = communityMap.get(community);
|
|
265
|
+
if (communityNodes) {
|
|
266
|
+
communityNodes.push(nodeId);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Array.from(communityMap.values());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Calculate total edge weight in the graph
|
|
275
|
+
*/
|
|
276
|
+
function getTotalEdgeWeight(graph: Graph): number {
|
|
277
|
+
let totalWeight = 0;
|
|
278
|
+
|
|
279
|
+
for (const edge of graph.edges()) {
|
|
280
|
+
totalWeight += edge.weight ?? 1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return totalWeight;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get the total degree (sum of edge weights) for a node
|
|
288
|
+
*/
|
|
289
|
+
function getNodeDegree(graph: Graph, nodeId: NodeId): number {
|
|
290
|
+
let degree = 0;
|
|
291
|
+
|
|
292
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
293
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
294
|
+
degree += edge?.weight ?? 1;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return degree;
|
|
298
|
+
}
|
|
@@ -26,7 +26,7 @@ export function connectedComponents(graph: Graph): NodeId[][] {
|
|
|
26
26
|
const unionFind = new UnionFind(nodes);
|
|
27
27
|
|
|
28
28
|
// Union connected nodes
|
|
29
|
-
for (const edge of graph.edges()) {
|
|
29
|
+
for (const edge of Array.from(graph.edges())) {
|
|
30
30
|
unionFind.union(edge.source, edge.target);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -44,7 +44,7 @@ export function connectedComponentsDFS(graph: Graph): NodeId[][] {
|
|
|
44
44
|
const visited = new Set<NodeId>();
|
|
45
45
|
const components: NodeId[][] = [];
|
|
46
46
|
|
|
47
|
-
for (const node of graph.nodes()) {
|
|
47
|
+
for (const node of Array.from(graph.nodes())) {
|
|
48
48
|
if (!visited.has(node.id)) {
|
|
49
49
|
const component: NodeId[] = [];
|
|
50
50
|
dfsComponent(graph, node.id, visited, component);
|
|
@@ -67,7 +67,7 @@ function dfsComponent(
|
|
|
67
67
|
visited.add(nodeId);
|
|
68
68
|
component.push(nodeId);
|
|
69
69
|
|
|
70
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
70
|
+
for (const neighbor of Array.from(graph.neighbors(nodeId))) {
|
|
71
71
|
if (!visited.has(neighbor)) {
|
|
72
72
|
dfsComponent(graph, neighbor, visited, component);
|
|
73
73
|
}
|
|
@@ -148,7 +148,7 @@ export function stronglyConnectedComponents(graph: Graph): NodeId[][] {
|
|
|
148
148
|
onStack.add(nodeId);
|
|
149
149
|
|
|
150
150
|
// Consider successors
|
|
151
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
151
|
+
for (const neighbor of Array.from(graph.neighbors(nodeId))) {
|
|
152
152
|
if (!indices.has(neighbor)) {
|
|
153
153
|
// Successor has not yet been visited; recurse on it
|
|
154
154
|
tarjanSCC(neighbor);
|
|
@@ -226,7 +226,7 @@ export function weaklyConnectedComponents(graph: Graph): NodeId[][] {
|
|
|
226
226
|
const unionFind = new UnionFind(nodes);
|
|
227
227
|
|
|
228
228
|
// Union nodes connected by edges (ignore direction)
|
|
229
|
-
for (const edge of graph.edges()) {
|
|
229
|
+
for (const edge of Array.from(graph.edges())) {
|
|
230
230
|
unionFind.union(edge.source, edge.target);
|
|
231
231
|
}
|
|
232
232
|
|
|
@@ -275,13 +275,13 @@ export function condensationGraph(graph: Graph): {
|
|
|
275
275
|
// Add edges between components
|
|
276
276
|
const addedEdges = new Set<string>();
|
|
277
277
|
|
|
278
|
-
for (const edge of graph.edges()) {
|
|
278
|
+
for (const edge of Array.from(graph.edges())) {
|
|
279
279
|
const sourceComponent = componentMap.get(edge.source);
|
|
280
280
|
const targetComponent = componentMap.get(edge.target);
|
|
281
281
|
|
|
282
282
|
if (sourceComponent !== undefined && targetComponent !== undefined &&
|
|
283
283
|
sourceComponent !== targetComponent) {
|
|
284
|
-
const edgeKey = `${sourceComponent}-${targetComponent}`;
|
|
284
|
+
const edgeKey = `${String(sourceComponent)}-${String(targetComponent)}`;
|
|
285
285
|
|
|
286
286
|
if (!addedEdges.has(edgeKey)) {
|
|
287
287
|
condensedGraph.addEdge(sourceComponent, targetComponent);
|
package/src/algorithms/index.ts
CHANGED
|
@@ -13,3 +13,24 @@ export * from "./centrality/index.js";
|
|
|
13
13
|
|
|
14
14
|
// Connected components algorithms
|
|
15
15
|
export * from "./components/index.js";
|
|
16
|
+
|
|
17
|
+
// Minimum spanning tree algorithms
|
|
18
|
+
export * from "./mst/index.js";
|
|
19
|
+
|
|
20
|
+
// Community detection algorithms
|
|
21
|
+
export * from "./community/index.js";
|
|
22
|
+
|
|
23
|
+
// Pathfinding algorithms
|
|
24
|
+
export * from "../pathfinding/index.js";
|
|
25
|
+
|
|
26
|
+
// Flow algorithms
|
|
27
|
+
export * from "../flow/index.js";
|
|
28
|
+
|
|
29
|
+
// Clustering algorithms
|
|
30
|
+
export * from "../clustering/index.js";
|
|
31
|
+
|
|
32
|
+
// Matching algorithms
|
|
33
|
+
export * from "./matching/index.js";
|
|
34
|
+
|
|
35
|
+
// Link prediction algorithms
|
|
36
|
+
export * from "../link-prediction/index.js";
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {NodeId} from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maximum Bipartite Matching implementation using Hopcroft-Karp algorithm
|
|
6
|
+
*
|
|
7
|
+
* Finds the maximum matching in a bipartite graph. A matching is a set of edges
|
|
8
|
+
* without common vertices. Maximum matching has the largest possible number of edges.
|
|
9
|
+
*
|
|
10
|
+
* Time complexity: O(√V × E)
|
|
11
|
+
* Space complexity: O(V)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface BipartiteMatchingResult {
|
|
15
|
+
matching: Map<NodeId, NodeId>; // Maps left nodes to right nodes
|
|
16
|
+
size: number; // Number of matched pairs
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface BipartiteMatchingOptions {
|
|
20
|
+
leftNodes?: Set<NodeId>; // Optional: specify left partition
|
|
21
|
+
rightNodes?: Set<NodeId>; // Optional: specify right partition
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find maximum matching in a bipartite graph using augmenting path algorithm
|
|
26
|
+
* (simplified implementation that's more reliable than Hopcroft-Karp for this context)
|
|
27
|
+
*/
|
|
28
|
+
export function maximumBipartiteMatching(
|
|
29
|
+
graph: Graph,
|
|
30
|
+
options: BipartiteMatchingOptions = {},
|
|
31
|
+
): BipartiteMatchingResult {
|
|
32
|
+
// If partitions not provided, try to infer them
|
|
33
|
+
let {leftNodes, rightNodes} = options;
|
|
34
|
+
|
|
35
|
+
if (!leftNodes || !rightNodes) {
|
|
36
|
+
const partition = bipartitePartition(graph);
|
|
37
|
+
if (!partition) {
|
|
38
|
+
throw new Error("Graph is not bipartite");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
leftNodes = partition.left;
|
|
42
|
+
rightNodes = partition.right;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const matching = new Map<NodeId, NodeId>();
|
|
46
|
+
const matchRight = new Map<NodeId, NodeId>(); // Right to left matching
|
|
47
|
+
|
|
48
|
+
// Find augmenting paths using DFS
|
|
49
|
+
const visited = new Set<NodeId>();
|
|
50
|
+
|
|
51
|
+
const dfs = (u: NodeId): boolean => {
|
|
52
|
+
const neighbors = Array.from(graph.neighbors(u));
|
|
53
|
+
|
|
54
|
+
for (const v of neighbors) {
|
|
55
|
+
if (!rightNodes.has(v) || visited.has(v)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
visited.add(v);
|
|
60
|
+
|
|
61
|
+
// If v is unmatched or we can find augmenting path from match of v
|
|
62
|
+
const matchedNode = matchRight.get(v);
|
|
63
|
+
if (!matchRight.has(v) || (matchedNode !== undefined && dfs(matchedNode))) {
|
|
64
|
+
matching.set(u, v);
|
|
65
|
+
matchRight.set(v, u);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Try to find augmenting path for each left node
|
|
73
|
+
let matchingSize = 0;
|
|
74
|
+
for (const u of leftNodes) {
|
|
75
|
+
visited.clear();
|
|
76
|
+
if (dfs(u)) {
|
|
77
|
+
matchingSize++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
matching,
|
|
83
|
+
size: matchingSize,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if graph is bipartite and return the partition
|
|
89
|
+
*/
|
|
90
|
+
export function bipartitePartition(graph: Graph): {left: Set<NodeId>, right: Set<NodeId>} | null {
|
|
91
|
+
const color = new Map<NodeId, boolean>();
|
|
92
|
+
const left = new Set<NodeId>();
|
|
93
|
+
const right = new Set<NodeId>();
|
|
94
|
+
|
|
95
|
+
const nodes = Array.from(graph.nodes());
|
|
96
|
+
|
|
97
|
+
for (const startNode of nodes) {
|
|
98
|
+
if (color.has(startNode.id)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// BFS to color the component
|
|
103
|
+
const queue = [startNode.id];
|
|
104
|
+
color.set(startNode.id, true);
|
|
105
|
+
left.add(startNode.id);
|
|
106
|
+
|
|
107
|
+
while (queue.length > 0) {
|
|
108
|
+
const u = queue.shift();
|
|
109
|
+
if (u === undefined) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const uColor = color.get(u);
|
|
114
|
+
if (uColor === undefined) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const neighbors = Array.from(graph.neighbors(u));
|
|
119
|
+
for (const v of neighbors) {
|
|
120
|
+
if (!color.has(v)) {
|
|
121
|
+
color.set(v, !uColor);
|
|
122
|
+
if (!uColor) {
|
|
123
|
+
left.add(v);
|
|
124
|
+
} else {
|
|
125
|
+
right.add(v);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
queue.push(v);
|
|
129
|
+
} else if (color.get(v) === uColor) {
|
|
130
|
+
// Same color as neighbor - not bipartite
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {left, right};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Simple greedy bipartite matching algorithm
|
|
142
|
+
* Useful for comparison or when Hopcroft-Karp is overkill
|
|
143
|
+
*/
|
|
144
|
+
export function greedyBipartiteMatching(
|
|
145
|
+
graph: Graph,
|
|
146
|
+
options: BipartiteMatchingOptions = {},
|
|
147
|
+
): BipartiteMatchingResult {
|
|
148
|
+
let {leftNodes, rightNodes} = options;
|
|
149
|
+
|
|
150
|
+
if (!leftNodes || !rightNodes) {
|
|
151
|
+
const partition = bipartitePartition(graph);
|
|
152
|
+
if (!partition) {
|
|
153
|
+
throw new Error("Graph is not bipartite");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
leftNodes = partition.left;
|
|
157
|
+
rightNodes = partition.right;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const matching = new Map<NodeId, NodeId>();
|
|
161
|
+
const matched = new Set<NodeId>();
|
|
162
|
+
|
|
163
|
+
for (const u of leftNodes) {
|
|
164
|
+
const neighbors = Array.from(graph.neighbors(u));
|
|
165
|
+
|
|
166
|
+
for (const v of neighbors) {
|
|
167
|
+
if (rightNodes.has(v) && !matched.has(v)) {
|
|
168
|
+
matching.set(u, v);
|
|
169
|
+
matched.add(v);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
matching,
|
|
177
|
+
size: matching.size,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph matching algorithms
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {BipartiteMatchingOptions, BipartiteMatchingResult} from "./bipartite.js";
|
|
6
|
+
export {bipartitePartition, greedyBipartiteMatching, maximumBipartiteMatching} from "./bipartite.js";
|
|
7
|
+
export type {IsomorphismOptions, IsomorphismResult} from "./isomorphism.js";
|
|
8
|
+
export {findAllIsomorphisms, isGraphIsomorphic} from "./isomorphism.js";
|