@graphty/algorithms 1.1.0 → 1.3.1
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 +820 -235
- package/dist/algorithms.d.ts +42 -2
- package/dist/algorithms.js +11099 -2
- package/dist/algorithms.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +4 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +124 -170
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +26 -1
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +56 -149
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +3 -3
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +39 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js +286 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts +80 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js +294 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
- package/dist/src/algorithms/centrality/index.d.ts +2 -0
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/index.js +1 -0
- package/dist/src/algorithms/centrality/index.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +17 -0
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +45 -0
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +47 -54
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/index.d.ts +2 -0
- package/dist/src/algorithms/community/index.d.ts.map +1 -1
- package/dist/src/algorithms/community/index.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +13 -3
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +50 -35
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/community/leiden.d.ts +3 -2
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
- package/dist/src/algorithms/community/leiden.js +34 -39
- package/dist/src/algorithms/community/leiden.js.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.d.ts +112 -0
- package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
- package/dist/src/algorithms/community/louvain-optimized.js +303 -0
- package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
- package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
- package/dist/src/algorithms/community/louvain.js +17 -76
- package/dist/src/algorithms/community/louvain.js.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.d.ts +59 -0
- package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
- package/dist/src/algorithms/community/modularity-utils.js +112 -0
- package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
- package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
- package/dist/src/algorithms/matching/bipartite.js +8 -40
- package/dist/src/algorithms/matching/bipartite.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +1 -12
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +37 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +193 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts +5 -2
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +15 -14
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +29 -0
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-unified.js +333 -0
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts +54 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.js +368 -0
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +5 -182
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/benchmark-all-algorithms.d.ts +23 -0
- package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
- package/dist/src/benchmark-all-algorithms.js +462 -0
- package/dist/src/benchmark-all-algorithms.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +16 -15
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +209 -223
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/index.d.ts +4 -2
- package/dist/src/clustering/index.d.ts.map +1 -1
- package/dist/src/clustering/index.js +2 -2
- package/dist/src/clustering/index.js.map +1 -1
- package/dist/src/clustering/k-core.d.ts +16 -15
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +57 -21
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/spectral.d.ts +8 -0
- package/dist/src/clustering/spectral.d.ts.map +1 -1
- package/dist/src/clustering/spectral.js +158 -35
- package/dist/src/clustering/spectral.js.map +1 -1
- package/dist/src/core/graph.js +4 -4
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/flow/ford-fulkerson.d.ts +17 -11
- package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
- package/dist/src/flow/ford-fulkerson.js +141 -217
- package/dist/src/flow/ford-fulkerson.js.map +1 -1
- package/dist/src/flow/min-cut.d.ts +6 -5
- package/dist/src/flow/min-cut.d.ts.map +1 -1
- package/dist/src/flow/min-cut.js +17 -8
- package/dist/src/flow/min-cut.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
- package/dist/src/link-prediction/adamic-adar.js +17 -26
- package/dist/src/link-prediction/adamic-adar.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +7 -11
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/optimized/bit-packed.d.ts +143 -0
- package/dist/src/optimized/bit-packed.d.ts.map +1 -0
- package/dist/src/optimized/bit-packed.js +292 -0
- package/dist/src/optimized/bit-packed.js.map +1 -0
- package/dist/src/optimized/csr-graph.d.ts +86 -0
- package/dist/src/optimized/csr-graph.d.ts.map +1 -0
- package/dist/src/optimized/csr-graph.js +341 -0
- package/dist/src/optimized/csr-graph.js.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts +74 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.js +223 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
- package/dist/src/optimized/graph-adapter.d.ts +62 -0
- package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
- package/dist/src/optimized/graph-adapter.js +146 -0
- package/dist/src/optimized/graph-adapter.js.map +1 -0
- package/dist/src/optimized/index.d.ts +14 -0
- package/dist/src/optimized/index.d.ts.map +1 -0
- package/dist/src/optimized/index.js +28 -0
- package/dist/src/optimized/index.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 +403 -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 +316 -0
- package/dist/src/research/sync.js.map +1 -0
- package/dist/src/research/terahac.d.ts +65 -0
- package/dist/src/research/terahac.d.ts.map +1 -0
- package/dist/src/research/terahac.js +379 -0
- package/dist/src/research/terahac.js.map +1 -0
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/utils/algorithm-utilities.d.ts +2 -0
- package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
- package/dist/src/utils/algorithm-utilities.js +2 -0
- package/dist/src/utils/algorithm-utilities.js.map +1 -0
- package/dist/src/utils/graph-converters.d.ts +79 -0
- package/dist/src/utils/graph-converters.d.ts.map +1 -0
- package/dist/src/utils/graph-converters.js +196 -0
- package/dist/src/utils/graph-converters.js.map +1 -0
- package/dist/src/utils/graph-utilities.d.ts +62 -0
- package/dist/src/utils/graph-utilities.d.ts.map +1 -0
- package/dist/src/utils/graph-utilities.js +148 -0
- package/dist/src/utils/graph-utilities.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/math-utilities.d.ts +52 -0
- package/dist/src/utils/math-utilities.d.ts.map +1 -0
- package/dist/src/utils/math-utilities.js +123 -0
- package/dist/src/utils/math-utilities.js.map +1 -0
- package/dist/src/utils/matrix-utilities.d.ts +2 -0
- package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
- package/dist/src/utils/matrix-utilities.js +2 -0
- package/dist/src/utils/matrix-utilities.js.map +1 -0
- package/dist/src/utils/optimization-helpers.d.ts +33 -0
- package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
- package/dist/src/utils/optimization-helpers.js +47 -0
- package/dist/src/utils/optimization-helpers.js.map +1 -0
- package/package.json +28 -4
- package/src/algorithms/centrality/betweenness.ts +167 -199
- package/src/algorithms/centrality/closeness.ts +73 -191
- package/src/algorithms/centrality/degree.ts +3 -3
- package/src/algorithms/centrality/delta-pagerank-simple.ts +366 -0
- package/src/algorithms/centrality/delta-pagerank.ts +435 -0
- package/src/algorithms/centrality/index.ts +2 -0
- package/src/algorithms/centrality/pagerank.ts +66 -0
- package/src/algorithms/community/girvan-newman.ts +52 -59
- package/src/algorithms/community/index.ts +2 -0
- package/src/algorithms/community/label-propagation.ts +64 -35
- package/src/algorithms/community/leiden.ts +40 -41
- package/src/algorithms/community/louvain-optimized.ts +445 -0
- package/src/algorithms/community/louvain.ts +24 -104
- package/src/algorithms/community/modularity-utils.ts +146 -0
- package/src/algorithms/matching/bipartite.ts +8 -44
- package/src/algorithms/shortest-path/bellman-ford.ts +1 -14
- package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
- package/src/algorithms/shortest-path/dijkstra.ts +17 -15
- package/src/algorithms/traversal/bfs-unified.ts +433 -0
- package/src/algorithms/traversal/bfs-variants.ts +496 -0
- package/src/algorithms/traversal/bfs.ts +10 -228
- package/src/benchmark-all-algorithms.ts +548 -0
- package/src/clustering/hierarchical.ts +241 -251
- package/src/clustering/index.ts +4 -2
- package/src/clustering/k-core.ts +87 -39
- package/src/clustering/spectral.ts +185 -34
- package/src/core/graph.ts +4 -4
- package/src/flow/ford-fulkerson.ts +186 -254
- package/src/flow/min-cut.ts +21 -11
- package/src/index.ts +9 -0
- package/src/link-prediction/adamic-adar.ts +17 -30
- package/src/link-prediction/common-neighbors.ts +7 -16
- package/src/optimized/bit-packed.ts +340 -0
- package/src/optimized/csr-graph.ts +455 -0
- package/src/optimized/direction-optimized-bfs.ts +291 -0
- package/src/optimized/graph-adapter.ts +200 -0
- package/src/optimized/index.ts +36 -0
- package/src/research/grsbm.ts +580 -0
- package/src/research/index.ts +17 -0
- package/src/research/sync.ts +440 -0
- package/src/research/terahac.ts +514 -0
- package/src/types/index.ts +8 -0
- package/src/utils/algorithm-utilities.ts +2 -0
- package/src/utils/graph-converters.ts +241 -0
- package/src/utils/graph-utilities.ts +199 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/math-utilities.ts +140 -0
- package/src/utils/matrix-utilities.ts +2 -0
- package/src/utils/optimization-helpers.ts +81 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {Graph} from "../../core/graph.js";
|
|
2
2
|
import type {NodeId} from "../../types/index.js";
|
|
3
|
+
import {bfsDistancesOnly, bfsWeightedDistances} from "../traversal/bfs-variants.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Closeness centrality implementation
|
|
@@ -25,39 +26,21 @@ export interface ClosenessCentralityOptions {
|
|
|
25
26
|
* Consider only nodes within this distance (default: undefined = all nodes)
|
|
26
27
|
*/
|
|
27
28
|
cutoff?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Whether to use optimized BFS implementation for large graphs
|
|
31
|
+
*/
|
|
32
|
+
optimized?: boolean;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
|
-
* Calculate closeness centrality
|
|
32
|
-
*/
|
|
33
|
-
export function closenessCentrality(
|
|
34
|
-
graph: Graph,
|
|
35
|
-
options: ClosenessCentralityOptions = {},
|
|
36
|
-
): Record<string, number> {
|
|
37
|
-
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
38
|
-
const centrality: Record<string, number> = {};
|
|
39
|
-
|
|
40
|
-
for (const sourceNode of nodes) {
|
|
41
|
-
centrality[String(sourceNode)] = nodeClosenessCentrality(graph, sourceNode, options);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return centrality;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Calculate closeness centrality for a specific node
|
|
36
|
+
* Calculate closeness centrality from distances
|
|
49
37
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
function calculateClosenessFromDistances(
|
|
39
|
+
distances: Map<NodeId, number>,
|
|
40
|
+
sourceNode: NodeId,
|
|
41
|
+
totalNodes: number,
|
|
42
|
+
options: ClosenessCentralityOptions,
|
|
54
43
|
): number {
|
|
55
|
-
if (!graph.hasNode(node)) {
|
|
56
|
-
throw new Error(`Node ${String(node)} not found in graph`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const distances = singleSourceShortestPathLengths(graph, node, options.cutoff);
|
|
60
|
-
|
|
61
44
|
if (distances.size <= 1) {
|
|
62
45
|
return 0; // No other nodes reachable
|
|
63
46
|
}
|
|
@@ -66,18 +49,23 @@ export function nodeClosenessCentrality(
|
|
|
66
49
|
|
|
67
50
|
if (options.harmonic) {
|
|
68
51
|
// Harmonic centrality: sum of reciprocals of distances
|
|
69
|
-
for (const [targetNode, distance] of
|
|
70
|
-
if (targetNode !==
|
|
52
|
+
for (const [targetNode, distance] of distances) {
|
|
53
|
+
if (targetNode !== sourceNode && distance > 0 && distance < Infinity) {
|
|
71
54
|
centrality += 1 / distance;
|
|
72
55
|
}
|
|
73
56
|
}
|
|
57
|
+
|
|
58
|
+
// Normalization for harmonic centrality
|
|
59
|
+
if (options.normalized && totalNodes > 1) {
|
|
60
|
+
centrality = centrality / (totalNodes - 1);
|
|
61
|
+
}
|
|
74
62
|
} else {
|
|
75
63
|
// Standard closeness: reciprocal of sum of distances
|
|
76
64
|
let totalDistance = 0;
|
|
77
65
|
let reachableNodes = 0;
|
|
78
66
|
|
|
79
|
-
for (const [targetNode, distance] of
|
|
80
|
-
if (targetNode !==
|
|
67
|
+
for (const [targetNode, distance] of distances) {
|
|
68
|
+
if (targetNode !== sourceNode && distance < Infinity) {
|
|
81
69
|
totalDistance += distance;
|
|
82
70
|
reachableNodes++;
|
|
83
71
|
}
|
|
@@ -87,70 +75,70 @@ export function nodeClosenessCentrality(
|
|
|
87
75
|
centrality = 1 / totalDistance;
|
|
88
76
|
|
|
89
77
|
// Wasserman and Faust normalization for disconnected graphs
|
|
90
|
-
if (options.normalized) {
|
|
91
|
-
|
|
92
|
-
centrality = centrality * reachableNodes / (n - 1);
|
|
78
|
+
if (options.normalized && totalNodes > 1) {
|
|
79
|
+
centrality = centrality * reachableNodes / (totalNodes - 1);
|
|
93
80
|
}
|
|
94
81
|
}
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
// Normalization for harmonic centrality
|
|
98
|
-
if (options.harmonic && options.normalized) {
|
|
99
|
-
const n = Array.from(graph.nodes()).length;
|
|
100
|
-
if (n > 1) {
|
|
101
|
-
centrality = centrality / (n - 1);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
84
|
return centrality;
|
|
106
85
|
}
|
|
107
86
|
|
|
108
87
|
/**
|
|
109
|
-
*
|
|
88
|
+
* Calculate closeness centrality for all nodes in the graph
|
|
89
|
+
*
|
|
90
|
+
* Closeness centrality measures how close a node is to all other nodes
|
|
91
|
+
* in the graph. It is the reciprocal of the sum of the shortest path
|
|
92
|
+
* distances to all other reachable nodes.
|
|
93
|
+
*
|
|
94
|
+
* @param graph - The input graph
|
|
95
|
+
* @param options - Algorithm options
|
|
96
|
+
* @returns Centrality scores for each node
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const graph = new Graph();
|
|
101
|
+
* graph.addEdge("A", "B");
|
|
102
|
+
* graph.addEdge("B", "C");
|
|
103
|
+
*
|
|
104
|
+
* const centrality = closenessCentrality(graph);
|
|
105
|
+
* // { A: 0.5, B: 1.0, C: 0.5 }
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* Time Complexity: O(V * (V + E)) for unweighted graphs
|
|
109
|
+
* Space Complexity: O(V)
|
|
110
110
|
*/
|
|
111
|
-
function
|
|
111
|
+
export function closenessCentrality(
|
|
112
112
|
graph: Graph,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
const
|
|
117
|
-
const visited = new Set<NodeId>();
|
|
118
|
-
const queue: {node: NodeId, distance: number}[] = [];
|
|
119
|
-
|
|
120
|
-
// Initialize
|
|
121
|
-
queue.push({node: source, distance: 0});
|
|
122
|
-
visited.add(source);
|
|
123
|
-
distances.set(source, 0);
|
|
124
|
-
|
|
125
|
-
while (queue.length > 0) {
|
|
126
|
-
const current = queue.shift();
|
|
127
|
-
if (!current) {
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Skip if beyond cutoff
|
|
132
|
-
if (cutoff !== undefined && current.distance >= cutoff) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
113
|
+
options: ClosenessCentralityOptions = {},
|
|
114
|
+
): Record<string, number> {
|
|
115
|
+
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
116
|
+
const centrality: Record<string, number> = {};
|
|
135
117
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const newDistance = current.distance + 1;
|
|
118
|
+
for (const sourceNode of nodes) {
|
|
119
|
+
centrality[String(sourceNode)] = nodeClosenessCentrality(graph, sourceNode, options);
|
|
120
|
+
}
|
|
140
121
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
122
|
+
return centrality;
|
|
123
|
+
}
|
|
145
124
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Calculate closeness centrality for a specific node
|
|
127
|
+
*/
|
|
128
|
+
export function nodeClosenessCentrality(
|
|
129
|
+
graph: Graph,
|
|
130
|
+
node: NodeId,
|
|
131
|
+
options: ClosenessCentralityOptions = {},
|
|
132
|
+
): number {
|
|
133
|
+
if (!graph.hasNode(node)) {
|
|
134
|
+
throw new Error(`Node ${String(node)} not found in graph`);
|
|
151
135
|
}
|
|
152
136
|
|
|
153
|
-
|
|
137
|
+
// Use optimized BFS variant for unweighted graphs
|
|
138
|
+
const distances = bfsDistancesOnly(graph, node, options.cutoff, options.optimized !== undefined ? {optimized: options.optimized} : {});
|
|
139
|
+
const totalNodes = graph.nodeCount;
|
|
140
|
+
|
|
141
|
+
return calculateClosenessFromDistances(distances, node, totalNodes, options);
|
|
154
142
|
}
|
|
155
143
|
|
|
156
144
|
/**
|
|
@@ -182,115 +170,9 @@ export function nodeWeightedClosenessCentrality(
|
|
|
182
170
|
throw new Error(`Node ${String(node)} not found in graph`);
|
|
183
171
|
}
|
|
184
172
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return 0; // No other nodes reachable
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
let centrality = 0;
|
|
192
|
-
|
|
193
|
-
if (options.harmonic) {
|
|
194
|
-
// Harmonic centrality: sum of reciprocals of distances
|
|
195
|
-
for (const [targetNode, distance] of Array.from(distances)) {
|
|
196
|
-
if (targetNode !== node && distance > 0 && distance < Infinity) {
|
|
197
|
-
centrality += 1 / distance;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} else {
|
|
201
|
-
// Standard closeness: reciprocal of sum of distances
|
|
202
|
-
let totalDistance = 0;
|
|
203
|
-
let reachableNodes = 0;
|
|
204
|
-
|
|
205
|
-
for (const [targetNode, distance] of Array.from(distances)) {
|
|
206
|
-
if (targetNode !== node && distance < Infinity) {
|
|
207
|
-
totalDistance += distance;
|
|
208
|
-
reachableNodes++;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (totalDistance > 0) {
|
|
213
|
-
centrality = 1 / totalDistance;
|
|
214
|
-
|
|
215
|
-
// Wasserman and Faust normalization for disconnected graphs
|
|
216
|
-
if (options.normalized) {
|
|
217
|
-
const n = Array.from(graph.nodes()).length;
|
|
218
|
-
centrality = centrality * reachableNodes / (n - 1);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Normalization for harmonic centrality
|
|
224
|
-
if (options.harmonic && options.normalized) {
|
|
225
|
-
const n = Array.from(graph.nodes()).length;
|
|
226
|
-
if (n > 1) {
|
|
227
|
-
centrality = centrality / (n - 1);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return centrality;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Single-source shortest path distances using Dijkstra's algorithm
|
|
236
|
-
*/
|
|
237
|
-
function dijkstraDistances(
|
|
238
|
-
graph: Graph,
|
|
239
|
-
source: NodeId,
|
|
240
|
-
cutoff?: number,
|
|
241
|
-
): Map<NodeId, number> {
|
|
242
|
-
const distances = new Map<NodeId, number>();
|
|
243
|
-
const visited = new Set<NodeId>();
|
|
244
|
-
const pq: {node: NodeId, distance: number}[] = [];
|
|
245
|
-
|
|
246
|
-
// Initialize distances
|
|
247
|
-
for (const node of Array.from(graph.nodes())) {
|
|
248
|
-
distances.set(node.id, node.id === source ? 0 : Infinity);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
pq.push({node: source, distance: 0});
|
|
252
|
-
|
|
253
|
-
while (pq.length > 0) {
|
|
254
|
-
// Simple priority queue (could be optimized with a proper heap)
|
|
255
|
-
pq.sort((a, b) => a.distance - b.distance);
|
|
256
|
-
const current = pq.shift();
|
|
257
|
-
|
|
258
|
-
if (!current || visited.has(current.node)) {
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
visited.add(current.node);
|
|
263
|
-
|
|
264
|
-
// Skip if beyond cutoff
|
|
265
|
-
if (cutoff !== undefined && current.distance > cutoff) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Explore neighbors
|
|
270
|
-
for (const neighbor of Array.from(graph.neighbors(current.node))) {
|
|
271
|
-
if (visited.has(neighbor)) {
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const edge = graph.getEdge(current.node, neighbor);
|
|
276
|
-
if (!edge) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const edgeWeight = edge.weight ?? 1;
|
|
281
|
-
const tentativeDistance = current.distance + edgeWeight;
|
|
282
|
-
const currentDistance = distances.get(neighbor) ?? Infinity;
|
|
283
|
-
|
|
284
|
-
if (tentativeDistance < currentDistance) {
|
|
285
|
-
distances.set(neighbor, tentativeDistance);
|
|
286
|
-
|
|
287
|
-
// Skip if beyond cutoff
|
|
288
|
-
if (cutoff === undefined || tentativeDistance <= cutoff) {
|
|
289
|
-
pq.push({node: neighbor, distance: tentativeDistance});
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
173
|
+
// Use optimized weighted BFS variant (simplified Dijkstra)
|
|
174
|
+
const distances = bfsWeightedDistances(graph, node, options.cutoff, options.optimized !== undefined ? {optimized: options.optimized} : {});
|
|
175
|
+
const totalNodes = graph.nodeCount;
|
|
294
176
|
|
|
295
|
-
return distances;
|
|
177
|
+
return calculateClosenessFromDistances(distances, node, totalNodes, options);
|
|
296
178
|
}
|
|
@@ -18,10 +18,10 @@ export function degreeCentrality(
|
|
|
18
18
|
const centrality: CentralityResult = {};
|
|
19
19
|
const {nodeCount} = graph;
|
|
20
20
|
|
|
21
|
-
// Calculate normalization factor
|
|
22
|
-
const maxPossibleDegree =
|
|
21
|
+
// Calculate normalization factor (same for both directed and undirected graphs)
|
|
22
|
+
const maxPossibleDegree = nodeCount - 1;
|
|
23
23
|
|
|
24
|
-
for (const node of
|
|
24
|
+
for (const node of graph.nodes()) {
|
|
25
25
|
let degree: number;
|
|
26
26
|
|
|
27
27
|
if (graph.isDirected && options.mode) {
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import type {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {NodeId} from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Simplified Delta-based PageRank that matches the standard algorithm
|
|
6
|
+
* but only processes active nodes for efficiency.
|
|
7
|
+
*
|
|
8
|
+
* Key optimization: Supports incremental updates when graph structure changes,
|
|
9
|
+
* avoiding full recomputation from scratch.
|
|
10
|
+
*/
|
|
11
|
+
export class SimpleDeltaPageRank {
|
|
12
|
+
private graph: Graph;
|
|
13
|
+
private nodeCount: number;
|
|
14
|
+
private previousScores: Map<NodeId, number> | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(graph: Graph) {
|
|
17
|
+
if (!graph.isDirected) {
|
|
18
|
+
throw new Error("PageRank requires a directed graph");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
this.graph = graph;
|
|
22
|
+
this.nodeCount = graph.nodeCount;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public compute(options: {
|
|
26
|
+
dampingFactor?: number;
|
|
27
|
+
tolerance?: number;
|
|
28
|
+
maxIterations?: number;
|
|
29
|
+
personalization?: Map<NodeId, number>;
|
|
30
|
+
weight?: string;
|
|
31
|
+
} = {}): Map<NodeId, number> {
|
|
32
|
+
const {
|
|
33
|
+
dampingFactor = 0.85,
|
|
34
|
+
tolerance = 1e-6,
|
|
35
|
+
maxIterations = 100,
|
|
36
|
+
personalization,
|
|
37
|
+
weight,
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
if (this.nodeCount === 0) {
|
|
41
|
+
return new Map();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Initialize scores
|
|
45
|
+
const scores = new Map<NodeId, number>();
|
|
46
|
+
const newScores = new Map<NodeId, number>();
|
|
47
|
+
|
|
48
|
+
for (const node of this.graph.nodes()) {
|
|
49
|
+
scores.set(node.id, 1.0 / this.nodeCount);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Precompute out-degrees and weights
|
|
53
|
+
const outWeights = new Map<NodeId, number>();
|
|
54
|
+
const danglingNodes = new Set<NodeId>();
|
|
55
|
+
|
|
56
|
+
for (const node of this.graph.nodes()) {
|
|
57
|
+
let totalWeight = 0;
|
|
58
|
+
let outDegree = 0;
|
|
59
|
+
|
|
60
|
+
for (const neighbor of Array.from(this.graph.neighbors(node.id))) {
|
|
61
|
+
outDegree++;
|
|
62
|
+
const edge = this.graph.getEdge(node.id, neighbor);
|
|
63
|
+
totalWeight += edge?.weight ?? 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
outWeights.set(node.id, totalWeight);
|
|
67
|
+
if (outDegree === 0) {
|
|
68
|
+
danglingNodes.add(node.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Normalize personalization vector if provided
|
|
73
|
+
let personalVector: Map<NodeId, number> | null = null;
|
|
74
|
+
if (personalization) {
|
|
75
|
+
personalVector = new Map(personalization);
|
|
76
|
+
this.normalizeMap(personalVector);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Power iteration with active node tracking
|
|
80
|
+
let activeNodes = new Set<NodeId>(scores.keys());
|
|
81
|
+
let iteration = 0;
|
|
82
|
+
|
|
83
|
+
for (iteration = 0; iteration < maxIterations; iteration++) {
|
|
84
|
+
// Initialize new scores
|
|
85
|
+
for (const nodeId of scores.keys()) {
|
|
86
|
+
if (personalVector) {
|
|
87
|
+
newScores.set(nodeId, (1 - dampingFactor) * (personalVector.get(nodeId) ?? 0));
|
|
88
|
+
} else {
|
|
89
|
+
newScores.set(nodeId, (1 - dampingFactor) / this.nodeCount);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Handle dangling nodes
|
|
94
|
+
let danglingSum = 0;
|
|
95
|
+
for (const danglingNode of danglingNodes) {
|
|
96
|
+
danglingSum += scores.get(danglingNode) ?? 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (danglingSum > 0) {
|
|
100
|
+
const danglingContribution = dampingFactor * danglingSum / this.nodeCount;
|
|
101
|
+
for (const nodeId of scores.keys()) {
|
|
102
|
+
const current = newScores.get(nodeId) ?? 0;
|
|
103
|
+
if (personalVector) {
|
|
104
|
+
const personalContrib = danglingContribution * (personalVector.get(nodeId) ?? 0);
|
|
105
|
+
newScores.set(nodeId, current + personalContrib);
|
|
106
|
+
} else {
|
|
107
|
+
newScores.set(nodeId, current + danglingContribution);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Propagate rank from active nodes only
|
|
113
|
+
for (const nodeId of activeNodes) {
|
|
114
|
+
const currentRank = scores.get(nodeId) ?? 0;
|
|
115
|
+
const nodeOutWeight = outWeights.get(nodeId) ?? 0;
|
|
116
|
+
|
|
117
|
+
if (nodeOutWeight > 0) {
|
|
118
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
119
|
+
let edgeWeight = 1;
|
|
120
|
+
if (weight) {
|
|
121
|
+
const edge = this.graph.getEdge(nodeId, neighbor);
|
|
122
|
+
edgeWeight = edge?.weight ?? 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const contribution = dampingFactor * currentRank * (edgeWeight / nodeOutWeight);
|
|
126
|
+
const neighborRank = newScores.get(neighbor) ?? 0;
|
|
127
|
+
newScores.set(neighbor, neighborRank + contribution);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check convergence and identify active nodes
|
|
133
|
+
const nextActive = new Set<NodeId>();
|
|
134
|
+
let maxDiff = 0;
|
|
135
|
+
|
|
136
|
+
for (const nodeId of scores.keys()) {
|
|
137
|
+
const oldRank = scores.get(nodeId) ?? 0;
|
|
138
|
+
const newRank = newScores.get(nodeId) ?? 0;
|
|
139
|
+
const diff = Math.abs(newRank - oldRank);
|
|
140
|
+
maxDiff = Math.max(maxDiff, diff);
|
|
141
|
+
|
|
142
|
+
// Mark as active if change is significant
|
|
143
|
+
if (diff > tolerance / 10) {
|
|
144
|
+
nextActive.add(nodeId);
|
|
145
|
+
// Also mark neighbors as potentially active
|
|
146
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
147
|
+
nextActive.add(neighbor);
|
|
148
|
+
}
|
|
149
|
+
for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
|
|
150
|
+
nextActive.add(neighbor);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Swap score maps
|
|
156
|
+
scores.clear();
|
|
157
|
+
for (const [k, v] of newScores) {
|
|
158
|
+
scores.set(k, v);
|
|
159
|
+
}
|
|
160
|
+
newScores.clear();
|
|
161
|
+
|
|
162
|
+
activeNodes = nextActive;
|
|
163
|
+
|
|
164
|
+
if (maxDiff < tolerance) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Store scores for potential incremental updates
|
|
170
|
+
this.previousScores = new Map(scores);
|
|
171
|
+
|
|
172
|
+
return scores;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Perform incremental update after graph modification.
|
|
177
|
+
* This is where delta-based approach provides significant speedup.
|
|
178
|
+
*
|
|
179
|
+
* @param modifiedNodes Set of nodes that were modified (edges added/removed)
|
|
180
|
+
* @param options Computation options
|
|
181
|
+
* @returns Updated PageRank scores
|
|
182
|
+
*/
|
|
183
|
+
public update(
|
|
184
|
+
modifiedNodes: Set<NodeId>,
|
|
185
|
+
options: {
|
|
186
|
+
dampingFactor?: number;
|
|
187
|
+
tolerance?: number;
|
|
188
|
+
maxIterations?: number;
|
|
189
|
+
personalization?: Map<NodeId, number>;
|
|
190
|
+
weight?: string;
|
|
191
|
+
} = {},
|
|
192
|
+
): Map<NodeId, number> {
|
|
193
|
+
if (!this.previousScores) {
|
|
194
|
+
// No previous computation, do full compute
|
|
195
|
+
return this.compute(options);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const {
|
|
199
|
+
dampingFactor = 0.85,
|
|
200
|
+
tolerance = 1e-6,
|
|
201
|
+
maxIterations = 100,
|
|
202
|
+
personalization,
|
|
203
|
+
weight,
|
|
204
|
+
} = options;
|
|
205
|
+
|
|
206
|
+
// Initialize scores from previous computation
|
|
207
|
+
const scores = new Map(this.previousScores);
|
|
208
|
+
const newScores = new Map<NodeId, number>();
|
|
209
|
+
|
|
210
|
+
// Ensure all current nodes are included
|
|
211
|
+
for (const node of this.graph.nodes()) {
|
|
212
|
+
if (!scores.has(node.id)) {
|
|
213
|
+
scores.set(node.id, 1.0 / this.nodeCount);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Start with only modified nodes and their neighbors as active
|
|
218
|
+
let activeNodes = new Set<NodeId>();
|
|
219
|
+
for (const nodeId of modifiedNodes) {
|
|
220
|
+
activeNodes.add(nodeId);
|
|
221
|
+
// Add incoming neighbors
|
|
222
|
+
for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
|
|
223
|
+
activeNodes.add(neighbor);
|
|
224
|
+
}
|
|
225
|
+
// Add outgoing neighbors
|
|
226
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
227
|
+
activeNodes.add(neighbor);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Recompute out-weights for modified nodes
|
|
232
|
+
const outWeights = new Map<NodeId, number>();
|
|
233
|
+
const danglingNodes = new Set<NodeId>();
|
|
234
|
+
|
|
235
|
+
for (const node of this.graph.nodes()) {
|
|
236
|
+
let totalWeight = 0;
|
|
237
|
+
let outDegree = 0;
|
|
238
|
+
|
|
239
|
+
for (const neighbor of Array.from(this.graph.neighbors(node.id))) {
|
|
240
|
+
outDegree++;
|
|
241
|
+
const edge = this.graph.getEdge(node.id, neighbor);
|
|
242
|
+
totalWeight += edge?.weight ?? 1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
outWeights.set(node.id, totalWeight);
|
|
246
|
+
if (outDegree === 0) {
|
|
247
|
+
danglingNodes.add(node.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Normalize personalization vector if provided
|
|
252
|
+
let personalVector: Map<NodeId, number> | null = null;
|
|
253
|
+
if (personalization) {
|
|
254
|
+
personalVector = new Map(personalization);
|
|
255
|
+
this.normalizeMap(personalVector);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Run limited iterations focusing on active nodes
|
|
259
|
+
let iteration = 0;
|
|
260
|
+
|
|
261
|
+
for (iteration = 0; iteration < maxIterations && activeNodes.size > 0; iteration++) {
|
|
262
|
+
// Initialize new scores
|
|
263
|
+
for (const nodeId of scores.keys()) {
|
|
264
|
+
if (personalVector) {
|
|
265
|
+
newScores.set(nodeId, (1 - dampingFactor) * (personalVector.get(nodeId) ?? 0));
|
|
266
|
+
} else {
|
|
267
|
+
newScores.set(nodeId, (1 - dampingFactor) / this.nodeCount);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle dangling nodes
|
|
272
|
+
let danglingSum = 0;
|
|
273
|
+
for (const danglingNode of danglingNodes) {
|
|
274
|
+
danglingSum += scores.get(danglingNode) ?? 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (danglingSum > 0) {
|
|
278
|
+
const danglingContribution = dampingFactor * danglingSum / this.nodeCount;
|
|
279
|
+
for (const nodeId of scores.keys()) {
|
|
280
|
+
const current = newScores.get(nodeId) ?? 0;
|
|
281
|
+
if (personalVector) {
|
|
282
|
+
const personalContrib = danglingContribution * (personalVector.get(nodeId) ?? 0);
|
|
283
|
+
newScores.set(nodeId, current + personalContrib);
|
|
284
|
+
} else {
|
|
285
|
+
newScores.set(nodeId, current + danglingContribution);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Propagate rank from ALL nodes (not just active)
|
|
291
|
+
// This ensures correctness
|
|
292
|
+
for (const nodeId of scores.keys()) {
|
|
293
|
+
const currentRank = scores.get(nodeId) ?? 0;
|
|
294
|
+
const nodeOutWeight = outWeights.get(nodeId) ?? 0;
|
|
295
|
+
|
|
296
|
+
if (nodeOutWeight > 0) {
|
|
297
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
298
|
+
let edgeWeight = 1;
|
|
299
|
+
if (weight) {
|
|
300
|
+
const edge = this.graph.getEdge(nodeId, neighbor);
|
|
301
|
+
edgeWeight = edge?.weight ?? 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const contribution = dampingFactor * currentRank * (edgeWeight / nodeOutWeight);
|
|
305
|
+
const neighborRank = newScores.get(neighbor) ?? 0;
|
|
306
|
+
newScores.set(neighbor, neighborRank + contribution);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check convergence only for active nodes
|
|
312
|
+
const nextActive = new Set<NodeId>();
|
|
313
|
+
let maxDiff = 0;
|
|
314
|
+
|
|
315
|
+
for (const nodeId of activeNodes) {
|
|
316
|
+
const oldRank = scores.get(nodeId) ?? 0;
|
|
317
|
+
const newRank = newScores.get(nodeId) ?? 0;
|
|
318
|
+
const diff = Math.abs(newRank - oldRank);
|
|
319
|
+
maxDiff = Math.max(maxDiff, diff);
|
|
320
|
+
|
|
321
|
+
// Mark as active if change is significant
|
|
322
|
+
if (diff > tolerance / 10) {
|
|
323
|
+
nextActive.add(nodeId);
|
|
324
|
+
// Also mark neighbors as potentially active
|
|
325
|
+
for (const neighbor of Array.from(this.graph.neighbors(nodeId))) {
|
|
326
|
+
nextActive.add(neighbor);
|
|
327
|
+
}
|
|
328
|
+
for (const neighbor of Array.from(this.graph.inNeighbors(nodeId))) {
|
|
329
|
+
nextActive.add(neighbor);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Swap score maps
|
|
335
|
+
scores.clear();
|
|
336
|
+
for (const [k, v] of newScores) {
|
|
337
|
+
scores.set(k, v);
|
|
338
|
+
}
|
|
339
|
+
newScores.clear();
|
|
340
|
+
|
|
341
|
+
activeNodes = nextActive;
|
|
342
|
+
|
|
343
|
+
if (maxDiff < tolerance) {
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Store scores for future incremental updates
|
|
349
|
+
this.previousScores = new Map(scores);
|
|
350
|
+
|
|
351
|
+
return scores;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private normalizeMap(map: Map<NodeId, number>): void {
|
|
355
|
+
let sum = 0;
|
|
356
|
+
for (const value of map.values()) {
|
|
357
|
+
sum += value;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (sum > 0) {
|
|
361
|
+
for (const [key, value] of Array.from(map)) {
|
|
362
|
+
map.set(key, value / sum);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|