@graphty/algorithms 1.7.3 → 1.8.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 (87) hide show
  1. package/dist/algorithms.d.ts +2 -37
  2. package/dist/algorithms.js +736 -223
  3. package/dist/algorithms.js.map +1 -1
  4. package/dist/algorithms.standalone.js +24524 -0
  5. package/dist/algorithms.standalone.js.map +1 -0
  6. package/dist/src/algorithms/centrality/betweenness.d.ts +16 -11
  7. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
  8. package/dist/src/algorithms/centrality/betweenness.js +17 -0
  9. package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
  10. package/dist/src/core/graph.d.ts +10 -0
  11. package/dist/src/core/graph.d.ts.map +1 -1
  12. package/dist/src/core/graph.js +31 -2
  13. package/dist/src/core/graph.js.map +1 -1
  14. package/dist/src/index.d.ts +10 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +7 -0
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/indexed/accelerator.d.ts +161 -0
  19. package/dist/src/indexed/accelerator.d.ts.map +1 -0
  20. package/dist/src/indexed/accelerator.js +60 -0
  21. package/dist/src/indexed/accelerator.js.map +1 -0
  22. package/dist/src/indexed/bfs.d.ts +28 -0
  23. package/dist/src/indexed/bfs.d.ts.map +1 -0
  24. package/dist/src/indexed/bfs.js +39 -0
  25. package/dist/src/indexed/bfs.js.map +1 -0
  26. package/dist/src/indexed/common-neighbors.d.ts +17 -0
  27. package/dist/src/indexed/common-neighbors.d.ts.map +1 -0
  28. package/dist/src/indexed/common-neighbors.js +41 -0
  29. package/dist/src/indexed/common-neighbors.js.map +1 -0
  30. package/dist/src/indexed/components.d.ts +28 -0
  31. package/dist/src/indexed/components.d.ts.map +1 -0
  32. package/dist/src/indexed/components.js +58 -0
  33. package/dist/src/indexed/components.js.map +1 -0
  34. package/dist/src/indexed/dijkstra.d.ts +63 -0
  35. package/dist/src/indexed/dijkstra.d.ts.map +1 -0
  36. package/dist/src/indexed/dijkstra.js +98 -0
  37. package/dist/src/indexed/dijkstra.js.map +1 -0
  38. package/dist/src/indexed/index.d.ts +22 -0
  39. package/dist/src/indexed/index.d.ts.map +1 -0
  40. package/dist/src/indexed/index.js +22 -0
  41. package/dist/src/indexed/index.js.map +1 -0
  42. package/dist/src/indexed/mst.d.ts +26 -0
  43. package/dist/src/indexed/mst.d.ts.map +1 -0
  44. package/dist/src/indexed/mst.js +48 -0
  45. package/dist/src/indexed/mst.js.map +1 -0
  46. package/dist/src/indexed/pagerank.d.ts +30 -0
  47. package/dist/src/indexed/pagerank.d.ts.map +1 -0
  48. package/dist/src/indexed/pagerank.js +50 -0
  49. package/dist/src/indexed/pagerank.js.map +1 -0
  50. package/dist/src/indexed/structures/arc-source.d.ts +13 -0
  51. package/dist/src/indexed/structures/arc-source.d.ts.map +1 -0
  52. package/dist/src/indexed/structures/arc-source.js +25 -0
  53. package/dist/src/indexed/structures/arc-source.js.map +1 -0
  54. package/dist/src/indexed/structures/index.d.ts +9 -0
  55. package/dist/src/indexed/structures/index.d.ts.map +1 -0
  56. package/dist/src/indexed/structures/index.js +9 -0
  57. package/dist/src/indexed/structures/index.js.map +1 -0
  58. package/dist/src/indexed/structures/min-heap.d.ts +44 -0
  59. package/dist/src/indexed/structures/min-heap.d.ts.map +1 -0
  60. package/dist/src/indexed/structures/min-heap.js +112 -0
  61. package/dist/src/indexed/structures/min-heap.js.map +1 -0
  62. package/dist/src/indexed/structures/union-find.d.ts +39 -0
  63. package/dist/src/indexed/structures/union-find.d.ts.map +1 -0
  64. package/dist/src/indexed/structures/union-find.js +70 -0
  65. package/dist/src/indexed/structures/union-find.js.map +1 -0
  66. package/dist/src/indexed/to-snapshot.d.ts +38 -0
  67. package/dist/src/indexed/to-snapshot.d.ts.map +1 -0
  68. package/dist/src/indexed/to-snapshot.js +58 -0
  69. package/dist/src/indexed/to-snapshot.js.map +1 -0
  70. package/dist/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +7 -3
  72. package/src/algorithms/centrality/betweenness.ts +36 -11
  73. package/src/core/graph.ts +35 -2
  74. package/src/index.ts +46 -0
  75. package/src/indexed/accelerator.ts +225 -0
  76. package/src/indexed/bfs.ts +57 -0
  77. package/src/indexed/common-neighbors.ts +46 -0
  78. package/src/indexed/components.ts +78 -0
  79. package/src/indexed/dijkstra.ts +134 -0
  80. package/src/indexed/index.ts +28 -0
  81. package/src/indexed/mst.ts +62 -0
  82. package/src/indexed/pagerank.ts +73 -0
  83. package/src/indexed/structures/arc-source.ts +25 -0
  84. package/src/indexed/structures/index.ts +9 -0
  85. package/src/indexed/structures/min-heap.ts +122 -0
  86. package/src/indexed/structures/union-find.ts +74 -0
  87. package/src/indexed/to-snapshot.ts +78 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphty/algorithms",
