@graphty/algorithms 1.2.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.
Files changed (230) hide show
  1. package/README.md +490 -336
  2. package/dist/algorithms.d.ts +42 -2
  3. package/dist/algorithms.js +11099 -2
  4. package/dist/algorithms.js.map +1 -1
  5. package/dist/src/algorithms/centrality/betweenness.d.ts +4 -0
  6. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  7. package/dist/src/algorithms/centrality/betweenness.js +124 -170
  8. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  9. package/dist/src/algorithms/centrality/closeness.d.ts +26 -1
  10. package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
  11. package/dist/src/algorithms/centrality/closeness.js +56 -149
  12. package/dist/src/algorithms/centrality/closeness.js.map +1 -1
  13. package/dist/src/algorithms/centrality/degree.js +3 -3
  14. package/dist/src/algorithms/centrality/degree.js.map +1 -1
  15. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +39 -0
  16. package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
  17. package/dist/src/algorithms/centrality/delta-pagerank-simple.js +286 -0
  18. package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
  19. package/dist/src/algorithms/centrality/delta-pagerank.d.ts +80 -0
  20. package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
  21. package/dist/src/algorithms/centrality/delta-pagerank.js +294 -0
  22. package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
  23. package/dist/src/algorithms/centrality/index.d.ts +2 -0
  24. package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
  25. package/dist/src/algorithms/centrality/index.js +1 -0
  26. package/dist/src/algorithms/centrality/index.js.map +1 -1
  27. package/dist/src/algorithms/centrality/pagerank.d.ts +17 -0
  28. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
  29. package/dist/src/algorithms/centrality/pagerank.js +45 -0
  30. package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
  31. package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
  32. package/dist/src/algorithms/community/girvan-newman.js +47 -54
  33. package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
  34. package/dist/src/algorithms/community/index.d.ts +2 -0
  35. package/dist/src/algorithms/community/index.d.ts.map +1 -1
  36. package/dist/src/algorithms/community/index.js.map +1 -1
  37. package/dist/src/algorithms/community/label-propagation.d.ts +13 -3
  38. package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
  39. package/dist/src/algorithms/community/label-propagation.js +50 -35
  40. package/dist/src/algorithms/community/label-propagation.js.map +1 -1
  41. package/dist/src/algorithms/community/leiden.d.ts +3 -2
  42. package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
  43. package/dist/src/algorithms/community/leiden.js +34 -39
  44. package/dist/src/algorithms/community/leiden.js.map +1 -1
  45. package/dist/src/algorithms/community/louvain-optimized.d.ts +112 -0
  46. package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
  47. package/dist/src/algorithms/community/louvain-optimized.js +303 -0
  48. package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
  49. package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
  50. package/dist/src/algorithms/community/louvain.js +17 -76
  51. package/dist/src/algorithms/community/louvain.js.map +1 -1
  52. package/dist/src/algorithms/community/modularity-utils.d.ts +59 -0
  53. package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
  54. package/dist/src/algorithms/community/modularity-utils.js +112 -0
  55. package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
  56. package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
  57. package/dist/src/algorithms/matching/bipartite.js +8 -40
  58. package/dist/src/algorithms/matching/bipartite.js.map +1 -1
  59. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
  60. package/dist/src/algorithms/shortest-path/bellman-ford.js +1 -12
  61. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
  62. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +37 -0
  63. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
  64. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +193 -0
  65. package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
  66. package/dist/src/algorithms/shortest-path/dijkstra.d.ts +5 -2
  67. package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
  68. package/dist/src/algorithms/shortest-path/dijkstra.js +15 -14
  69. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
  70. package/dist/src/algorithms/traversal/bfs-unified.d.ts +29 -0
  71. package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
  72. package/dist/src/algorithms/traversal/bfs-unified.js +333 -0
  73. package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
  74. package/dist/src/algorithms/traversal/bfs-variants.d.ts +54 -0
  75. package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
  76. package/dist/src/algorithms/traversal/bfs-variants.js +368 -0
  77. package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
  78. package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
  79. package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
  80. package/dist/src/algorithms/traversal/bfs.js +5 -182
  81. package/dist/src/algorithms/traversal/bfs.js.map +1 -1
  82. package/dist/src/benchmark-all-algorithms.d.ts +23 -0
  83. package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
  84. package/dist/src/benchmark-all-algorithms.js +462 -0
  85. package/dist/src/benchmark-all-algorithms.js.map +1 -0
  86. package/dist/src/clustering/hierarchical.d.ts +16 -15
  87. package/dist/src/clustering/hierarchical.d.ts.map +1 -1
  88. package/dist/src/clustering/hierarchical.js +209 -223
  89. package/dist/src/clustering/hierarchical.js.map +1 -1
  90. package/dist/src/clustering/index.d.ts +4 -2
  91. package/dist/src/clustering/index.d.ts.map +1 -1
  92. package/dist/src/clustering/index.js +2 -2
  93. package/dist/src/clustering/index.js.map +1 -1
  94. package/dist/src/clustering/k-core.d.ts +16 -15
  95. package/dist/src/clustering/k-core.d.ts.map +1 -1
  96. package/dist/src/clustering/k-core.js +57 -21
  97. package/dist/src/clustering/k-core.js.map +1 -1
  98. package/dist/src/clustering/spectral.d.ts +8 -0
  99. package/dist/src/clustering/spectral.d.ts.map +1 -1
  100. package/dist/src/clustering/spectral.js +40 -21
  101. package/dist/src/clustering/spectral.js.map +1 -1
  102. package/dist/src/core/graph.js +4 -4
  103. package/dist/src/core/graph.js.map +1 -1
  104. package/dist/src/flow/ford-fulkerson.d.ts +17 -11
  105. package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
  106. package/dist/src/flow/ford-fulkerson.js +141 -217
  107. package/dist/src/flow/ford-fulkerson.js.map +1 -1
  108. package/dist/src/flow/min-cut.d.ts +6 -5
  109. package/dist/src/flow/min-cut.d.ts.map +1 -1
  110. package/dist/src/flow/min-cut.js +17 -8
  111. package/dist/src/flow/min-cut.js.map +1 -1
  112. package/dist/src/index.d.ts +1 -0
  113. package/dist/src/index.d.ts.map +1 -1
  114. package/dist/src/index.js +4 -0
  115. package/dist/src/index.js.map +1 -1
  116. package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
  117. package/dist/src/link-prediction/adamic-adar.js +17 -26
  118. package/dist/src/link-prediction/adamic-adar.js.map +1 -1
  119. package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
  120. package/dist/src/link-prediction/common-neighbors.js +7 -11
  121. package/dist/src/link-prediction/common-neighbors.js.map +1 -1
  122. package/dist/src/optimized/bit-packed.d.ts +143 -0
  123. package/dist/src/optimized/bit-packed.d.ts.map +1 -0
  124. package/dist/src/optimized/bit-packed.js +292 -0
  125. package/dist/src/optimized/bit-packed.js.map +1 -0
  126. package/dist/src/optimized/csr-graph.d.ts +86 -0
  127. package/dist/src/optimized/csr-graph.d.ts.map +1 -0
  128. package/dist/src/optimized/csr-graph.js +341 -0
  129. package/dist/src/optimized/csr-graph.js.map +1 -0
  130. package/dist/src/optimized/direction-optimized-bfs.d.ts +74 -0
  131. package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
  132. package/dist/src/optimized/direction-optimized-bfs.js +223 -0
  133. package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
  134. package/dist/src/optimized/graph-adapter.d.ts +62 -0
  135. package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
  136. package/dist/src/optimized/graph-adapter.js +146 -0
  137. package/dist/src/optimized/graph-adapter.js.map +1 -0
  138. package/dist/src/optimized/index.d.ts +14 -0
  139. package/dist/src/optimized/index.d.ts.map +1 -0
  140. package/dist/src/optimized/index.js +28 -0
  141. package/dist/src/optimized/index.js.map +1 -0
  142. package/dist/src/research/grsbm.d.ts.map +1 -1
  143. package/dist/src/research/grsbm.js +6 -14
  144. package/dist/src/research/grsbm.js.map +1 -1
  145. package/dist/src/research/sync.d.ts.map +1 -1
  146. package/dist/src/research/sync.js +3 -29
  147. package/dist/src/research/sync.js.map +1 -1
  148. package/dist/src/research/terahac.d.ts +2 -0
  149. package/dist/src/research/terahac.d.ts.map +1 -1
  150. package/dist/src/research/terahac.js +7 -3
  151. package/dist/src/research/terahac.js.map +1 -1
  152. package/dist/src/types/index.d.ts +8 -0
  153. package/dist/src/types/index.d.ts.map +1 -1
  154. package/dist/src/utils/algorithm-utilities.d.ts +2 -0
  155. package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
  156. package/dist/src/utils/algorithm-utilities.js +2 -0
  157. package/dist/src/utils/algorithm-utilities.js.map +1 -0
  158. package/dist/src/utils/graph-converters.d.ts +79 -0
  159. package/dist/src/utils/graph-converters.d.ts.map +1 -0
  160. package/dist/src/utils/graph-converters.js +196 -0
  161. package/dist/src/utils/graph-converters.js.map +1 -0
  162. package/dist/src/utils/graph-utilities.d.ts +62 -0
  163. package/dist/src/utils/graph-utilities.d.ts.map +1 -0
  164. package/dist/src/utils/graph-utilities.js +148 -0
  165. package/dist/src/utils/graph-utilities.js.map +1 -0
  166. package/dist/src/utils/index.d.ts +5 -0
  167. package/dist/src/utils/index.d.ts.map +1 -0
  168. package/dist/src/utils/index.js +5 -0
  169. package/dist/src/utils/index.js.map +1 -0
  170. package/dist/src/utils/math-utilities.d.ts +52 -0
  171. package/dist/src/utils/math-utilities.d.ts.map +1 -0
  172. package/dist/src/utils/math-utilities.js +123 -0
  173. package/dist/src/utils/math-utilities.js.map +1 -0
  174. package/dist/src/utils/matrix-utilities.d.ts +2 -0
  175. package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
  176. package/dist/src/utils/matrix-utilities.js +2 -0
  177. package/dist/src/utils/matrix-utilities.js.map +1 -0
  178. package/dist/src/utils/optimization-helpers.d.ts +33 -0
  179. package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
  180. package/dist/src/utils/optimization-helpers.js +47 -0
  181. package/dist/src/utils/optimization-helpers.js.map +1 -0
  182. package/package.json +18 -3
  183. package/src/algorithms/centrality/betweenness.ts +167 -199
  184. package/src/algorithms/centrality/closeness.ts +73 -191
  185. package/src/algorithms/centrality/degree.ts +3 -3
  186. package/src/algorithms/centrality/delta-pagerank-simple.ts +366 -0
  187. package/src/algorithms/centrality/delta-pagerank.ts +435 -0
  188. package/src/algorithms/centrality/index.ts +2 -0
  189. package/src/algorithms/centrality/pagerank.ts +66 -0
  190. package/src/algorithms/community/girvan-newman.ts +52 -59
  191. package/src/algorithms/community/index.ts +2 -0
  192. package/src/algorithms/community/label-propagation.ts +64 -35
  193. package/src/algorithms/community/leiden.ts +40 -41
  194. package/src/algorithms/community/louvain-optimized.ts +445 -0
  195. package/src/algorithms/community/louvain.ts +24 -104
  196. package/src/algorithms/community/modularity-utils.ts +146 -0
  197. package/src/algorithms/matching/bipartite.ts +8 -44
  198. package/src/algorithms/shortest-path/bellman-ford.ts +1 -14
  199. package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
  200. package/src/algorithms/shortest-path/dijkstra.ts +17 -15
  201. package/src/algorithms/traversal/bfs-unified.ts +433 -0
  202. package/src/algorithms/traversal/bfs-variants.ts +496 -0
  203. package/src/algorithms/traversal/bfs.ts +10 -228
  204. package/src/benchmark-all-algorithms.ts +548 -0
  205. package/src/clustering/hierarchical.ts +241 -251
  206. package/src/clustering/index.ts +4 -2
  207. package/src/clustering/k-core.ts +87 -39
  208. package/src/clustering/spectral.ts +44 -20
  209. package/src/core/graph.ts +4 -4
  210. package/src/flow/ford-fulkerson.ts +186 -254
  211. package/src/flow/min-cut.ts +21 -11
  212. package/src/index.ts +6 -0
  213. package/src/link-prediction/adamic-adar.ts +17 -30
  214. package/src/link-prediction/common-neighbors.ts +7 -16
  215. package/src/optimized/bit-packed.ts +340 -0
  216. package/src/optimized/csr-graph.ts +455 -0
  217. package/src/optimized/direction-optimized-bfs.ts +291 -0
  218. package/src/optimized/graph-adapter.ts +200 -0
  219. package/src/optimized/index.ts +36 -0
  220. package/src/research/grsbm.ts +7 -16
  221. package/src/research/sync.ts +3 -32
  222. package/src/research/terahac.ts +10 -2
  223. package/src/types/index.ts +8 -0
  224. package/src/utils/algorithm-utilities.ts +2 -0
  225. package/src/utils/graph-converters.ts +241 -0
  226. package/src/utils/graph-utilities.ts +199 -0
  227. package/src/utils/index.ts +4 -0
  228. package/src/utils/math-utilities.ts +140 -0
  229. package/src/utils/matrix-utilities.ts +2 -0
  230. 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 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
