@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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/dist/src/algorithms/centrality/betweenness.d.ts +34 -0
  4. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -0
  5. package/dist/src/algorithms/centrality/betweenness.js +223 -0
  6. package/dist/src/algorithms/centrality/betweenness.js.map +1 -0
  7. package/dist/src/algorithms/centrality/closeness.d.ts +43 -0
  8. package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -0
  9. package/dist/src/algorithms/centrality/closeness.js +202 -0
  10. package/dist/src/algorithms/centrality/closeness.js.map +1 -0
  11. package/dist/src/algorithms/centrality/degree.d.ts +17 -0
  12. package/dist/src/algorithms/centrality/degree.d.ts.map +1 -0
  13. package/dist/src/algorithms/centrality/degree.js +86 -0
  14. package/dist/src/algorithms/centrality/degree.js.map +1 -0
  15. package/dist/src/algorithms/centrality/index.d.ts +11 -0
  16. package/dist/src/algorithms/centrality/index.d.ts.map +1 -0
  17. package/dist/src/algorithms/centrality/index.js +8 -0
  18. package/dist/src/algorithms/centrality/index.js.map +1 -0
  19. package/dist/src/algorithms/centrality/pagerank.d.ts +74 -0
  20. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -0
  21. package/dist/src/algorithms/centrality/pagerank.js +191 -0
  22. package/dist/src/algorithms/centrality/pagerank.js.map +1 -0
  23. package/dist/src/algorithms/components/connected.d.ts +58 -0
  24. package/dist/src/algorithms/components/connected.d.ts.map +1 -0
  25. package/dist/src/algorithms/components/connected.js +234 -0
  26. package/dist/src/algorithms/components/connected.js.map +1 -0
  27. package/dist/src/algorithms/components/index.d.ts +5 -0
  28. package/dist/src/algorithms/components/index.d.ts.map +1 -0
  29. package/dist/src/algorithms/components/index.js +5 -0
  30. package/dist/src/algorithms/components/index.js.map +1 -0
  31. package/dist/src/algorithms/index.d.ts +8 -0
  32. package/dist/src/algorithms/index.d.ts.map +1 -0
  33. package/dist/src/algorithms/index.js +12 -0
  34. package/dist/src/algorithms/index.js.map +1 -0
  35. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts +52 -0
  36. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -0
  37. package/dist/src/algorithms/shortest-path/bellman-ford.js +131 -0
  38. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -0
  39. package/dist/src/algorithms/shortest-path/dijkstra.d.ts +26 -0
  40. package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -0
  41. package/dist/src/algorithms/shortest-path/dijkstra.js +199 -0
  42. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -0
  43. package/dist/src/algorithms/shortest-path/index.d.ts +7 -0
  44. package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -0
  45. package/dist/src/algorithms/shortest-path/index.js +6 -0
  46. package/dist/src/algorithms/shortest-path/index.js.map +1 -0
  47. package/dist/src/algorithms/traversal/bfs.d.ts +25 -0
  48. package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -0
  49. package/dist/src/algorithms/traversal/bfs.js +189 -0
  50. package/dist/src/algorithms/traversal/bfs.js.map +1 -0
  51. package/dist/src/algorithms/traversal/dfs.d.ts +32 -0
  52. package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -0
  53. package/dist/src/algorithms/traversal/dfs.js +259 -0
  54. package/dist/src/algorithms/traversal/dfs.js.map +1 -0
  55. package/dist/src/algorithms/traversal/index.d.ts +7 -0
  56. package/dist/src/algorithms/traversal/index.d.ts.map +1 -0
  57. package/dist/src/algorithms/traversal/index.js +6 -0
  58. package/dist/src/algorithms/traversal/index.js.map +1 -0
  59. package/dist/src/core/graph.d.ts +100 -0
  60. package/dist/src/core/graph.d.ts.map +1 -0
  61. package/dist/src/core/graph.js +276 -0
  62. package/dist/src/core/graph.js.map +1 -0
  63. package/dist/src/data-structures/index.d.ts +6 -0
  64. package/dist/src/data-structures/index.d.ts.map +1 -0
  65. package/dist/src/data-structures/index.js +6 -0
  66. package/dist/src/data-structures/index.js.map +1 -0
  67. package/dist/src/data-structures/priority-queue.d.ts +60 -0
  68. package/dist/src/data-structures/priority-queue.d.ts.map +1 -0
  69. package/dist/src/data-structures/priority-queue.js +152 -0
  70. package/dist/src/data-structures/priority-queue.js.map +1 -0
  71. package/dist/src/data-structures/union-find.d.ts +54 -0
  72. package/dist/src/data-structures/union-find.d.ts.map +1 -0
  73. package/dist/src/data-structures/union-find.js +145 -0
  74. package/dist/src/data-structures/union-find.js.map +1 -0
  75. package/dist/src/index.d.ts +11 -0
  76. package/dist/src/index.d.ts.map +1 -0
  77. package/dist/src/index.js +13 -0
  78. package/dist/src/index.js.map +1 -0
  79. package/dist/src/types/index.d.ts +74 -0
  80. package/dist/src/types/index.d.ts.map +1 -0
  81. package/dist/src/types/index.js +5 -0
  82. package/dist/src/types/index.js.map +1 -0
  83. package/dist/test/browser/basic.test.d.ts +2 -0
  84. package/dist/test/browser/basic.test.d.ts.map +1 -0
  85. package/dist/test/browser/basic.test.js +70 -0
  86. package/dist/test/browser/basic.test.js.map +1 -0
  87. package/dist/test/unit/bellman-ford.test.d.ts +2 -0
  88. package/dist/test/unit/bellman-ford.test.d.ts.map +1 -0
  89. package/dist/test/unit/bellman-ford.test.js +231 -0
  90. package/dist/test/unit/bellman-ford.test.js.map +1 -0
  91. package/dist/test/unit/betweenness-centrality.test.d.ts +2 -0
  92. package/dist/test/unit/betweenness-centrality.test.d.ts.map +1 -0
  93. package/dist/test/unit/betweenness-centrality.test.js +311 -0
  94. package/dist/test/unit/betweenness-centrality.test.js.map +1 -0
  95. package/dist/test/unit/bfs.test.d.ts +2 -0
  96. package/dist/test/unit/bfs.test.d.ts.map +1 -0
  97. package/dist/test/unit/bfs.test.js +277 -0
  98. package/dist/test/unit/bfs.test.js.map +1 -0
  99. package/dist/test/unit/closeness.test.d.ts +2 -0
  100. package/dist/test/unit/closeness.test.d.ts.map +1 -0
  101. package/dist/test/unit/closeness.test.js +266 -0
  102. package/dist/test/unit/closeness.test.js.map +1 -0
  103. package/dist/test/unit/connected-components.test.d.ts +2 -0
  104. package/dist/test/unit/connected-components.test.d.ts.map +1 -0
  105. package/dist/test/unit/connected-components.test.js +355 -0
  106. package/dist/test/unit/connected-components.test.js.map +1 -0
  107. package/dist/test/unit/degree-centrality.test.d.ts +2 -0
  108. package/dist/test/unit/degree-centrality.test.d.ts.map +1 -0
  109. package/dist/test/unit/degree-centrality.test.js +260 -0
  110. package/dist/test/unit/degree-centrality.test.js.map +1 -0
  111. package/dist/test/unit/dfs.test.d.ts +2 -0
  112. package/dist/test/unit/dfs.test.d.ts.map +1 -0
  113. package/dist/test/unit/dfs.test.js +350 -0
  114. package/dist/test/unit/dfs.test.js.map +1 -0
  115. package/dist/test/unit/dijkstra.test.d.ts +2 -0
  116. package/dist/test/unit/dijkstra.test.d.ts.map +1 -0
  117. package/dist/test/unit/dijkstra.test.js +311 -0
  118. package/dist/test/unit/dijkstra.test.js.map +1 -0
  119. package/dist/test/unit/graph.test.d.ts +2 -0
  120. package/dist/test/unit/graph.test.d.ts.map +1 -0
  121. package/dist/test/unit/graph.test.js +175 -0
  122. package/dist/test/unit/graph.test.js.map +1 -0
  123. package/dist/test/unit/pagerank.test.d.ts +2 -0
  124. package/dist/test/unit/pagerank.test.d.ts.map +1 -0
  125. package/dist/test/unit/pagerank.test.js +281 -0
  126. package/dist/test/unit/pagerank.test.js.map +1 -0
  127. package/dist/test/unit/priority-queue.test.d.ts +2 -0
  128. package/dist/test/unit/priority-queue.test.d.ts.map +1 -0
  129. package/dist/test/unit/priority-queue.test.js +189 -0
  130. package/dist/test/unit/priority-queue.test.js.map +1 -0
  131. package/dist/test/unit/union-find.test.d.ts +2 -0
  132. package/dist/test/unit/union-find.test.d.ts.map +1 -0
  133. package/dist/test/unit/union-find.test.js +245 -0
  134. package/dist/test/unit/union-find.test.js.map +1 -0
  135. package/package.json +100 -0
  136. package/src/algorithms/centrality/betweenness.ts +303 -0
  137. package/src/algorithms/centrality/closeness.ts +296 -0
  138. package/src/algorithms/centrality/degree.ts +105 -0
  139. package/src/algorithms/centrality/index.ts +11 -0
  140. package/src/algorithms/centrality/pagerank.ts +302 -0
  141. package/src/algorithms/components/connected.ts +298 -0
  142. package/src/algorithms/components/index.ts +17 -0
  143. package/src/algorithms/index.ts +15 -0
  144. package/src/algorithms/shortest-path/bellman-ford.ts +214 -0
  145. package/src/algorithms/shortest-path/dijkstra.ts +256 -0
  146. package/src/algorithms/shortest-path/index.ts +7 -0
  147. package/src/algorithms/traversal/bfs.ts +236 -0
  148. package/src/algorithms/traversal/dfs.ts +367 -0
  149. package/src/algorithms/traversal/index.ts +7 -0
  150. package/src/core/graph.ts +326 -0
  151. package/src/data-structures/index.ts +6 -0
  152. package/src/data-structures/priority-queue.ts +180 -0
  153. package/src/data-structures/union-find.ts +170 -0
  154. package/src/index.ts +35 -0
  155. 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
+ }