@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,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label Propagation Algorithm for Community Detection
|
|
3
|
+
*
|
|
4
|
+
* A fast, near-linear time algorithm that detects communities by
|
|
5
|
+
* propagating labels through the network. Each node adopts the label
|
|
6
|
+
* that most of its neighbors have.
|
|
7
|
+
*
|
|
8
|
+
* Reference: Raghavan et al. (2007) "Near linear time algorithm to
|
|
9
|
+
* detect community structures in large-scale networks"
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface LabelPropagationOptions {
|
|
13
|
+
maxIterations?: number;
|
|
14
|
+
randomSeed?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LabelPropagationResult {
|
|
18
|
+
communities: Map<string, number>;
|
|
19
|
+
iterations: number;
|
|
20
|
+
converged: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Label Propagation Algorithm
|
|
25
|
+
* Each node adopts the most frequent label among its neighbors
|
|
26
|
+
*
|
|
27
|
+
* @param graph - Undirected graph (can be weighted)
|
|
28
|
+
* @param options - Algorithm options
|
|
29
|
+
* @returns Community assignments
|
|
30
|
+
*
|
|
31
|
+
* Time Complexity: O(m) per iteration, typically O(km) for k iterations
|
|
32
|
+
* Space Complexity: O(n)
|
|
33
|
+
*/
|
|
34
|
+
export function labelPropagation(
|
|
35
|
+
graph: Map<string, Map<string, number>>,
|
|
36
|
+
options: LabelPropagationOptions = {},
|
|
37
|
+
): LabelPropagationResult {
|
|
38
|
+
const {
|
|
39
|
+
maxIterations = 100,
|
|
40
|
+
randomSeed = 42,
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
if (graph.size === 0) {
|
|
44
|
+
return {
|
|
45
|
+
communities: new Map(),
|
|
46
|
+
iterations: 0,
|
|
47
|
+
converged: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize random number generator
|
|
52
|
+
let seed = randomSeed;
|
|
53
|
+
const random = (): number => {
|
|
54
|
+
seed = ((seed * 1664525) + 1013904223) % 2147483647;
|
|
55
|
+
return seed / 2147483647;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Initialize labels - each node gets its own label
|
|
59
|
+
const labels = new Map<string, number>();
|
|
60
|
+
const nodes = Array.from(graph.keys());
|
|
61
|
+
nodes.forEach((node, i) => labels.set(node, i));
|
|
62
|
+
|
|
63
|
+
let iterations = 0;
|
|
64
|
+
let converged = false;
|
|
65
|
+
|
|
66
|
+
// Main loop
|
|
67
|
+
while (iterations < maxIterations && !converged) {
|
|
68
|
+
iterations++;
|
|
69
|
+
converged = true;
|
|
70
|
+
|
|
71
|
+
// Create random order for node updates
|
|
72
|
+
const nodeOrder = [... nodes];
|
|
73
|
+
shuffle(nodeOrder, random);
|
|
74
|
+
|
|
75
|
+
// Update each node's label
|
|
76
|
+
for (const node of nodeOrder) {
|
|
77
|
+
const neighbors = graph.get(node);
|
|
78
|
+
if (!neighbors || neighbors.size === 0) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Count label frequencies among neighbors
|
|
83
|
+
const labelCounts = new Map<number, number>();
|
|
84
|
+
let maxCount = 0;
|
|
85
|
+
const candidateLabels: number[] = [];
|
|
86
|
+
|
|
87
|
+
for (const [neighbor, weight] of neighbors) {
|
|
88
|
+
const neighborLabel = labels.get(neighbor);
|
|
89
|
+
if (neighborLabel === undefined) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
|
|
94
|
+
labelCounts.set(neighborLabel, count);
|
|
95
|
+
|
|
96
|
+
if (count > maxCount) {
|
|
97
|
+
maxCount = count;
|
|
98
|
+
candidateLabels.length = 0;
|
|
99
|
+
candidateLabels.push(neighborLabel);
|
|
100
|
+
} else if (count === maxCount) {
|
|
101
|
+
candidateLabels.push(neighborLabel);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Choose label (break ties randomly)
|
|
106
|
+
const currentLabel = labels.get(node);
|
|
107
|
+
if (currentLabel === undefined) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let newLabel = currentLabel;
|
|
112
|
+
|
|
113
|
+
if (candidateLabels.length > 0) {
|
|
114
|
+
// Include current label if it has max count
|
|
115
|
+
if (labelCounts.get(currentLabel) === maxCount) {
|
|
116
|
+
candidateLabels.push(currentLabel);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Random selection from candidates
|
|
120
|
+
const index = Math.floor(random() * candidateLabels.length);
|
|
121
|
+
const selectedLabel = candidateLabels[index];
|
|
122
|
+
if (selectedLabel !== undefined) {
|
|
123
|
+
newLabel = selectedLabel;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Update label if changed
|
|
128
|
+
if (newLabel !== currentLabel) {
|
|
129
|
+
labels.set(node, newLabel);
|
|
130
|
+
converged = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Renumber communities consecutively
|
|
136
|
+
const uniqueLabels = new Set(labels.values());
|
|
137
|
+
const labelMap = new Map<number, number>();
|
|
138
|
+
let communityId = 0;
|
|
139
|
+
|
|
140
|
+
for (const label of uniqueLabels) {
|
|
141
|
+
labelMap.set(label, communityId++);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const communities = new Map<string, number>();
|
|
145
|
+
for (const [node, label] of labels) {
|
|
146
|
+
const mappedLabel = labelMap.get(label);
|
|
147
|
+
if (mappedLabel !== undefined) {
|
|
148
|
+
communities.set(node, mappedLabel);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
communities,
|
|
154
|
+
iterations,
|
|
155
|
+
converged,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Asynchronous Label Propagation
|
|
161
|
+
* Updates all nodes simultaneously (can lead to oscillations)
|
|
162
|
+
*/
|
|
163
|
+
export function labelPropagationAsync(
|
|
164
|
+
graph: Map<string, Map<string, number>>,
|
|
165
|
+
options: LabelPropagationOptions = {},
|
|
166
|
+
): LabelPropagationResult {
|
|
167
|
+
const {
|
|
168
|
+
maxIterations = 100,
|
|
169
|
+
} = options;
|
|
170
|
+
|
|
171
|
+
if (graph.size === 0) {
|
|
172
|
+
return {
|
|
173
|
+
communities: new Map(),
|
|
174
|
+
iterations: 0,
|
|
175
|
+
converged: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Initialize labels
|
|
180
|
+
const labels = new Map<string, number>();
|
|
181
|
+
const nodes = Array.from(graph.keys());
|
|
182
|
+
nodes.forEach((node, i) => labels.set(node, i));
|
|
183
|
+
|
|
184
|
+
let iterations = 0;
|
|
185
|
+
let converged = false;
|
|
186
|
+
|
|
187
|
+
// Main loop
|
|
188
|
+
while (iterations < maxIterations && !converged) {
|
|
189
|
+
iterations++;
|
|
190
|
+
converged = true;
|
|
191
|
+
|
|
192
|
+
// Store new labels
|
|
193
|
+
const newLabels = new Map<string, number>();
|
|
194
|
+
|
|
195
|
+
// Update all nodes simultaneously
|
|
196
|
+
for (const node of nodes) {
|
|
197
|
+
const neighbors = graph.get(node);
|
|
198
|
+
if (!neighbors || neighbors.size === 0) {
|
|
199
|
+
const nodeLabel = labels.get(node);
|
|
200
|
+
if (nodeLabel !== undefined) {
|
|
201
|
+
newLabels.set(node, nodeLabel);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Count label frequencies
|
|
208
|
+
const labelCounts = new Map<number, number>();
|
|
209
|
+
let maxCount = 0;
|
|
210
|
+
const nodeLabel = labels.get(node);
|
|
211
|
+
if (nodeLabel === undefined) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let maxLabel = nodeLabel;
|
|
216
|
+
|
|
217
|
+
for (const [neighbor, weight] of neighbors) {
|
|
218
|
+
const neighborLabel = labels.get(neighbor);
|
|
219
|
+
if (neighborLabel === undefined) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
|
|
224
|
+
labelCounts.set(neighborLabel, count);
|
|
225
|
+
|
|
226
|
+
if (count > maxCount || (count === maxCount && neighborLabel < maxLabel)) {
|
|
227
|
+
maxCount = count;
|
|
228
|
+
maxLabel = neighborLabel;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
newLabels.set(node, maxLabel);
|
|
233
|
+
|
|
234
|
+
if (maxLabel !== labels.get(node)) {
|
|
235
|
+
converged = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Apply new labels
|
|
240
|
+
for (const [node, label] of newLabels) {
|
|
241
|
+
labels.set(node, label);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Renumber communities
|
|
246
|
+
const uniqueLabels = new Set(labels.values());
|
|
247
|
+
const labelMap = new Map<number, number>();
|
|
248
|
+
let communityId = 0;
|
|
249
|
+
|
|
250
|
+
for (const label of uniqueLabels) {
|
|
251
|
+
labelMap.set(label, communityId++);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const communities = new Map<string, number>();
|
|
255
|
+
for (const [node, label] of labels) {
|
|
256
|
+
const mappedLabel = labelMap.get(label);
|
|
257
|
+
if (mappedLabel !== undefined) {
|
|
258
|
+
communities.set(node, mappedLabel);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
communities,
|
|
264
|
+
iterations,
|
|
265
|
+
converged,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Semi-supervised Label Propagation
|
|
271
|
+
* Some nodes have fixed labels that don't change
|
|
272
|
+
*/
|
|
273
|
+
export function labelPropagationSemiSupervised(
|
|
274
|
+
graph: Map<string, Map<string, number>>,
|
|
275
|
+
seedLabels: Map<string, number>,
|
|
276
|
+
options: LabelPropagationOptions = {},
|
|
277
|
+
): LabelPropagationResult {
|
|
278
|
+
const {
|
|
279
|
+
maxIterations = 100,
|
|
280
|
+
randomSeed = 42,
|
|
281
|
+
} = options;
|
|
282
|
+
|
|
283
|
+
// Initialize random number generator
|
|
284
|
+
let seed = randomSeed;
|
|
285
|
+
const random = (): number => {
|
|
286
|
+
seed = ((seed * 1664525) + 1013904223) % 2147483647;
|
|
287
|
+
return seed / 2147483647;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Initialize labels
|
|
291
|
+
const labels = new Map<string, number>();
|
|
292
|
+
const nodes = Array.from(graph.keys());
|
|
293
|
+
let labelCounter = Math.max(... Array.from(seedLabels.values())) + 1;
|
|
294
|
+
|
|
295
|
+
for (const node of nodes) {
|
|
296
|
+
if (seedLabels.has(node)) {
|
|
297
|
+
const seedLabel = seedLabels.get(node);
|
|
298
|
+
if (seedLabel !== undefined) {
|
|
299
|
+
labels.set(node, seedLabel);
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
labels.set(node, labelCounter++);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let iterations = 0;
|
|
307
|
+
let converged = false;
|
|
308
|
+
|
|
309
|
+
// Main loop
|
|
310
|
+
while (iterations < maxIterations && !converged) {
|
|
311
|
+
iterations++;
|
|
312
|
+
converged = true;
|
|
313
|
+
|
|
314
|
+
// Create random order
|
|
315
|
+
const nodeOrder = nodes.filter((n) => !seedLabels.has(n));
|
|
316
|
+
shuffle(nodeOrder, random);
|
|
317
|
+
|
|
318
|
+
// Update non-seed nodes
|
|
319
|
+
for (const node of nodeOrder) {
|
|
320
|
+
const neighbors = graph.get(node);
|
|
321
|
+
if (!neighbors || neighbors.size === 0) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Count label frequencies
|
|
326
|
+
const labelCounts = new Map<number, number>();
|
|
327
|
+
let maxCount = 0;
|
|
328
|
+
const candidateLabels: number[] = [];
|
|
329
|
+
|
|
330
|
+
for (const [neighbor, weight] of neighbors) {
|
|
331
|
+
const neighborLabel = labels.get(neighbor);
|
|
332
|
+
if (neighborLabel === undefined) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const count = (labelCounts.get(neighborLabel) ?? 0) + weight;
|
|
337
|
+
labelCounts.set(neighborLabel, count);
|
|
338
|
+
|
|
339
|
+
if (count > maxCount) {
|
|
340
|
+
maxCount = count;
|
|
341
|
+
candidateLabels.length = 0;
|
|
342
|
+
candidateLabels.push(neighborLabel);
|
|
343
|
+
} else if (count === maxCount) {
|
|
344
|
+
candidateLabels.push(neighborLabel);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Choose label
|
|
349
|
+
const currentLabel = labels.get(node);
|
|
350
|
+
if (currentLabel === undefined) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let newLabel = currentLabel;
|
|
355
|
+
|
|
356
|
+
if (candidateLabels.length > 0) {
|
|
357
|
+
const index = Math.floor(random() * candidateLabels.length);
|
|
358
|
+
const selectedLabel = candidateLabels[index];
|
|
359
|
+
if (selectedLabel !== undefined) {
|
|
360
|
+
newLabel = selectedLabel;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (newLabel !== currentLabel) {
|
|
365
|
+
labels.set(node, newLabel);
|
|
366
|
+
converged = false;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
communities: labels,
|
|
373
|
+
iterations,
|
|
374
|
+
converged,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Fisher-Yates shuffle
|
|
380
|
+
*/
|
|
381
|
+
function shuffle(array: unknown[], random: () => number): void {
|
|
382
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
383
|
+
const j = Math.floor(random() * (i + 1));
|
|
384
|
+
const temp = array[i];
|
|
385
|
+
const swapItem = array[j];
|
|
386
|
+
if (temp !== undefined && swapItem !== undefined) {
|
|
387
|
+
array[i] = swapItem;
|
|
388
|
+
array[j] = temp;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|