@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
|
@@ -111,7 +111,7 @@ export function pageRank(
|
|
|
111
111
|
let outDegree = 0;
|
|
112
112
|
let totalOutWeight = 0;
|
|
113
113
|
|
|
114
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
114
|
+
for (const neighbor of Array.from(graph.neighbors(nodeId))) {
|
|
115
115
|
outDegree++;
|
|
116
116
|
if (weight) {
|
|
117
117
|
const edge = graph.getEdge(nodeId, neighbor);
|
|
@@ -178,7 +178,7 @@ export function pageRank(
|
|
|
178
178
|
const nodeOutWeight = outWeights.get(nodeId) ?? 0;
|
|
179
179
|
|
|
180
180
|
if (nodeOutWeight > 0) {
|
|
181
|
-
for (const neighbor of graph.neighbors(nodeId)) {
|
|
181
|
+
for (const neighbor of Array.from(graph.neighbors(nodeId))) {
|
|
182
182
|
let edgeWeight = 1;
|
|
183
183
|
if (weight) {
|
|
184
184
|
const edge = graph.getEdge(nodeId, neighbor);
|
|
@@ -295,7 +295,7 @@ function normalizeRanks(ranks: Map<NodeId, number>): void {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
if (sum > 0) {
|
|
298
|
-
for (const [nodeId, rank] of ranks) {
|
|
298
|
+
for (const [nodeId, rank] of Array.from(ranks)) {
|
|
299
299
|
ranks.set(nodeId, rank / sum);
|
|
300
300
|
}
|
|
301
301
|
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {CommunityResult, ComponentResult, GirvanNewmanOptions, NodeId} from "../../types/index.js";
|
|
3
|
+
import {connectedComponents} from "../components/connected.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Girvan-Newman community detection algorithm
|
|
7
|
+
*
|
|
8
|
+
* Implements the Girvan-Newman method for community detection by iteratively
|
|
9
|
+
* removing edges with the highest betweenness centrality until the graph
|
|
10
|
+
* splits into disconnected components.
|
|
11
|
+
*
|
|
12
|
+
* This is a divisive hierarchical clustering algorithm that produces a
|
|
13
|
+
* dendrogram of community structures.
|
|
14
|
+
*
|
|
15
|
+
* References:
|
|
16
|
+
* - Girvan, M., & Newman, M. E. J. (2002). Community structure in social
|
|
17
|
+
* and biological networks. Proceedings of the National Academy of Sciences,
|
|
18
|
+
* 99(12), 7821-7826.
|
|
19
|
+
*
|
|
20
|
+
* @param graph - The input graph
|
|
21
|
+
* @param options - Algorithm options
|
|
22
|
+
* @returns Array of community detection results representing the dendrogram
|
|
23
|
+
*/
|
|
24
|
+
export function girvanNewman(
|
|
25
|
+
graph: Graph,
|
|
26
|
+
options: GirvanNewmanOptions = {},
|
|
27
|
+
): CommunityResult[] {
|
|
28
|
+
const {maxCommunities} = options;
|
|
29
|
+
const minCommunitySize = options.minCommunitySize ?? 1;
|
|
30
|
+
|
|
31
|
+
const dendrogram: CommunityResult[] = [];
|
|
32
|
+
const workingGraph = cloneGraph(graph);
|
|
33
|
+
|
|
34
|
+
// Initial state: one large component
|
|
35
|
+
let components = getConnectedComponentsResult(workingGraph);
|
|
36
|
+
dendrogram.push({
|
|
37
|
+
communities: components.components.filter((community) => community.length >= minCommunitySize),
|
|
38
|
+
modularity: calculateModularity(graph, components.componentMap),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
while (Array.from(workingGraph.edges()).length > 0) {
|
|
42
|
+
// Calculate edge betweenness centrality
|
|
43
|
+
const edgeBetweenness = calculateEdgeBetweenness(workingGraph);
|
|
44
|
+
|
|
45
|
+
if (edgeBetweenness.size === 0) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find edges with maximum betweenness
|
|
50
|
+
const maxBetweenness = Math.max(... edgeBetweenness.values());
|
|
51
|
+
const edgesToRemove: {source: NodeId, target: NodeId}[] = [];
|
|
52
|
+
|
|
53
|
+
for (const [edgeKey, centrality] of edgeBetweenness) {
|
|
54
|
+
if (Math.abs(centrality - maxBetweenness) < 1e-10) {
|
|
55
|
+
const [source, target] = edgeKey.split("|");
|
|
56
|
+
if (source && target) {
|
|
57
|
+
edgesToRemove.push({source, target});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Remove edges with highest betweenness
|
|
63
|
+
for (const {source, target} of edgesToRemove) {
|
|
64
|
+
workingGraph.removeEdge(source, target);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Find new connected components (communities)
|
|
68
|
+
components = getConnectedComponentsResult(workingGraph);
|
|
69
|
+
|
|
70
|
+
// Filter communities by minimum size
|
|
71
|
+
const validCommunities = components.components.filter(
|
|
72
|
+
(community) => community.length >= minCommunitySize,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Calculate modularity for the new community structure
|
|
76
|
+
const modularity = calculateModularity(graph, components.componentMap);
|
|
77
|
+
|
|
78
|
+
dendrogram.push({
|
|
79
|
+
communities: validCommunities,
|
|
80
|
+
modularity,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Stop if desired number of communities reached
|
|
84
|
+
if (maxCommunities && validCommunities.length >= maxCommunities) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stop if no more meaningful communities can be formed
|
|
89
|
+
if (validCommunities.length === workingGraph.nodeCount) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return dendrogram;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Calculate edge betweenness centrality for all edges in the graph
|
|
99
|
+
*
|
|
100
|
+
* Edge betweenness is the fraction of shortest paths that pass through the edge.
|
|
101
|
+
* We adapt node betweenness centrality calculation to work with edges.
|
|
102
|
+
*/
|
|
103
|
+
function calculateEdgeBetweenness(graph: Graph): Map<string, number> {
|
|
104
|
+
const edgeBetweenness = new Map<string, number>();
|
|
105
|
+
|
|
106
|
+
// Initialize all edges with 0 betweenness
|
|
107
|
+
for (const edge of graph.edges()) {
|
|
108
|
+
const edgeKey = getEdgeKey(edge.source, edge.target);
|
|
109
|
+
edgeBetweenness.set(edgeKey, 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// For each node as source, calculate shortest paths and accumulate edge betweenness
|
|
113
|
+
for (const sourceNode of graph.nodes()) {
|
|
114
|
+
const source = sourceNode.id;
|
|
115
|
+
|
|
116
|
+
// Run BFS to find all shortest paths from source
|
|
117
|
+
const {distances, predecessors, pathCounts} = findAllShortestPaths(graph, source);
|
|
118
|
+
|
|
119
|
+
// Calculate dependency for each node and accumulate edge betweenness
|
|
120
|
+
const dependency = new Map<NodeId, number>();
|
|
121
|
+
|
|
122
|
+
// Initialize dependency
|
|
123
|
+
for (const node of graph.nodes()) {
|
|
124
|
+
dependency.set(node.id, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Process nodes in order of decreasing distance
|
|
128
|
+
const sortedNodes = Array.from(distances.keys())
|
|
129
|
+
.filter((node) => {
|
|
130
|
+
const distance = distances.get(node);
|
|
131
|
+
return distance !== undefined && distance < Infinity;
|
|
132
|
+
})
|
|
133
|
+
.sort((a, b) => {
|
|
134
|
+
const distanceA = distances.get(a);
|
|
135
|
+
const distanceB = distances.get(b);
|
|
136
|
+
if (distanceA === undefined || distanceB === undefined) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return distanceB - distanceA;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const node of sortedNodes) {
|
|
144
|
+
if (node === source) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const nodeDependency = dependency.get(node);
|
|
149
|
+
const nodePathCount = pathCounts.get(node);
|
|
150
|
+
|
|
151
|
+
if (nodeDependency === undefined || nodePathCount === undefined) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Distribute dependency to predecessors
|
|
156
|
+
const nodePredecessors = predecessors.get(node) ?? [];
|
|
157
|
+
for (const predecessor of nodePredecessors) {
|
|
158
|
+
const predPathCount = pathCounts.get(predecessor);
|
|
159
|
+
if (predPathCount === undefined) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const edgeDependency = (predPathCount / nodePathCount) * (1 + nodeDependency);
|
|
164
|
+
|
|
165
|
+
// Update predecessor dependency
|
|
166
|
+
const currentDependency = dependency.get(predecessor);
|
|
167
|
+
if (currentDependency !== undefined) {
|
|
168
|
+
dependency.set(predecessor, currentDependency + edgeDependency);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Update edge betweenness
|
|
172
|
+
const edgeKey = getEdgeKey(predecessor, node);
|
|
173
|
+
const currentBetweenness = edgeBetweenness.get(edgeKey) ?? 0;
|
|
174
|
+
edgeBetweenness.set(edgeKey, currentBetweenness + edgeDependency);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Normalize: divide by 2 for undirected graphs (each edge counted twice)
|
|
180
|
+
if (!graph.isDirected) {
|
|
181
|
+
for (const [edgeKey, betweenness] of edgeBetweenness) {
|
|
182
|
+
edgeBetweenness.set(edgeKey, betweenness / 2);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return edgeBetweenness;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Find all shortest paths from a source node using BFS
|
|
191
|
+
*/
|
|
192
|
+
function findAllShortestPaths(graph: Graph, source: NodeId): {
|
|
193
|
+
distances: Map<NodeId, number>;
|
|
194
|
+
predecessors: Map<NodeId, NodeId[]>;
|
|
195
|
+
pathCounts: Map<NodeId, number>;
|
|
196
|
+
} {
|
|
197
|
+
const distances = new Map<NodeId, number>();
|
|
198
|
+
const predecessors = new Map<NodeId, NodeId[]>();
|
|
199
|
+
const pathCounts = new Map<NodeId, number>();
|
|
200
|
+
const queue: NodeId[] = [];
|
|
201
|
+
|
|
202
|
+
// Initialize
|
|
203
|
+
for (const node of graph.nodes()) {
|
|
204
|
+
distances.set(node.id, Infinity);
|
|
205
|
+
predecessors.set(node.id, []);
|
|
206
|
+
pathCounts.set(node.id, 0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
distances.set(source, 0);
|
|
210
|
+
pathCounts.set(source, 1);
|
|
211
|
+
queue.push(source);
|
|
212
|
+
|
|
213
|
+
// BFS
|
|
214
|
+
while (queue.length > 0) {
|
|
215
|
+
const current = queue.shift();
|
|
216
|
+
if (current === undefined) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const currentDistance = distances.get(current);
|
|
221
|
+
if (currentDistance === undefined) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
for (const neighbor of graph.neighbors(current)) {
|
|
226
|
+
const edge = graph.getEdge(current, neighbor);
|
|
227
|
+
const edgeWeight = edge?.weight ?? 1;
|
|
228
|
+
const newDistance = currentDistance + edgeWeight;
|
|
229
|
+
const neighborDistance = distances.get(neighbor);
|
|
230
|
+
if (neighborDistance === undefined) {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (newDistance < neighborDistance) {
|
|
235
|
+
// Found shorter path
|
|
236
|
+
distances.set(neighbor, newDistance);
|
|
237
|
+
predecessors.set(neighbor, [current]);
|
|
238
|
+
const currentPathCount = pathCounts.get(current);
|
|
239
|
+
if (currentPathCount !== undefined) {
|
|
240
|
+
pathCounts.set(neighbor, currentPathCount);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
queue.push(neighbor);
|
|
244
|
+
} else if (Math.abs(newDistance - neighborDistance) < 1e-10) {
|
|
245
|
+
// Found alternative shortest path
|
|
246
|
+
const neighborPredecessors = predecessors.get(neighbor);
|
|
247
|
+
const neighborPathCount = pathCounts.get(neighbor);
|
|
248
|
+
const currentPathCount = pathCounts.get(current);
|
|
249
|
+
|
|
250
|
+
if (neighborPredecessors && neighborPathCount !== undefined && currentPathCount !== undefined) {
|
|
251
|
+
neighborPredecessors.push(current);
|
|
252
|
+
pathCounts.set(neighbor, neighborPathCount + currentPathCount);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {distances, predecessors, pathCounts};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate a consistent edge key for undirected graphs
|
|
263
|
+
*/
|
|
264
|
+
function getEdgeKey(source: NodeId, target: NodeId): string {
|
|
265
|
+
// For undirected graphs, ensure consistent ordering
|
|
266
|
+
const sourceStr = String(source);
|
|
267
|
+
const targetStr = String(target);
|
|
268
|
+
|
|
269
|
+
if (sourceStr <= targetStr) {
|
|
270
|
+
return `${sourceStr}|${targetStr}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return `${targetStr}|${sourceStr}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Calculate modularity for a given community structure
|
|
278
|
+
*/
|
|
279
|
+
function calculateModularity(
|
|
280
|
+
graph: Graph,
|
|
281
|
+
communityMap: Map<NodeId, number>,
|
|
282
|
+
): number {
|
|
283
|
+
const totalEdgeWeight = getTotalEdgeWeight(graph);
|
|
284
|
+
if (totalEdgeWeight === 0) {
|
|
285
|
+
return 0;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let modularity = 0;
|
|
289
|
+
|
|
290
|
+
// Calculate modularity: Q = (1/2m) * Σ[A_ij - (k_i * k_j)/(2m)] * δ(c_i, c_j)
|
|
291
|
+
for (const nodeI of graph.nodes()) {
|
|
292
|
+
for (const nodeJ of graph.nodes()) {
|
|
293
|
+
if (communityMap.get(nodeI.id) === communityMap.get(nodeJ.id)) {
|
|
294
|
+
const edge = graph.getEdge(nodeI.id, nodeJ.id);
|
|
295
|
+
const edgeWeight = edge?.weight ?? 0;
|
|
296
|
+
|
|
297
|
+
const degreeI = getNodeDegree(graph, nodeI.id);
|
|
298
|
+
const degreeJ = getNodeDegree(graph, nodeJ.id);
|
|
299
|
+
|
|
300
|
+
modularity += edgeWeight - ((degreeI * degreeJ) / (2 * totalEdgeWeight));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return modularity / (2 * totalEdgeWeight);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Calculate total edge weight in the graph
|
|
310
|
+
*/
|
|
311
|
+
function getTotalEdgeWeight(graph: Graph): number {
|
|
312
|
+
let totalWeight = 0;
|
|
313
|
+
|
|
314
|
+
for (const edge of graph.edges()) {
|
|
315
|
+
totalWeight += edge.weight ?? 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return totalWeight;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get the total degree (sum of edge weights) for a node
|
|
323
|
+
*/
|
|
324
|
+
function getNodeDegree(graph: Graph, nodeId: NodeId): number {
|
|
325
|
+
let degree = 0;
|
|
326
|
+
|
|
327
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
328
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
329
|
+
degree += edge?.weight ?? 1;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return degree;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Convert connected components result to ComponentResult format
|
|
337
|
+
*/
|
|
338
|
+
function getConnectedComponentsResult(graph: Graph): ComponentResult {
|
|
339
|
+
const components = connectedComponents(graph);
|
|
340
|
+
const componentMap = new Map<NodeId, number>();
|
|
341
|
+
|
|
342
|
+
components.forEach((component, index) => {
|
|
343
|
+
component.forEach((nodeId) => {
|
|
344
|
+
componentMap.set(nodeId, index);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
return {components, componentMap};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Create a deep clone of the graph
|
|
353
|
+
*/
|
|
354
|
+
function cloneGraph(graph: Graph): Graph {
|
|
355
|
+
const clone = new Graph({
|
|
356
|
+
directed: graph.isDirected,
|
|
357
|
+
allowSelfLoops: true,
|
|
358
|
+
allowParallelEdges: false,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Add all nodes
|
|
362
|
+
for (const node of graph.nodes()) {
|
|
363
|
+
clone.addNode(node.id, node.data);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Add all edges
|
|
367
|
+
for (const edge of graph.edges()) {
|
|
368
|
+
clone.addEdge(edge.source, edge.target, edge.weight, edge.data);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return clone;
|
|
372
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Community Detection Algorithms
|
|
3
|
+
*
|
|
4
|
+
* This module provides algorithms for detecting community structure in graphs.
|
|
5
|
+
* Communities are groups of nodes that are more densely connected internally
|
|
6
|
+
* than with the rest of the network.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export {girvanNewman} from "./girvan-newman.js";
|
|
10
|
+
export {labelPropagation, labelPropagationAsync, labelPropagationSemiSupervised} from "./label-propagation.js";
|
|
11
|
+
export {leiden} from "./leiden.js";
|
|
12
|
+
export {louvain} from "./louvain.js";
|
|
13
|
+
|