3
- "version": "1.7.3",
3
+ "version": "1.8.1",
4
4
  "description": "Graph algorithms library for browser environments implemented in TypeScript",
5
5
  "author": "Adam Powers <apowers@ato.ms>",
6
6
  "main": "dist/algorithms.js",
@@ -88,7 +88,11 @@
88
88
  "vitest": "^3.2.4"
89
89
  },
90
90
  "dependencies": {
91
- "typedfastbitset": "^0.6.1"
91
+ "typedfastbitset": "^0.6.1",
92
+ "@graphty/graph-format": "^1.0.2"
93
+ },
94
+ "peerDependencies": {
95
+ "@graphty/graph-format": "^1.0.0"
92
96
  },
93
97
  "scripts": {
94
98
  "test": "vitest",
@@ -114,7 +118,7 @@
114
118
  "benchmark:validation": "tsx src/optimized/benchmark-validation.ts",
115
119
  "benchmark:comprehensive": "tsx src/optimized/benchmark-comprehensive.ts",
116
120
  "memory:profile": "tsx test/helpers/memory-profiler.ts",
117
- "lint": "eslint && tsc --noEmit",
121
+ "lint": "eslint && tsc --noEmit && tsc -p tsconfig.typecheck.json",
118
122
  "lint:fix": "eslint --fix",
119
123
  "lint:knip": "cd .. && pnpm run lint:knip -- --workspace algorithms",
120
124
  "typecheck": "tsc --noEmit",
@@ -10,21 +10,43 @@ import { bfsWithPathCounting } from "../traversal/bfs-variants.js";
10
10
  */
11
11
 
12
12
  /**
13
- * Betweenness centrality options
13
+ * Betweenness centrality options.
14
+ *
15
+ * Every member is `readonly` and carries `| undefined`: the GPU package compiles this declaration
16
+ * a second time under `exactOptionalPropertyTypes`, where `sources?: readonly number[]` and
17
+ * `sources?: readonly number[] | undefined` are different types (plan decision PD-6).
14
18
  */
15
19
  export interface BetweennessCentralityOptions {
20
+ /** Whether to normalize the centrality values (default: false) */
21
+ readonly normalized?: boolean | undefined;
22
+ /** Whether to use endpoints in path counting (default: false) */
23
+ readonly endpoints?: boolean | undefined;
24
+ /** Whether to use optimized BFS implementation for large graphs */
25
+ readonly optimized?: boolean | undefined;
16
26
  /**
17
- * Whether to normalize the centrality values (default: false)
18
- */
19
- normalized?: boolean;
20
- /**
21
- * Whether to use endpoints in path counting (default: false)
27
+ * Sampled betweenness: the source NODE INDICES to run from. Indices are meaningful only against
28
+ * a `GraphSnapshot`, so the legacy `Graph`-taking entry points below THROW when this is set.
22
29
  */
23
- endpoints?: boolean;
24
- /**
25
- * Whether to use optimized BFS implementation for large graphs
26
- */
27
- optimized?: boolean;
30
+ readonly sources?: readonly number[] | undefined;
31
+ /** Sampled betweenness: how many sources to draw when `sources` is not given. */
32
+ readonly k?: number | undefined;
33
+ }
34
+
35
+ /**
36
+ * Refuse the index-space options on a path that has no index space.
37
+ *
38
+ * Ignoring them would be the expensive silence: a caller asking for a 128-source sample would get
39
+ * an exact all-sources run, correct and a thousand times too slow, with no signal at all.
40
+ * @param options - The caller's options
41
+ * @param fn - The entry point's name, for the message
42
+ */
43
+ function rejectIndexOptions(options: BetweennessCentralityOptions, fn: string): void {
44
+ if (options.sources !== undefined || options.k !== undefined) {
45
+ throw new Error(
46
+ `${fn}: 'sources' and 'k' are node INDICES and are meaningful only against a GraphSnapshot; ` +
47
+ "call accelerated(accelerator).betweennessCentrality(snapshot, options) instead",
48
+ );
49
+ }
28
50
  }
29
51
 
30
52
  /**
@@ -205,6 +227,7 @@ export function betweennessCentrality(
205
227
  graph: Graph,
206
228
  options: BetweennessCentralityOptions = {},
207
229
  ): Record<string, number> {
230
+ rejectIndexOptions(options, "betweennessCentrality");
208
231
  const nodes = Array.from(graph.nodes()).map((node) => node.id);
209
232
  const centrality: Record<string, number> = {};
210
233
 
@@ -250,6 +273,7 @@ export function nodeBetweennessCentrality(
250
273
  targetNode: NodeId,
251
274
  options: BetweennessCentralityOptions = {},
252
275
  ): number {
276
+ rejectIndexOptions(options, "nodeBetweennessCentrality");
253
277
  if (!graph.hasNode(targetNode)) {
254
278
  throw new Error(`Node ${String(targetNode)} not found in graph`);
255
279
  }
@@ -268,6 +292,7 @@ export function edgeBetweennessCentrality(
268
292
  graph: Graph,
269
293
  options: BetweennessCentralityOptions = {},
270
294
  ): Map<string, number> {
295
+ rejectIndexOptions(options, "edgeBetweennessCentrality");
271
296
  const nodes = Array.from(graph.nodes()).map((node) => node.id);
272
297
  const edgeCentrality = new Map<string, number>();
273
298
 
package/src/core/graph.ts CHANGED
@@ -1,5 +1,16 @@
1
1
  import type { Edge, GraphConfig, Node, NodeId } from "../types/index.js";
2
2
 
3
+ /**
4
+ * True when an undirected edge seen from `source` is the mirror of one already yielded from
5
+ * `target`: ids of one type compare by value, ids of different types by their type name.
6
+ * @param source - The node the edge is read from
7
+ * @param target - The node the edge points to
8
+ * @returns True when this side of the edge is the duplicate
9
+ */
10
+ function isMirror(source: NodeId, target: NodeId): boolean {
11
+ return typeof source === typeof target ? source > target : typeof source > typeof target;
12
+ }
13
+
3
14
  /**
4
15
  * Core Graph data structure for the Graphty Algorithms library
5
16
  *
@@ -12,6 +23,7 @@ export class Graph {
12
23
  private incomingEdges: Map<NodeId, Map<NodeId, Edge>>; // For directed graphs
13
24
  private config: GraphConfig;
14
25
  private edgeCount: number;
26
+ private mutations: number;
15
27
 
16
28
  /**
17
29
  * Creates a new Graph instance.
@@ -28,6 +40,7 @@ export class Graph {
28
40
  this.adjacencyList = new Map();
29
41
  this.incomingEdges = new Map();
30
42
  this.edgeCount = 0;
43
+ this.mutations = 0;
31
44
  }
32
45
 
33
46
  /**
@@ -43,6 +56,8 @@ export class Graph {
43
56
  if (this.config.directed) {
44
57
  this.incomingEdges.set(id, new Map());
45
58
  }
59
+
60
+ this.mutations++;
46
61
  }
47
62
  }
48
63
 
@@ -88,6 +103,7 @@ export class Graph {
88
103
  this.incomingEdges.delete(id);
89
104
  }
90
105
 
106
+ this.mutations++;
91
107
  return true;
92
108
  }
93
109
 
@@ -142,6 +158,7 @@ export class Graph {
142
158
  }
143
159
 
144
160
  this.edgeCount++;
161
+ this.mutations++;
145
162
  }
146
163
 
147
164
  /**
@@ -172,6 +189,7 @@ export class Graph {
172
189
  }
173
190
 
174
191
  this.edgeCount--;
192
+ this.mutations++;
175
193
  return true;
176
194
  }
177
195
 
@@ -223,6 +241,18 @@ export class Graph {
223
241
  return this.nodeMap.size;
224
242
  }
225
243
 
244
+ /**
245
+ * A counter that increases on every topology change (`addNode` of a new id, `removeNode`,
246
+ * `addEdge`, `removeEdge`, `clear`) and never on a read. Callers memoise derived structures on
247
+ * `(graph, mutationCount)`; graph-format design 14.1 rule 4 names this counter, and `toSnapshot`
248
+ * is its first consumer. It is monotone and is NOT reset by `clear()`: a reset could hand a
249
+ * stale cache entry a matching key.
250
+ * @returns The number of topology changes made to this graph
251
+ */
252
+ get mutationCount(): number {
253
+ return this.mutations;
254
+ }
255
+
226
256
  /**
227
257
  * Get the number of edges in the graph
228
258
  * @returns The total count of edges
@@ -254,8 +284,10 @@ export class Graph {
254
284
  *edges(): IterableIterator<Edge> {
255
285
  for (const [source, edges] of this.adjacencyList) {
256
286
  for (const edge of edges.values()) {
257
- // For undirected graphs, only yield each edge once
258
- if (!this.config.directed && source > edge.target) {
287
+ // For undirected graphs, only yield each edge once. A string never compares
288
+ // greater than a number (both `<` and `>` are false), so mixed-type ids are
289
+ // ordered by their type name first; the mirror is then skipped on exactly one side.
290
+ if (!this.config.directed && isMirror(source, edge.target)) {
259
291
  continue;
260
292
  }
261
293
 
@@ -371,6 +403,7 @@ export class Graph {
371
403
  this.adjacencyList.clear();
372
404
  this.incomingEdges.clear();
373
405
  this.edgeCount = 0;
406
+ this.mutations++;
374
407
  }
375
408
 
376
409
  /**
package/src/index.ts CHANGED
@@ -25,6 +25,12 @@ export type {
25
25
  MSTResult,
26
26
  Node,
27
27
  NodeId,
28
+ // NOTE: this explicit re-export SHADOWS the `PageRankOptions` that `export * from
29
+ // "./algorithms/index.js"` below re-exports from centrality/pagerank.ts, which is the one
30
+ // pageRank() actually takes. The two differ (`alpha` here, `dampingFactor` there), so
31
+ // `const o: PageRankOptions = { alpha: 0.9 }` compiles and is silently ignored. Neither is
32
+ // changed during the dual-API window (graph-format design 14.1 rule 1); this one is removed at
33
+ // 2.0. See design/decisions/2026-09-19-pagerank-options-shadowing.md.
28
34
  PageRankOptions,
29
35
  ShortestPathResult,
30
36
  TraversalOptions,
@@ -43,5 +49,45 @@ export * from "./data-structures/index.js";
43
49
  // Optimized algorithm exports
44
50
  export * from "./optimized/index.js";
45
51
 
52
+ // Index-based implementations over @graphty/graph-format snapshots (graph-format design 14.1 rule 2).
53
+ // A NAMESPACE, not a flat re-export: indexed.pageRank, indexed.dijkstra, indexed.breadthFirstSearch,
54
+ // indexed.connectedComponents and indexed.kruskalMST all collide by name with the legacy functions above.
55
+ export * as indexed from "./indexed/index.js";
56
+
57
+ // The graph-format bridge (graph-format design 14.6 row A1).
58
+ export { toSnapshot } from "./indexed/to-snapshot.js";
59
+
60
+ // The accelerator seam (design/webgpu/webgpu-acceleration-plan.md section 9.2). Flat, NOT through the
61
+ // namespace: the GPU package writes `import type { AlgorithmAccelerator } from "@graphty/algorithms"`.
62
+ export type {
63
+ AcceleratedAlgorithms,
64
+ AlgorithmAccelerator,
65
+ ApspResultLike,
66
+ BellmanFordResultLike,
67
+ BetweennessAcceleratorOptions,
68
+ BfsResultLike,
69
+ CommunityResultLike,
70
+ CorenessResultLike,
71
+ EdgeScoresResultLike,
72
+ HitsOptionsLike,
73
+ HitsResultLike,
74
+ LabelResultLike,
75
+ MstResultLike,
76
+ PageRankResultLike,
77
+ ScoresResultLike,
78
+ SsspResultLike,
79
+ } from "./indexed/accelerator.js";
80
+ export { accelerated } from "./indexed/accelerator.js";
81
+ export type { BfsOptions, BfsResult } from "./indexed/bfs.js";
82
+ export type { CommonNeighborsOptions } from "./indexed/common-neighbors.js";
83
+ export type { LabelResult } from "./indexed/components.js";
84
+ export type { SsspOptions, SsspResult } from "./indexed/dijkstra.js";
85
+ export type { MstOptions, MstResult } from "./indexed/mst.js";
86
+ // Aliased: the flat names are taken twice over (types/index.ts:96 and centrality/pagerank.ts:15).
87
+ export type {
88
+ PageRankOptions as IndexedPageRankOptions,
89
+ PageRankResult as IndexedPageRankResult,
90
+ } from "./indexed/pagerank.js";
91
+
46
92
  // Note: Configuration exports have been removed.
47
93
  // The library now automatically optimizes based on graph size.
@@ -0,0 +1,225 @@
1
+ /**
2
+ * The accelerator seam of `@graphty/algorithms` (WebGPU design section 9.2). It contains NO
3
+ * WebGPU types: an accelerator is anything that satisfies `AlgorithmAccelerator` structurally,
4
+ * and this package never imports the GPU package (design section 9.1, the dependency direction).
5
+ *
6
+ * `accelerated(acc)` is the ONE dispatcher object (design section 9.2, the dispatcher rule); the
7
+ * spelling is `accelerated(acc).pageRank(s, options)`, never
8
+ * `runAlgorithm(snapshot, { accelerator })`. There is no try/catch anywhere below: an accelerator
9
+ * method that throws propagates its throw unchanged, because a silent CPU fallback would hide a
10
+ * broken device behind a slow answer.
11
+ * @module
12
+ */
13
+
14
+ import type { F32, F64, GraphSnapshot, NumericVector, U32 } from "@graphty/graph-format";
15
+
16
+ import type { BfsOptions } from "./bfs.js";
17
+ import { type SsspOptions, type SsspResult, walkPredArcs, walkPredEdges } from "./dijkstra.js";
18
+ import * as indexed from "./index.js";
19
+ import type { MstOptions } from "./mst.js";
20
+ import type { PageRankOptions } from "./pagerank.js";
21
+
22
+ // ============================================================ result shapes (design 9.2 lines 2909-2922)
23
+ // Scores may be f32 (an accelerator) or f64 (the CPU ports), so every score field is NumericVector.
24
+
25
+ /** A score vector with its convergence report. @public */
26
+ export interface ScoresResultLike {
27
+ readonly scores: NumericVector;
28
+ readonly iterations: number;
29
+ readonly converged: boolean;
30
+ }
31
+ /** ScoresResultLike plus the mass held by dangling nodes. @public */
32
+ export interface PageRankResultLike extends ScoresResultLike {
33
+ readonly danglingMass?: number | undefined;
34
+ }
35
+ /** The two HITS vectors with their convergence report. @public */
36
+ export interface HitsResultLike {
37
+ readonly hubs: NumericVector;
38
+ readonly authorities: NumericVector;
39
+ readonly iterations: number;
40
+ readonly converged: boolean;
41
+ }
42
+ /** A partition: dense labels, a count, and the grouped node indices. @public */
43
+ export interface LabelResultLike {
44
+ readonly labels: U32;
45
+ readonly count: number;
46
+ groups(): U32[];
47
+ }
48
+ /** A breadth-first traversal. @public */
49
+ export interface BfsResultLike {
50
+ readonly depth: U32;
51
+ readonly parent: U32;
52
+ readonly order: U32;
53
+ readonly visitedCount: number;
54
+ }
55
+ /** Single-source distances plus the relaxing arc per node. @public */
56
+ export interface SsspResultLike {
57
+ readonly dist: NumericVector;
58
+ readonly predArc: U32;
59
+ }
60
+ /** SsspResultLike plus the negative-cycle flag. @public */
61
+ export interface BellmanFordResultLike extends SsspResultLike {
62
+ readonly hasNegativeCycle: boolean;
63
+ }
64
+ /** A per-logical-edge score vector. @public */
65
+ export interface EdgeScoresResultLike {
66
+ readonly scores: NumericVector;
67
+ }
68
+ /** An all-pairs distance matrix, row-major, n by n. @public */
69
+ export interface ApspResultLike {
70
+ readonly dist: NumericVector;
71
+ readonly n: number;
72
+ }
73
+ /** Per-node coreness. @public */
74
+ export interface CorenessResultLike {
75
+ readonly coreness: U32;
76
+ }
77
+ /** A spanning forest as logical edge indices. @public */
78
+ export interface MstResultLike {
79
+ readonly edges: U32;
80
+ readonly totalWeight: number;
81
+ }
82
+ /** A partition with its modularity. @public */
83
+ export interface CommunityResultLike extends LabelResultLike {
84
+ readonly modularity: number;
85
+ }
86
+
87
+ // ============================================================ the injected object (design 9.2 lines 2925-2948)
88
+
89
+ /**
90
+ * The structural contract an injected accelerator satisfies. EVERY member except `kind` is
91
+ * optional: an accelerator declares only what it implements, and the dispatcher runs the CPU port
92
+ * for everything else. The list is the design's, in full, so that an accelerator written against
93
+ * it needs no change as the ports land.
94
+ * @public
95
+ */
96
+ export interface AlgorithmAccelerator {
97
+ readonly kind: string;
98
+ pageRank?(s: GraphSnapshot, options?: PageRankOptions): Promise<PageRankResultLike>;
99
+ personalizedPageRank?(
100
+ s: GraphSnapshot,
101
+ personalization: F32 | F64,
102
+ options?: PageRankOptions,
103
+ ): Promise<PageRankResultLike>;
104
+ hits?(s: GraphSnapshot, options?: HitsOptionsLike): Promise<HitsResultLike>;
105
+ eigenvectorCentrality?(s: GraphSnapshot, options?: HitsOptionsLike): Promise<ScoresResultLike>;
106
+ katzCentrality?(s: GraphSnapshot, options?: HitsOptionsLike): Promise<ScoresResultLike>;
107
+ connectedComponents?(s: GraphSnapshot): Promise<LabelResultLike>;
108
+ weaklyConnectedComponents?(s: GraphSnapshot): Promise<LabelResultLike>;
109
+ breadthFirstSearch?(s: GraphSnapshot, source: number, options?: BfsOptions): Promise<BfsResultLike>;
110
+ sssp?(s: GraphSnapshot, source: number, options?: SsspOptions): Promise<SsspResultLike>;
111
+ bellmanFord?(s: GraphSnapshot, source: number, options?: SsspOptions): Promise<BellmanFordResultLike>;
112
+ closenessCentrality?(s: GraphSnapshot, options?: HitsOptionsLike): Promise<ScoresResultLike>;
113
+ betweennessCentrality?(s: GraphSnapshot, options?: BetweennessAcceleratorOptions): Promise<ScoresResultLike>;
114
+ edgeBetweennessCentrality?(
115
+ s: GraphSnapshot,
116
+ options?: BetweennessAcceleratorOptions,
117
+ ): Promise<EdgeScoresResultLike>;
118
+ allPairsShortestPath?(s: GraphSnapshot, options?: SsspOptions): Promise<ApspResultLike>;
119
+ kCoreDecomposition?(s: GraphSnapshot): Promise<CorenessResultLike>;
120
+ triangleCount?(s: GraphSnapshot): Promise<{ readonly perNode: U32; readonly total: number }>;
121
+ labelPropagation?(s: GraphSnapshot, options?: HitsOptionsLike): Promise<LabelResultLike>;
122
+ minimumSpanningTree?(s: GraphSnapshot, options?: MstOptions): Promise<MstResultLike>;
123
+ louvain?(s: GraphSnapshot, options?: HitsOptionsLike): Promise<CommunityResultLike>;
124
+ release?(s: GraphSnapshot): void;
125
+ dispose?(): void;
126
+ }
127
+
128
+ /**
129
+ * The option shape of the power-iteration family until each one's `indexed.*` port lands and
130
+ * brings its real option type (plan decision PD-2's "NOT TOUCHED" rows). It is DELIBERATELY not
131
+ * `Record<string, unknown>`: these three are the members every one of those algorithms takes, so
132
+ * an accelerator can honour them today and the type narrows rather than widens as ports arrive.
133
+ * @public
134
+ */
135
+ export interface HitsOptionsLike {
136
+ readonly maxIterations?: number | undefined;
137
+ readonly tolerance?: number | undefined;
138
+ readonly weighted?: boolean | undefined;
139
+ }
140
+
141
+ /**
142
+ * Betweenness options as the accelerator sees them: node INDICES, which is the only form that
143
+ * means anything on a snapshot (plan decision PD-5).
144
+ * @public
145
+ */
146
+ export interface BetweennessAcceleratorOptions {
147
+ readonly normalized?: boolean | undefined;
148
+ readonly endpoints?: boolean | undefined;
149
+ readonly sources?: readonly number[] | undefined;
150
+ readonly k?: number | undefined;
151
+ }
152
+
153
+ // ============================================================ the dispatcher (design 9.2 lines 2950-2957)
154
+
155
+ /**
156
+ * The async dispatcher: one method per accelerable `indexed.*` function. Each delegates to the
157
+ * accelerator when it has the method and runs the CPU port otherwise, wrapped in `Promise.resolve`
158
+ * so both paths are async and graphty-element's `async run()` adapters treat them alike.
159
+ *
160
+ * The list GROWS with the A2 ports -- each port PR adds its method. Today it carries the six whose
161
+ * ports exist (plan departure DEP-8A-E).
162
+ * @public
163
+ */
164
+ export interface AcceleratedAlgorithms {
165
+ readonly accelerator: AlgorithmAccelerator | null;
166
+ pageRank(s: GraphSnapshot, options?: PageRankOptions): Promise<PageRankResultLike>;
167
+ sssp(s: GraphSnapshot, source: number, options?: SsspOptions): Promise<SsspResult>;
168
+ breadthFirstSearch(s: GraphSnapshot, source: number, options?: BfsOptions): Promise<BfsResultLike>;
169
+ connectedComponents(s: GraphSnapshot): Promise<LabelResultLike>;
170
+ weaklyConnectedComponents(s: GraphSnapshot): Promise<LabelResultLike>;
171
+ minimumSpanningTree(s: GraphSnapshot, options?: MstOptions): Promise<MstResultLike>;
172
+ }
173
+
174
+ /**
175
+ * Attach `pathTo` / `pathEdges` to an accelerator's bare `{ dist, predArc }`, so both paths return
176
+ * the design's `SsspResult` and the element keeps ONE result-writing loop. The GPU package cannot
177
+ * attach them itself: it must not depend on the CPU package at runtime (design 9.2 line 2971, D3).
178
+ * @param s - The snapshot the search ran on
179
+ * @param source - The search's source node index
180
+ * @param like - The accelerator's result
181
+ * @returns The decorated result
182
+ */
183
+ function decorateSssp(s: GraphSnapshot, source: number, like: SsspResultLike): SsspResult {
184
+ const { predArc } = like;
185
+ return {
186
+ dist: like.dist,
187
+ predArc,
188
+ pathTo: (target: number): U32 => walkPredArcs(s, predArc, source, target),
189
+ pathEdges: (target: number): U32 => walkPredEdges(s, predArc, source, target),
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Build the dispatcher for an accelerator, or for none.
195
+ * @param acc - The injected accelerator, or `null` / `undefined` for the CPU path
196
+ * @returns A dispatcher whose methods delegate where they can and run the CPU port otherwise
197
+ * @public
198
+ */
199
+ export function accelerated(acc: AlgorithmAccelerator | null | undefined): AcceleratedAlgorithms {
200
+ return {
201
+ accelerator: acc ?? null,
202
+ pageRank: (s, options) =>
203
+ acc?.pageRank !== undefined ? acc.pageRank(s, options) : Promise.resolve(indexed.pageRank(s, options)),
204
+ sssp: (s, source, options) =>
205
+ acc?.sssp !== undefined
206
+ ? acc.sssp(s, source, options).then((like) => decorateSssp(s, source, like))
207
+ : Promise.resolve(indexed.dijkstra(s, source, options)),
208
+ breadthFirstSearch: (s, source, options) =>
209
+ acc?.breadthFirstSearch !== undefined
210
+ ? acc.breadthFirstSearch(s, source, options)
211
+ : Promise.resolve(indexed.breadthFirstSearch(s, source, options)),
212
+ connectedComponents: (s) =>
213
+ acc?.connectedComponents !== undefined
214
+ ? acc.connectedComponents(s)
215
+ : Promise.resolve(indexed.connectedComponents(s)),
216
+ weaklyConnectedComponents: (s) =>
217
+ acc?.weaklyConnectedComponents !== undefined
218
+ ? acc.weaklyConnectedComponents(s)
219
+ : Promise.resolve(indexed.weaklyConnectedComponents(s)),
220
+ minimumSpanningTree: (s, options) =>
221
+ acc?.minimumSpanningTree !== undefined
222
+ ? acc.minimumSpanningTree(s, options)
223
+ : Promise.resolve(indexed.kruskalMST(s, options)),
224
+ };
225
+ }
@@ -0,0 +1,57 @@
1
+ import { type AdjacencyView, INVALID_INDEX, type U32 } from "@graphty/graph-format";
2
+
3
+ /** Result of the index-based BFS (graph-format design 14.2 Port 1). @public */
4
+ export interface BfsResult {
5
+ /** Visit order, one entry per visited node; a subarray of length `visitedCount`. */
6
+ readonly order: U32;
7
+ /** Parent of every node, INVALID_INDEX for the start node and for unvisited nodes. */
8
+ readonly parent: U32;
9
+ /** Hop depth of every node, INVALID_INDEX for unvisited nodes. */
10
+ readonly depth: U32;
11
+ /** How many nodes were visited. */
12
+ readonly visitedCount: number;
13
+ }
14
+
15
+ /** Options of the index-based BFS. @public */
16
+ export interface BfsOptions {
17
+ /** Stop expanding at this depth; unbounded when omitted. */
18
+ readonly maxDepth?: number | undefined;
19
+ }
20
+
21
+ /**
22
+ * Breadth-first search over out-neighbours. Takes any `AdjacencyView`, so `s.reverse()` gives an
23
+ * in-neighbour BFS with no extra code.
24
+ * @param g - The adjacency to traverse
25
+ * @param start - The node index to start from
26
+ * @param options - Traversal options
27
+ * @returns The visit order, the parent array, the depth array and the visited count
28
+ * @public
29
+ */
30
+ export function breadthFirstSearch(g: AdjacencyView, start: number, options: BfsOptions = {}): BfsResult {
31
+ const { nodeCount, rowPtr, colIdx } = g;
32
+ const parent = new Uint32Array(nodeCount).fill(INVALID_INDEX);
33
+ const depth = new Uint32Array(nodeCount).fill(INVALID_INDEX);
34
+ const order = new Uint32Array(nodeCount);
35
+ const maxDepth = options.maxDepth ?? INVALID_INDEX;
36
+ let head = 0;
37
+ let tail = 0;
38
+ order[tail++] = start;
39
+ depth[start] = 0;
40
+ while (head < tail) {
41
+ const u = order[head++];
42
+ const d = depth[u];
43
+ if (d >= maxDepth) {
44
+ continue;
45
+ }
46
+ const end = rowPtr[u + 1];
47
+ for (let a = rowPtr[u]; a < end; a++) {
48
+ const v = colIdx[a];
49
+ if (depth[v] === INVALID_INDEX) {
50
+ depth[v] = d + 1;
51
+ parent[v] = u;
52
+ order[tail++] = v;
53
+ }
54
+ }
55
+ }
56
+ return { order: order.subarray(0, tail), parent, depth, visitedCount: tail };
57
+ }
@@ -0,0 +1,46 @@
1
+ import type { AdjacencyView, GraphSnapshot } from "@graphty/graph-format";
2
+
3
+ /** Options of the index-based common-neighbour score. @public */
4
+ export interface CommonNeighborsOptions {
5
+ /** Intersect out(u) with in(v) instead of the two undirected rows. */
6
+ readonly directed?: boolean | undefined;
7
+ }
8
+
9
+ /**
10
+ * The number of distinct common neighbours of two nodes, by a merge of their sorted rows.
11
+ * @param s - The snapshot
12
+ * @param u - A node index
13
+ * @param v - A node index
14
+ * @param o - Options
15
+ * @returns The count of distinct common neighbours
16
+ * @public
17
+ */
18
+ export function commonNeighborsScore(s: GraphSnapshot, u: number, v: number, o: CommonNeighborsOptions = {}): number {
19
+ const fwd: AdjacencyView = s;
20
+ const bwd: AdjacencyView = o.directed === true ? s.reverse() : s;
21
+ let i = fwd.rowPtr[u];
22
+ const iEnd = fwd.rowPtr[u + 1];
23
+ let j = bwd.rowPtr[v];
24
+ const jEnd = bwd.rowPtr[v + 1];
25
+ let count = 0;
26
+ while (i < iEnd && j < jEnd) {
27
+ const a = fwd.colIdx[i];
28
+ const b = bwd.colIdx[j];
29
+ if (a === b) {
30
+ count++;
31
+ i++;
32
+ j++;
33
+ while (i < iEnd && fwd.colIdx[i] === a) {
34
+ i++;
35
+ }
36
+ while (j < jEnd && bwd.colIdx[j] === b) {
37
+ j++;
38
+ }
39
+ } else if (a < b) {
40
+ i++;
41
+ } else {
42
+ j++;
43
+ }
44
+ }
45
+ return count;
46
+ }
@@ -0,0 +1,78 @@
1
+ import { type GraphSnapshot, type U32 } from "@graphty/graph-format";
2
+
3
+ import { IntUnionFind } from "./structures/union-find.js";
4
+
5
+ /** A partition of the node set (graph-format design 14.2's result table, line 3738). @public */
6
+ export interface LabelResult {
7
+ /** Dense label per node index, in first-seen order. */
8
+ readonly labels: U32;
9
+ /** Number of distinct labels. */
10
+ readonly count: number;
11
+ /**
12
+ * Node indices grouped by label, computed once and cached.
13
+ * @returns One array per label
14
+ */
15
+ groups(): U32[];
16
+ }
17
+
18
+ function withGroups(labels: U32, count: number): LabelResult {
19
+ let cached: U32[] | null = null;
20
+ return {
21
+ labels,
22
+ count,
23
+ groups(): U32[] {
24
+ if (cached === null) {
25
+ const sizes = new Uint32Array(count);
26
+ for (let i = 0; i < labels.length; i++) {
27
+ sizes[labels[i]]++;
28
+ }
29
+ const out: U32[] = [];
30
+ for (let c = 0; c < count; c++) {
31
+ out.push(new Uint32Array(sizes[c]));
32
+ }
33
+ const fill = new Uint32Array(count);
34
+ for (let i = 0; i < labels.length; i++) {
35
+ const c = labels[i];
36
+ out[c][fill[c]++] = i;
37
+ }
38
+ cached = out;
39
+ }
40
+ return cached;
41
+ },
42
+ };
43
+ }
44
+
45
+ function unionEdges(s: GraphSnapshot): LabelResult {
46
+ const uf = new IntUnionFind(s.nodeCount);
47
+ const el = s.edgeList();
48
+ for (let e = 0; e < s.edgeCount; e++) {
49
+ uf.union(el.src[e], el.dst[e]);
50
+ }
51
+ const { labels, count } = uf.toLabels();
52
+ return withGroups(labels, count);
53
+ }
54
+
55
+ /**
56
+ * Connected components of an UNDIRECTED snapshot.
57
+ * @param s - An undirected snapshot
58
+ * @returns The partition
59
+ * @public
60
+ */
61
+ export function connectedComponents(s: GraphSnapshot): LabelResult {
62
+ if (s.directed) {
63
+ throw new Error(
64
+ "Connected components requires an undirected graph. Use weaklyConnectedComponents, or pass s.toUndirected().snapshot.",
65
+ );
66
+ }
67
+ return unionEdges(s);
68
+ }
69
+
70
+ /**
71
+ * Weakly connected components: the same union-find pass with the direction check dropped.
72
+ * @param s - Any snapshot
73
+ * @returns The partition
74
+ * @public
75
+ */
76
+ export function weaklyConnectedComponents(s: GraphSnapshot): LabelResult {
77
+ return unionEdges(s);
78
+ }