@graphty/algorithms 1.0.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/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts +34 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/betweenness.js +223 -0
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -0
- package/dist/src/algorithms/centrality/closeness.d.ts +43 -0
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/closeness.js +202 -0
- package/dist/src/algorithms/centrality/closeness.js.map +1 -0
- package/dist/src/algorithms/centrality/degree.d.ts +17 -0
- package/dist/src/algorithms/centrality/degree.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/degree.js +86 -0
- package/dist/src/algorithms/centrality/degree.js.map +1 -0
- package/dist/src/algorithms/centrality/index.d.ts +11 -0
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/index.js +8 -0
- package/dist/src/algorithms/centrality/index.js.map +1 -0
- package/dist/src/algorithms/centrality/pagerank.d.ts +74 -0
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/pagerank.js +191 -0
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -0
- package/dist/src/algorithms/components/connected.d.ts +58 -0
- package/dist/src/algorithms/components/connected.d.ts.map +1 -0
- package/dist/src/algorithms/components/connected.js +234 -0
- package/dist/src/algorithms/components/connected.js.map +1 -0
- package/dist/src/algorithms/components/index.d.ts +5 -0
- package/dist/src/algorithms/components/index.d.ts.map +1 -0
- package/dist/src/algorithms/components/index.js +5 -0
- package/dist/src/algorithms/components/index.js.map +1 -0
- package/dist/src/algorithms/index.d.ts +8 -0
- package/dist/src/algorithms/index.d.ts.map +1 -0
- package/dist/src/algorithms/index.js +12 -0
- package/dist/src/algorithms/index.js.map +1 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts +52 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js +131 -0
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -0
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts +26 -0
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/dijkstra.js +199 -0
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -0
- package/dist/src/algorithms/shortest-path/index.d.ts +7 -0
- package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/index.js +6 -0
- package/dist/src/algorithms/shortest-path/index.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs.d.ts +25 -0
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs.js +189 -0
- package/dist/src/algorithms/traversal/bfs.js.map +1 -0
- package/dist/src/algorithms/traversal/dfs.d.ts +32 -0
- package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/dfs.js +259 -0
- package/dist/src/algorithms/traversal/dfs.js.map +1 -0
- package/dist/src/algorithms/traversal/index.d.ts +7 -0
- package/dist/src/algorithms/traversal/index.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/index.js +6 -0
- package/dist/src/algorithms/traversal/index.js.map +1 -0
- package/dist/src/core/graph.d.ts +100 -0
- package/dist/src/core/graph.d.ts.map +1 -0
- package/dist/src/core/graph.js +276 -0
- package/dist/src/core/graph.js.map +1 -0
- package/dist/src/data-structures/index.d.ts +6 -0
- package/dist/src/data-structures/index.d.ts.map +1 -0
- package/dist/src/data-structures/index.js +6 -0
- package/dist/src/data-structures/index.js.map +1 -0
- package/dist/src/data-structures/priority-queue.d.ts +60 -0
- package/dist/src/data-structures/priority-queue.d.ts.map +1 -0
- package/dist/src/data-structures/priority-queue.js +152 -0
- package/dist/src/data-structures/priority-queue.js.map +1 -0
- package/dist/src/data-structures/union-find.d.ts +54 -0
- package/dist/src/data-structures/union-find.d.ts.map +1 -0
- package/dist/src/data-structures/union-find.js +145 -0
- package/dist/src/data-structures/union-find.js.map +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +13 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/types/index.d.ts +74 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +5 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/test/browser/basic.test.d.ts +2 -0
- package/dist/test/browser/basic.test.d.ts.map +1 -0
- package/dist/test/browser/basic.test.js +70 -0
- package/dist/test/browser/basic.test.js.map +1 -0
- package/dist/test/unit/bellman-ford.test.d.ts +2 -0
- package/dist/test/unit/bellman-ford.test.d.ts.map +1 -0
- package/dist/test/unit/bellman-ford.test.js +231 -0
- package/dist/test/unit/bellman-ford.test.js.map +1 -0
- package/dist/test/unit/betweenness-centrality.test.d.ts +2 -0
- package/dist/test/unit/betweenness-centrality.test.d.ts.map +1 -0
- package/dist/test/unit/betweenness-centrality.test.js +311 -0
- package/dist/test/unit/betweenness-centrality.test.js.map +1 -0
- package/dist/test/unit/bfs.test.d.ts +2 -0
- package/dist/test/unit/bfs.test.d.ts.map +1 -0
- package/dist/test/unit/bfs.test.js +277 -0
- package/dist/test/unit/bfs.test.js.map +1 -0
- package/dist/test/unit/closeness.test.d.ts +2 -0
- package/dist/test/unit/closeness.test.d.ts.map +1 -0
- package/dist/test/unit/closeness.test.js +266 -0
- package/dist/test/unit/closeness.test.js.map +1 -0
- package/dist/test/unit/connected-components.test.d.ts +2 -0
- package/dist/test/unit/connected-components.test.d.ts.map +1 -0
- package/dist/test/unit/connected-components.test.js +355 -0
- package/dist/test/unit/connected-components.test.js.map +1 -0
- package/dist/test/unit/degree-centrality.test.d.ts +2 -0
- package/dist/test/unit/degree-centrality.test.d.ts.map +1 -0
- package/dist/test/unit/degree-centrality.test.js +260 -0
- package/dist/test/unit/degree-centrality.test.js.map +1 -0
- package/dist/test/unit/dfs.test.d.ts +2 -0
- package/dist/test/unit/dfs.test.d.ts.map +1 -0
- package/dist/test/unit/dfs.test.js +350 -0
- package/dist/test/unit/dfs.test.js.map +1 -0
- package/dist/test/unit/dijkstra.test.d.ts +2 -0
- package/dist/test/unit/dijkstra.test.d.ts.map +1 -0
- package/dist/test/unit/dijkstra.test.js +311 -0
- package/dist/test/unit/dijkstra.test.js.map +1 -0
- package/dist/test/unit/graph.test.d.ts +2 -0
- package/dist/test/unit/graph.test.d.ts.map +1 -0
- package/dist/test/unit/graph.test.js +175 -0
- package/dist/test/unit/graph.test.js.map +1 -0
- package/dist/test/unit/pagerank.test.d.ts +2 -0
- package/dist/test/unit/pagerank.test.d.ts.map +1 -0
- package/dist/test/unit/pagerank.test.js +281 -0
- package/dist/test/unit/pagerank.test.js.map +1 -0
- package/dist/test/unit/priority-queue.test.d.ts +2 -0
- package/dist/test/unit/priority-queue.test.d.ts.map +1 -0
- package/dist/test/unit/priority-queue.test.js +189 -0
- package/dist/test/unit/priority-queue.test.js.map +1 -0
- package/dist/test/unit/union-find.test.d.ts +2 -0
- package/dist/test/unit/union-find.test.d.ts.map +1 -0
- package/dist/test/unit/union-find.test.js +245 -0
- package/dist/test/unit/union-find.test.js.map +1 -0
- package/package.json +100 -0
- package/src/algorithms/centrality/betweenness.ts +303 -0
- package/src/algorithms/centrality/closeness.ts +296 -0
- package/src/algorithms/centrality/degree.ts +105 -0
- package/src/algorithms/centrality/index.ts +11 -0
- package/src/algorithms/centrality/pagerank.ts +302 -0
- package/src/algorithms/components/connected.ts +298 -0
- package/src/algorithms/components/index.ts +17 -0
- package/src/algorithms/index.ts +15 -0
- package/src/algorithms/shortest-path/bellman-ford.ts +214 -0
- package/src/algorithms/shortest-path/dijkstra.ts +256 -0
- package/src/algorithms/shortest-path/index.ts +7 -0
- package/src/algorithms/traversal/bfs.ts +236 -0
- package/src/algorithms/traversal/dfs.ts +367 -0
- package/src/algorithms/traversal/index.ts +7 -0
- package/src/core/graph.ts +326 -0
- package/src/data-structures/index.ts +6 -0
- package/src/data-structures/priority-queue.ts +180 -0
- package/src/data-structures/union-find.ts +170 -0
- package/src/index.ts +35 -0
- package/src/types/index.ts +95 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {NodeId} from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Closeness centrality implementation
|
|
6
|
+
*
|
|
7
|
+
* Measures how close a node is to all other nodes in the graph.
|
|
8
|
+
* Uses BFS to compute shortest path distances efficiently.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Closeness centrality options
|
|
13
|
+
*/
|
|
14
|
+
export interface ClosenessCentralityOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Whether to normalize the centrality values (default: false)
|
|
17
|
+
*/
|
|
18
|
+
normalized?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Use harmonic mean instead of reciprocal of sum (default: false)
|
|
21
|
+
* Better for disconnected graphs
|
|
22
|
+
*/
|
|
23
|
+
harmonic?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Consider only nodes within this distance (default: undefined = all nodes)
|
|
26
|
+
*/
|
|
27
|
+
cutoff?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculate closeness centrality for all nodes
|
|
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
|
|
49
|
+
*/
|
|
50
|
+
export function nodeClosenessCentrality(
|
|
51
|
+
graph: Graph,
|
|
52
|
+
node: NodeId,
|
|
53
|
+
options: ClosenessCentralityOptions = {},
|
|
54
|
+
): 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
|
+
if (distances.size <= 1) {
|
|
62
|
+
return 0; // No other nodes reachable
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let centrality = 0;
|
|
66
|
+
|
|
67
|
+
if (options.harmonic) {
|
|
68
|
+
// Harmonic centrality: sum of reciprocals of distances
|
|
69
|
+
for (const [targetNode, distance] of distances) {
|
|
70
|
+
if (targetNode !== node && distance > 0) {
|
|
71
|
+
centrality += 1 / distance;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Standard closeness: reciprocal of sum of distances
|
|
76
|
+
let totalDistance = 0;
|
|
77
|
+
let reachableNodes = 0;
|
|
78
|
+
|
|
79
|
+
for (const [targetNode, distance] of distances) {
|
|
80
|
+
if (targetNode !== node) {
|
|
81
|
+
totalDistance += distance;
|
|
82
|
+
reachableNodes++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (totalDistance > 0) {
|
|
87
|
+
centrality = 1 / totalDistance;
|
|
88
|
+
|
|
89
|
+
// Wasserman and Faust normalization for disconnected graphs
|
|
90
|
+
if (options.normalized) {
|
|
91
|
+
const n = Array.from(graph.nodes()).length;
|
|
92
|
+
centrality = centrality * reachableNodes / (n - 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
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
|
+
return centrality;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Single-source shortest path lengths using BFS (for unweighted graphs)
|
|
110
|
+
*/
|
|
111
|
+
function singleSourceShortestPathLengths(
|
|
112
|
+
graph: Graph,
|
|
113
|
+
source: NodeId,
|
|
114
|
+
cutoff?: number,
|
|
115
|
+
): Map<NodeId, number> {
|
|
116
|
+
const distances = new Map<NodeId, number>();
|
|
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
|
+
}
|
|
135
|
+
|
|
136
|
+
// Explore neighbors
|
|
137
|
+
for (const neighbor of graph.neighbors(current.node)) {
|
|
138
|
+
if (!visited.has(neighbor)) {
|
|
139
|
+
const newDistance = current.distance + 1;
|
|
140
|
+
|
|
141
|
+
// Skip if beyond cutoff
|
|
142
|
+
if (cutoff !== undefined && newDistance > cutoff) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
visited.add(neighbor);
|
|
147
|
+
distances.set(neighbor, newDistance);
|
|
148
|
+
queue.push({node: neighbor, distance: newDistance});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return distances;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Calculate weighted closeness centrality using Dijkstra's algorithm
|
|
158
|
+
*/
|
|
159
|
+
export function weightedClosenessCentrality(
|
|
160
|
+
graph: Graph,
|
|
161
|
+
options: ClosenessCentralityOptions = {},
|
|
162
|
+
): Record<string, number> {
|
|
163
|
+
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
164
|
+
const centrality: Record<string, number> = {};
|
|
165
|
+
|
|
166
|
+
for (const sourceNode of nodes) {
|
|
167
|
+
centrality[String(sourceNode)] = nodeWeightedClosenessCentrality(graph, sourceNode, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return centrality;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Calculate weighted closeness centrality for a specific node using Dijkstra
|
|
175
|
+
*/
|
|
176
|
+
export function nodeWeightedClosenessCentrality(
|
|
177
|
+
graph: Graph,
|
|
178
|
+
node: NodeId,
|
|
179
|
+
options: ClosenessCentralityOptions = {},
|
|
180
|
+
): number {
|
|
181
|
+
if (!graph.hasNode(node)) {
|
|
182
|
+
throw new Error(`Node ${String(node)} not found in graph`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const distances = dijkstraDistances(graph, node, options.cutoff);
|
|
186
|
+
|
|
187
|
+
if (distances.size <= 1) {
|
|
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 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 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 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 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
|
+
}
|
|
294
|
+
|
|
295
|
+
return distances;
|
|
296
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {CentralityOptions, CentralityResult} from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Degree centrality implementation
|
|
6
|
+
*
|
|
7
|
+
* Measures the importance of a node based on the number of connections it has.
|
|
8
|
+
* The most basic centrality measure that simply counts incident edges.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculate degree centrality for all nodes in the graph
|
|
13
|
+
*/
|
|
14
|
+
export function degreeCentrality(
|
|
15
|
+
graph: Graph,
|
|
16
|
+
options: CentralityOptions = {},
|
|
17
|
+
): CentralityResult {
|
|
18
|
+
const centrality: CentralityResult = {};
|
|
19
|
+
const {nodeCount} = graph;
|
|
20
|
+
|
|
21
|
+
// Calculate normalization factor
|
|
22
|
+
const maxPossibleDegree = graph.isDirected ? nodeCount - 1 : nodeCount - 1;
|
|
23
|
+
|
|
24
|
+
for (const node of graph.nodes()) {
|
|
25
|
+
let degree: number;
|
|
26
|
+
|
|
27
|
+
if (graph.isDirected && options.mode) {
|
|
28
|
+
// For directed graphs, calculate specific degree type
|
|
29
|
+
switch (options.mode) {
|
|
30
|
+
case "in": {
|
|
31
|
+
degree = graph.inDegree(node.id);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "out": {
|
|
35
|
+
degree = graph.outDegree(node.id);
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "total": {
|
|
39
|
+
degree = graph.degree(node.id);
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
default: {
|
|
43
|
+
degree = graph.degree(node.id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
// For undirected graphs or default case
|
|
48
|
+
degree = graph.degree(node.id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Apply normalization if requested
|
|
52
|
+
const normalizedDegree = options.normalized && maxPossibleDegree > 0 ?
|
|
53
|
+
degree / maxPossibleDegree :
|
|
54
|
+
degree;
|
|
55
|
+
|
|
56
|
+
centrality[node.id.toString()] = normalizedDegree;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return centrality;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculate degree centrality for a specific node
|
|
64
|
+
*/
|
|
65
|
+
export function nodeDegreeCentrality(
|
|
66
|
+
graph: Graph,
|
|
67
|
+
nodeId: string | number,
|
|
68
|
+
options: CentralityOptions = {},
|
|
69
|
+
): number {
|
|
70
|
+
if (!graph.hasNode(nodeId)) {
|
|
71
|
+
throw new Error(`Node ${String(nodeId)} not found in graph`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const {nodeCount} = graph;
|
|
75
|
+
let degree: number;
|
|
76
|
+
|
|
77
|
+
if (graph.isDirected && options.mode) {
|
|
78
|
+
switch (options.mode) {
|
|
79
|
+
case "in": {
|
|
80
|
+
degree = graph.inDegree(nodeId);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "out": {
|
|
84
|
+
degree = graph.outDegree(nodeId);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "total": {
|
|
88
|
+
degree = graph.degree(nodeId);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default: {
|
|
92
|
+
degree = graph.degree(nodeId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
degree = graph.degree(nodeId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Apply normalization if requested
|
|
100
|
+
const maxPossibleDegree = nodeCount - 1;
|
|
101
|
+
|
|
102
|
+
return options.normalized && maxPossibleDegree > 0 ?
|
|
103
|
+
degree / maxPossibleDegree :
|
|
104
|
+
degree;
|
|
105
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centrality algorithms
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type {BetweennessCentralityOptions} from "./betweenness.js";
|
|
6
|
+
export {betweennessCentrality, edgeBetweennessCentrality, nodeBetweennessCentrality} from "./betweenness.js";
|
|
7
|
+
export type {ClosenessCentralityOptions} from "./closeness.js";
|
|
8
|
+
export {closenessCentrality, nodeClosenessCentrality, nodeWeightedClosenessCentrality, weightedClosenessCentrality} from "./closeness.js";
|
|
9
|
+
export {degreeCentrality, nodeDegreeCentrality} from "./degree.js";
|
|
10
|
+
export type {PageRankOptions, PageRankResult} from "./pagerank.js";
|
|
11
|
+
export {pageRank, pageRankCentrality, personalizedPageRank, topPageRankNodes} from "./pagerank.js";
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type {Graph} from "../../core/graph.js";
|
|
2
|
+
import type {NodeId} from "../../types/index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PageRank algorithm implementation
|
|
6
|
+
*
|
|
7
|
+
* Measures the importance of nodes in a directed graph based on the
|
|
8
|
+
* structure of incoming links. Originally designed for ranking web pages.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PageRank algorithm options
|
|
13
|
+
*/
|
|
14
|
+
export interface PageRankOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Damping factor (probability of following a link) (default: 0.85)
|
|
17
|
+
*/
|
|
18
|
+
dampingFactor?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Maximum number of iterations (default: 100)
|
|
21
|
+
*/
|
|
22
|
+
maxIterations?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Convergence tolerance (default: 1e-6)
|
|
25
|
+
*/
|
|
26
|
+
tolerance?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Initial PageRank values for nodes (default: uniform distribution)
|
|
29
|
+
*/
|
|
30
|
+
initialRanks?: Map<NodeId, number>;
|
|
31
|
+
/**
|
|
32
|
+
* Personalization vector for Personalized PageRank (default: null)
|
|
33
|
+
*/
|
|
34
|
+
personalization?: Map<NodeId, number>;
|
|
35
|
+
/**
|
|
36
|
+
* Weight attribute for weighted PageRank (default: null = unweighted)
|
|
37
|
+
*/
|
|
38
|
+
weight?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* PageRank algorithm result
|
|
43
|
+
*/
|
|
44
|
+
export interface PageRankResult {
|
|
45
|
+
/**
|
|
46
|
+
* PageRank scores for each node
|
|
47
|
+
*/
|
|
48
|
+
ranks: Record<string, number>;
|
|
49
|
+
/**
|
|
50
|
+
* Number of iterations until convergence
|
|
51
|
+
*/
|
|
52
|
+
iterations: number;
|
|
53
|
+
/**
|
|
54
|
+
* Whether the algorithm converged
|
|
55
|
+
*/
|
|
56
|
+
converged: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate PageRank for all nodes in the graph
|
|
61
|
+
*/
|
|
62
|
+
export function pageRank(
|
|
63
|
+
graph: Graph,
|
|
64
|
+
options: PageRankOptions = {},
|
|
65
|
+
): PageRankResult {
|
|
66
|
+
const {
|
|
67
|
+
dampingFactor = 0.85,
|
|
68
|
+
maxIterations = 100,
|
|
69
|
+
tolerance = 1e-6,
|
|
70
|
+
initialRanks,
|
|
71
|
+
personalization,
|
|
72
|
+
weight,
|
|
73
|
+
} = options;
|
|
74
|
+
|
|
75
|
+
if (!graph.isDirected) {
|
|
76
|
+
throw new Error("PageRank requires a directed graph");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (dampingFactor < 0 || dampingFactor > 1) {
|
|
80
|
+
throw new Error("Damping factor must be between 0 and 1");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
84
|
+
const n = nodes.length;
|
|
85
|
+
|
|
86
|
+
if (n === 0) {
|
|
87
|
+
return {ranks: {}, iterations: 0, converged: true};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Initialize PageRank values
|
|
91
|
+
let ranks = new Map<NodeId, number>();
|
|
92
|
+
if (initialRanks) {
|
|
93
|
+
for (const nodeId of nodes) {
|
|
94
|
+
ranks.set(nodeId, initialRanks.get(nodeId) ?? 1 / n);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
for (const nodeId of nodes) {
|
|
98
|
+
ranks.set(nodeId, 1 / n);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Normalize initial ranks
|
|
103
|
+
normalizeRanks(ranks);
|
|
104
|
+
|
|
105
|
+
// Calculate out-degrees and handle dangling nodes
|
|
106
|
+
const outDegrees = new Map<NodeId, number>();
|
|
107
|
+
const outWeights = new Map<NodeId, number>();
|
|
108
|
+
const danglingNodes: NodeId[] = [];
|
|
109
|
+
|
|
110
|
+
for (const nodeId of nodes) {
|
|
111
|
+
let outDegree = 0;
|
|
112
|
+
let totalOutWeight = 0;
|
|
113
|
+
|
|
114
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
115
|
+
outDegree++;
|
|
116
|
+
if (weight) {
|
|
117
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
118
|
+
const edgeWeight = edge?.weight ?? 1;
|
|
119
|
+
totalOutWeight += edgeWeight;
|
|
120
|
+
} else {
|
|
121
|
+
totalOutWeight += 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
outDegrees.set(nodeId, outDegree);
|
|
126
|
+
outWeights.set(nodeId, totalOutWeight);
|
|
127
|
+
|
|
128
|
+
if (outDegree === 0) {
|
|
129
|
+
danglingNodes.push(nodeId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Setup personalization vector
|
|
134
|
+
let personalVector: Map<NodeId, number> | null = null;
|
|
135
|
+
if (personalization) {
|
|
136
|
+
personalVector = new Map(personalization);
|
|
137
|
+
normalizeRanks(personalVector);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let converged = false;
|
|
141
|
+
let iteration = 0;
|
|
142
|
+
|
|
143
|
+
// Power iteration
|
|
144
|
+
for (iteration = 0; iteration < maxIterations; iteration++) {
|
|
145
|
+
const newRanks = new Map<NodeId, number>();
|
|
146
|
+
|
|
147
|
+
// Initialize with teleportation probability
|
|
148
|
+
for (const nodeId of nodes) {
|
|
149
|
+
if (personalVector) {
|
|
150
|
+
newRanks.set(nodeId, (1 - dampingFactor) * (personalVector.get(nodeId) ?? 0));
|
|
151
|
+
} else {
|
|
152
|
+
newRanks.set(nodeId, (1 - dampingFactor) / n);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle dangling nodes
|
|
157
|
+
let danglingSum = 0;
|
|
158
|
+
for (const danglingNode of danglingNodes) {
|
|
159
|
+
danglingSum += ranks.get(danglingNode) ?? 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (danglingSum > 0) {
|
|
163
|
+
const danglingContribution = dampingFactor * danglingSum / n;
|
|
164
|
+
for (const nodeId of nodes) {
|
|
165
|
+
const currentRank = newRanks.get(nodeId) ?? 0;
|
|
166
|
+
if (personalVector) {
|
|
167
|
+
const personalContrib = danglingContribution * (personalVector.get(nodeId) ?? 0);
|
|
168
|
+
newRanks.set(nodeId, currentRank + personalContrib);
|
|
169
|
+
} else {
|
|
170
|
+
newRanks.set(nodeId, currentRank + danglingContribution);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Propagate rank from each node to its neighbors
|
|
176
|
+
for (const nodeId of nodes) {
|
|
177
|
+
const currentRank = ranks.get(nodeId) ?? 0;
|
|
178
|
+
const nodeOutWeight = outWeights.get(nodeId) ?? 0;
|
|
179
|
+
|
|
180
|
+
if (nodeOutWeight > 0) {
|
|
181
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
182
|
+
let edgeWeight = 1;
|
|
183
|
+
if (weight) {
|
|
184
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
185
|
+
edgeWeight = edge?.weight ?? 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const contribution = dampingFactor * currentRank * (edgeWeight / nodeOutWeight);
|
|
189
|
+
const neighborRank = newRanks.get(neighbor) ?? 0;
|
|
190
|
+
newRanks.set(neighbor, neighborRank + contribution);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check for convergence
|
|
196
|
+
let maxDiff = 0;
|
|
197
|
+
for (const nodeId of nodes) {
|
|
198
|
+
const oldRank = ranks.get(nodeId) ?? 0;
|
|
199
|
+
const newRank = newRanks.get(nodeId) ?? 0;
|
|
200
|
+
maxDiff = Math.max(maxDiff, Math.abs(newRank - oldRank));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ranks = newRanks;
|
|
204
|
+
|
|
205
|
+
if (maxDiff < tolerance) {
|
|
206
|
+
converged = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert to string-keyed record
|
|
212
|
+
const result: Record<string, number> = {};
|
|
213
|
+
for (const nodeId of nodes) {
|
|
214
|
+
result[String(nodeId)] = ranks.get(nodeId) ?? 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
ranks: result,
|
|
219
|
+
iterations: iteration + 1,
|
|
220
|
+
converged,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculate Personalized PageRank for a specific set of source nodes
|
|
226
|
+
*/
|
|
227
|
+
export function personalizedPageRank(
|
|
228
|
+
graph: Graph,
|
|
229
|
+
personalNodes: NodeId[],
|
|
230
|
+
options: Omit<PageRankOptions, "personalization"> = {},
|
|
231
|
+
): PageRankResult {
|
|
232
|
+
const nodes = Array.from(graph.nodes()).map((node) => node.id);
|
|
233
|
+
const personalization = new Map<NodeId, number>();
|
|
234
|
+
|
|
235
|
+
// Initialize personalization vector
|
|
236
|
+
for (const nodeId of nodes) {
|
|
237
|
+
personalization.set(nodeId, 0);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Set equal probability for personal nodes
|
|
241
|
+
const personalValue = 1 / personalNodes.length;
|
|
242
|
+
for (const personalNode of personalNodes) {
|
|
243
|
+
if (!graph.hasNode(personalNode)) {
|
|
244
|
+
throw new Error(`Personal node ${String(personalNode)} not found in graph`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
personalization.set(personalNode, personalValue);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return pageRank(graph, {
|
|
251
|
+
... options,
|
|
252
|
+
personalization,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Calculate PageRank centrality (normalized PageRank scores)
|
|
258
|
+
*/
|
|
259
|
+
export function pageRankCentrality(
|
|
260
|
+
graph: Graph,
|
|
261
|
+
options: PageRankOptions = {},
|
|
262
|
+
): Record<string, number> {
|
|
263
|
+
const result = pageRank(graph, options);
|
|
264
|
+
return result.ranks;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the top-k nodes by PageRank score
|
|
269
|
+
*/
|
|
270
|
+
export function topPageRankNodes(
|
|
271
|
+
graph: Graph,
|
|
272
|
+
k: number,
|
|
273
|
+
options: PageRankOptions = {},
|
|
274
|
+
): {node: NodeId, rank: number}[] {
|
|
275
|
+
const result = pageRank(graph, options);
|
|
276
|
+
|
|
277
|
+
const nodeRanks: {node: NodeId, rank: number}[] = [];
|
|
278
|
+
for (const [nodeStr, rank] of Object.entries(result.ranks)) {
|
|
279
|
+
nodeRanks.push({node: nodeStr as NodeId, rank});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Sort by rank in descending order
|
|
283
|
+
nodeRanks.sort((a, b) => b.rank - a.rank);
|
|
284
|
+
|
|
285
|
+
return nodeRanks.slice(0, k);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Normalize ranks so they sum to 1
|
|
290
|
+
*/
|
|
291
|
+
function normalizeRanks(ranks: Map<NodeId, number>): void {
|
|
292
|
+
let sum = 0;
|
|
293
|
+
for (const rank of ranks.values()) {
|
|
294
|
+
sum += rank;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (sum > 0) {
|
|
298
|
+
for (const [nodeId, rank] of ranks) {
|
|
299
|
+
ranks.set(nodeId, rank / sum);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|