@graphty/algorithms 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/algorithms.d.ts +2 -37
- package/dist/algorithms.js +736 -223
- package/dist/algorithms.js.map +1 -1
- package/dist/algorithms.standalone.js +24524 -0
- package/dist/algorithms.standalone.js.map +1 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts +16 -11
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +17 -0
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/core/graph.d.ts +10 -0
- package/dist/src/core/graph.d.ts.map +1 -1
- package/dist/src/core/graph.js +31 -2
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/indexed/accelerator.d.ts +161 -0
- package/dist/src/indexed/accelerator.d.ts.map +1 -0
- package/dist/src/indexed/accelerator.js +60 -0
- package/dist/src/indexed/accelerator.js.map +1 -0
- package/dist/src/indexed/bfs.d.ts +28 -0
- package/dist/src/indexed/bfs.d.ts.map +1 -0
- package/dist/src/indexed/bfs.js +39 -0
- package/dist/src/indexed/bfs.js.map +1 -0
- package/dist/src/indexed/common-neighbors.d.ts +17 -0
- package/dist/src/indexed/common-neighbors.d.ts.map +1 -0
- package/dist/src/indexed/common-neighbors.js +41 -0
- package/dist/src/indexed/common-neighbors.js.map +1 -0
- package/dist/src/indexed/components.d.ts +28 -0
- package/dist/src/indexed/components.d.ts.map +1 -0
- package/dist/src/indexed/components.js +58 -0
- package/dist/src/indexed/components.js.map +1 -0
- package/dist/src/indexed/dijkstra.d.ts +63 -0
- package/dist/src/indexed/dijkstra.d.ts.map +1 -0
- package/dist/src/indexed/dijkstra.js +98 -0
- package/dist/src/indexed/dijkstra.js.map +1 -0
- package/dist/src/indexed/index.d.ts +22 -0
- package/dist/src/indexed/index.d.ts.map +1 -0
- package/dist/src/indexed/index.js +22 -0
- package/dist/src/indexed/index.js.map +1 -0
- package/dist/src/indexed/mst.d.ts +26 -0
- package/dist/src/indexed/mst.d.ts.map +1 -0
- package/dist/src/indexed/mst.js +48 -0
- package/dist/src/indexed/mst.js.map +1 -0
- package/dist/src/indexed/pagerank.d.ts +30 -0
- package/dist/src/indexed/pagerank.d.ts.map +1 -0
- package/dist/src/indexed/pagerank.js +50 -0
- package/dist/src/indexed/pagerank.js.map +1 -0
- package/dist/src/indexed/structures/arc-source.d.ts +13 -0
- package/dist/src/indexed/structures/arc-source.d.ts.map +1 -0
- package/dist/src/indexed/structures/arc-source.js +25 -0
- package/dist/src/indexed/structures/arc-source.js.map +1 -0
- package/dist/src/indexed/structures/index.d.ts +9 -0
- package/dist/src/indexed/structures/index.d.ts.map +1 -0
- package/dist/src/indexed/structures/index.js +9 -0
- package/dist/src/indexed/structures/index.js.map +1 -0
- package/dist/src/indexed/structures/min-heap.d.ts +44 -0
- package/dist/src/indexed/structures/min-heap.d.ts.map +1 -0
- package/dist/src/indexed/structures/min-heap.js +112 -0
- package/dist/src/indexed/structures/min-heap.js.map +1 -0
- package/dist/src/indexed/structures/union-find.d.ts +39 -0
- package/dist/src/indexed/structures/union-find.d.ts.map +1 -0
- package/dist/src/indexed/structures/union-find.js +70 -0
- package/dist/src/indexed/structures/union-find.js.map +1 -0
- package/dist/src/indexed/to-snapshot.d.ts +38 -0
- package/dist/src/indexed/to-snapshot.d.ts.map +1 -0
- package/dist/src/indexed/to-snapshot.js +58 -0
- package/dist/src/indexed/to-snapshot.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -3
- package/src/algorithms/centrality/betweenness.ts +36 -11
- package/src/core/graph.ts +35 -2
- package/src/index.ts +46 -0
- package/src/indexed/accelerator.ts +225 -0
- package/src/indexed/bfs.ts +57 -0
- package/src/indexed/common-neighbors.ts +46 -0
- package/src/indexed/components.ts +78 -0
- package/src/indexed/dijkstra.ts +134 -0
- package/src/indexed/index.ts +28 -0
- package/src/indexed/mst.ts +62 -0
- package/src/indexed/pagerank.ts +73 -0
- package/src/indexed/structures/arc-source.ts +25 -0
- package/src/indexed/structures/index.ts +9 -0
- package/src/indexed/structures/min-heap.ts +122 -0
- package/src/indexed/structures/union-find.ts +74 -0
- 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.
|
|
3
|
+
"version": "1.8.0",
|
|
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.1"
|
|
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
|
-
*
|
|
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
|
-
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
+
}
|