36
+ * Calculate closeness centrality from distances
49
37
  */
50
- export function nodeClosenessCentrality(
51
- graph: Graph,
52
- node: NodeId,
53
- options: ClosenessCentralityOptions = {},
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 Array.from(distances)) {
70
- if (targetNode !== node && distance > 0) {
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 Array.from(distances)) {
80
- if (targetNode !== node) {
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
- const n = Array.from(graph.nodes()).length;
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
- * Single-source shortest path lengths using BFS (for unweighted graphs)
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 singleSourceShortestPathLengths(
111
+ export function closenessCentrality(
112
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
- }
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
- // Explore neighbors
137
- for (const neighbor of Array.from(graph.neighbors(current.node))) {
138
- if (!visited.has(neighbor)) {
139
- const newDistance = current.distance + 1;
118
+ for (const sourceNode of nodes) {
119
+ centrality[String(sourceNode)] = nodeClosenessCentrality(graph, sourceNode, options);
120
+ }
140
121
 
141
- // Skip if beyond cutoff
142
- if (cutoff !== undefined && newDistance > cutoff) {
143
- continue;
144
- }
122
+ return centrality;
123
+ }
145
124
 
146
- visited.add(neighbor);
147
- distances.set(neighbor, newDistance);
148
- queue.push({node: neighbor, distance: newDistance});
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
- return distances;
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
- 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 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 = graph.isDirected ? nodeCount - 1 : nodeCount - 1;
21
+ // Calculate normalization factor (same for both directed and undirected graphs)
22
+ const maxPossibleDegree = nodeCount - 1;
23
23
 
24
- for (const node of Array.from(graph.nodes())) {
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
+ }