@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
@@ -0,0 +1,134 @@
1
+ import { type AdjacencyView, INVALID_INDEX, type NumericVector, type U32 } from "@graphty/graph-format";
2
+
3
+ import { arcSourceIn } from "./structures/arc-source.js";
4
+ import { IndexedMinHeap } from "./structures/min-heap.js";
5
+
6
+ /**
7
+ * Single-source shortest paths, index-based (graph-format design 14.2 Port 2, line 3860).
8
+ * `dist` is a `NumericVector` rather than the design's `F64` because ONE declaration serves both
9
+ * the CPU port (which produces a `Float64Array`) and the dispatcher's decoration of an
10
+ * accelerator's f32 result (plan decision PD-3, departure DEP-8A-C).
11
+ * @public
12
+ */
13
+ export interface SsspResult {
14
+ /** Distance per node; +Infinity for unreached nodes. */
15
+ readonly dist: NumericVector;
16
+ /** The ARC that relaxed each node; INVALID_INDEX for the source and for unreached nodes. */
17
+ readonly predArc: U32;
18
+ /**
19
+ * Node indices from the source to `target` inclusive; empty when `target` is unreached.
20
+ * @param target - The node index to walk back from
21
+ */
22
+ pathTo(target: number): U32;
23
+ /**
24
+ * LOGICAL EDGE indices along that path, one fewer than `pathTo`; this is what graphty-element's
25
+ * `isInPath` writes through. Empty when `target` is unreached.
26
+ * @param target - The node index to walk back from
27
+ */
28
+ pathEdges(target: number): U32;
29
+ }
30
+
31
+ /** Options of the index-based SSSP. @public */
32
+ export interface SsspOptions {
33
+ /** Stop relaxing beyond this distance. */
34
+ readonly cutoff?: number | undefined;
35
+ /** Per-arc weight override, arcCount long -- the facade passes `expandEdges(s, shadow.data)`. */
36
+ readonly weights?: NumericVector | undefined;
37
+ }
38
+
39
+ /**
40
+ * Walk the predecessor arcs back from `target` and return the node path, source first.
41
+ * @param g - The adjacency the search ran on
42
+ * @param predArc - The search's predecessor-arc array
43
+ * @param source - The search's source node index
44
+ * @param target - The node to walk back from
45
+ * @returns Node indices from source to target inclusive, or an empty array when unreached
46
+ * @public
47
+ */
48
+ export function walkPredArcs(g: AdjacencyView, predArc: U32, source: number, target: number): U32 {
49
+ if (target === source) {
50
+ return Uint32Array.of(source);
51
+ }
52
+ if (predArc[target] === INVALID_INDEX) {
53
+ return new Uint32Array(0);
54
+ }
55
+ const reversed: number[] = [target];
56
+ let node = target;
57
+ while (node !== source) {
58
+ node = arcSourceIn(g.rowPtr, predArc[node]);
59
+ reversed.push(node);
60
+ }
61
+ const out = new Uint32Array(reversed.length);
62
+ for (let i = 0; i < reversed.length; i++) {
63
+ out[i] = reversed[reversed.length - 1 - i];
64
+ }
65
+ return out;
66
+ }
67
+
68
+ /**
69
+ * Walk the predecessor arcs back from `target` and return the logical edges on the path.
70
+ * @param g - The adjacency the search ran on
71
+ * @param predArc - The search's predecessor-arc array
72
+ * @param source - The search's source node index
73
+ * @param target - The node to walk back from
74
+ * @returns Logical edge indices from source to target, or an empty array when unreached
75
+ * @public
76
+ */
77
+ export function walkPredEdges(g: AdjacencyView, predArc: U32, source: number, target: number): U32 {
78
+ if (target === source || predArc[target] === INVALID_INDEX) {
79
+ return new Uint32Array(0);
80
+ }
81
+ const reversed: number[] = [];
82
+ let node = target;
83
+ while (node !== source) {
84
+ const arc = predArc[node];
85
+ reversed.push(g.arcToEdge[arc]); // the EXACT parallel edge, not a (u, v) lookup
86
+ node = arcSourceIn(g.rowPtr, arc);
87
+ }
88
+ const out = new Uint32Array(reversed.length);
89
+ for (let i = 0; i < reversed.length; i++) {
90
+ out[i] = reversed[reversed.length - 1 - i];
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Dijkstra over an adjacency view, with the predecessor recorded as the relaxing ARC so a parallel
97
+ * edge on the path is identified exactly.
98
+ * @param g - The adjacency to search
99
+ * @param source - The node index to start from
100
+ * @param options - Cutoff and per-arc weight override
101
+ * @returns The distances, the predecessor arcs, and the two path accessors
102
+ * @public
103
+ */
104
+ export function dijkstra(g: AdjacencyView, source: number, options: SsspOptions = {}): SsspResult {
105
+ const { nodeCount, rowPtr, colIdx } = g;
106
+ const weights: NumericVector | null = options.weights ?? g.weights;
107
+ const dist = new Float64Array(nodeCount).fill(Infinity);
108
+ const predArc = new Uint32Array(nodeCount).fill(INVALID_INDEX);
109
+ const heap = new IndexedMinHeap(nodeCount);
110
+ const cutoff = options.cutoff ?? Infinity;
111
+ dist[source] = 0;
112
+ heap.push(source, 0);
113
+ while (!heap.isEmpty()) {
114
+ const u = heap.pop();
115
+ const du = dist[u];
116
+ const end = rowPtr[u + 1];
117
+ for (let a = rowPtr[u]; a < end; a++) {
118
+ const w = weights === null ? 1 : weights[a];
119
+ const v = colIdx[a];
120
+ const dv = du + w;
121
+ if (dv < dist[v] && dv <= cutoff) {
122
+ dist[v] = dv;
123
+ predArc[v] = a;
124
+ heap.pushOrDecrease(v, dv);
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ dist,
130
+ predArc,
131
+ pathTo: (target: number): U32 => walkPredArcs(g, predArc, source, target),
132
+ pathEdges: (target: number): U32 => walkPredEdges(g, predArc, source, target),
133
+ };
134
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Index-based implementations over `@graphty/graph-format` snapshots (graph-format design 14.1
3
+ * rule 2): every function takes a `GraphSnapshot` or an `AdjacencyView` first and an options object
4
+ * last, and returns typed arrays plus scalars. Reached as the `indexed` namespace of
5
+ * `@graphty/algorithms`.
6
+ *
7
+ * NOT re-exported here, deliberately (plan decision PD-9):
8
+ * - `./accelerator.js`, which imports this barrel -- re-exporting it would make a cycle, and its
9
+ * symbols are exported FLAT from the package barrel because the GPU package must write
10
+ * `import type { AlgorithmAccelerator } from "@graphty/algorithms"`.
11
+ * - `./to-snapshot.js`, whose parameter is a legacy `Graph`, which is not what this namespace
12
+ * promises. It is exported flat too.
13
+ * @module
14
+ */
15
+
16
+ export { type BfsOptions, type BfsResult, breadthFirstSearch } from "./bfs.js";
17
+ export { type CommonNeighborsOptions, commonNeighborsScore } from "./common-neighbors.js";
18
+ export { connectedComponents, type LabelResult, weaklyConnectedComponents } from "./components.js";
19
+ export {
20
+ dijkstra,
21
+ type SsspOptions,
22
+ type SsspResult,
23
+ walkPredArcs,
24
+ walkPredEdges,
25
+ } from "./dijkstra.js";
26
+ export { kruskalMST, type MstOptions, type MstResult } from "./mst.js";
27
+ export { pageRank, type PageRankOptions, type PageRankResult } from "./pagerank.js";
28
+ export { arcSourceIn, IndexedMinHeap, IntUnionFind } from "./structures/index.js";
@@ -0,0 +1,62 @@
1
+ import type { GraphSnapshot, NumericVector, U32 } from "@graphty/graph-format";
2
+
3
+ import { IntUnionFind } from "./structures/union-find.js";
4
+
5
+ /** Options of the index-based MST. @public */
6
+ export interface MstOptions {
7
+ /** Per-arc weight override, arcCount long; gathered back to per-edge through `edgeToArc`. */
8
+ readonly weights?: NumericVector | undefined;
9
+ }
10
+
11
+ /** Result of the index-based MST (graph-format design 14.2's result table, line 3745). @public */
12
+ export interface MstResult {
13
+ /** Logical edge indices of the spanning forest, in the order they were accepted. */
14
+ readonly edges: U32;
15
+ /** Sum of the accepted edges' weights. */
16
+ readonly totalWeight: number;
17
+ }
18
+
19
+ /**
20
+ * Kruskal's minimum spanning forest over logical edges.
21
+ *
22
+ * The sort is a comparator over an f64 key array, tie-broken by edge index, rather than the
23
+ * design's radix sort on the f32 bit pattern (plan departure DEP-8A-D): the tie-break makes it
24
+ * stable by construction and it has to serve both the f32 arc array and an f64 override.
25
+ * @param s - The snapshot
26
+ * @param o - The optional per-arc weight override
27
+ * @returns The accepted edge indices and their total weight
28
+ * @public
29
+ */
30
+ export function kruskalMST(s: GraphSnapshot, o: MstOptions = {}): MstResult {
31
+ const el = s.edgeList();
32
+ const m = s.edgeCount;
33
+ const keys = new Float64Array(m);
34
+ if (o.weights !== undefined) {
35
+ // The override is per ARC; edgeList().arc holds the arc of each edge's declared orientation.
36
+ for (let e = 0; e < m; e++) {
37
+ keys[e] = o.weights[el.arc[e]];
38
+ }
39
+ } else if (el.weights !== null) {
40
+ for (let e = 0; e < m; e++) {
41
+ keys[e] = el.weights[e];
42
+ }
43
+ } else {
44
+ keys.fill(1);
45
+ }
46
+ const order: number[] = new Array<number>(m);
47
+ for (let e = 0; e < m; e++) {
48
+ order[e] = e;
49
+ }
50
+ order.sort((a, b) => keys[a] - keys[b] || a - b);
51
+ const uf = new IntUnionFind(s.nodeCount);
52
+ const accepted = new Uint32Array(Math.max(s.nodeCount - 1, 0));
53
+ let taken = 0;
54
+ let totalWeight = 0;
55
+ for (const e of order) {
56
+ if (uf.union(el.src[e], el.dst[e])) {
57
+ accepted[taken++] = e;
58
+ totalWeight += keys[e];
59
+ }
60
+ }
61
+ return { edges: accepted.subarray(0, taken), totalWeight };
62
+ }
@@ -0,0 +1,73 @@
1
+ import type { F64, GraphSnapshot, NumericVector } from "@graphty/graph-format";
2
+
3
+ /** Options of the index-based PageRank (graph-format design 14.2 Port 3, line 3892). @public */
4
+ export interface PageRankOptions {
5
+ /** Probability of following a link; default 0.85. */
6
+ readonly dampingFactor?: number | undefined;
7
+ /** Iteration cap; default 100. */
8
+ readonly maxIterations?: number | undefined;
9
+ /** L1 convergence tolerance; default 1e-6. */
10
+ readonly tolerance?: number | undefined;
11
+ /** Use the snapshot's arc weights; default false. */
12
+ readonly weighted?: boolean | undefined;
13
+ }
14
+
15
+ /** Result of the index-based PageRank. @public */
16
+ export interface PageRankResult {
17
+ /** Score per node index. */
18
+ readonly scores: F64;
19
+ /** Iterations actually run. */
20
+ readonly iterations: number;
21
+ /** Whether the L1 delta fell below the tolerance. */
22
+ readonly converged: boolean;
23
+ }
24
+
25
+ /**
26
+ * PageRank by pull over `reverse()`, with the dangling mass redistributed uniformly.
27
+ * @param s - A DIRECTED snapshot
28
+ * @param o - Algorithm options
29
+ * @returns The scores, the iteration count and the convergence flag
30
+ * @public
31
+ */
32
+ export function pageRank(s: GraphSnapshot, o: PageRankOptions = {}): PageRankResult {
33
+ if (!s.directed) {
34
+ throw new Error("PageRank requires a directed graph");
35
+ }
36
+ const n = s.nodeCount;
37
+ const d = o.dampingFactor ?? 0.85;
38
+ const maxIter = o.maxIterations ?? 100;
39
+ const tol = o.tolerance ?? 1e-6;
40
+ const rev = s.reverse();
41
+ const weighted = o.weighted === true && rev.weights !== null;
42
+ const outW: NumericVector = weighted ? s.weightedOutDegree() : s.outDegree();
43
+ let rank = new Float64Array(n).fill(1 / n);
44
+ let next = new Float64Array(n);
45
+ let it = 0;
46
+ let converged = false;
47
+ for (; it < maxIter && !converged; it++) {
48
+ let dangling = 0;
49
+ for (let u = 0; u < n; u++) {
50
+ if (outW[u] === 0) {
51
+ dangling += rank[u];
52
+ }
53
+ }
54
+ const base = (1 - d) / n + (d * dangling) / n;
55
+ let delta = 0;
56
+ for (let v = 0; v < n; v++) {
57
+ let acc = 0;
58
+ const end = rev.rowPtr[v + 1];
59
+ for (let a = rev.rowPtr[v]; a < end; a++) {
60
+ const u = rev.colIdx[a];
61
+ const ow = outW[u];
62
+ if (ow > 0) {
63
+ acc += (rank[u] * (weighted && rev.weights !== null ? rev.weights[a] : 1)) / ow;
64
+ }
65
+ }
66
+ next[v] = base + d * acc;
67
+ delta += Math.abs(next[v] - rank[v]);
68
+ }
69
+ [rank, next] = [next, rank];
70
+ converged = delta < tol;
71
+ }
72
+ return { scores: rank, iterations: it, converged };
73
+ }
@@ -0,0 +1,25 @@
1
+ import type { U32 } from "@graphty/graph-format";
2
+
3
+ /**
4
+ * The source node of an arc, by binary search on `rowPtr`. `GraphSnapshot` exposes `arcSource(a)`
5
+ * (`graph-format/src/snapshot/graph-snapshot.ts:544`), but an `AdjacencyView` -- which is what the
6
+ * Port 1 and Port 2 signatures take, so that `reverse()` works as an input for free -- does not,
7
+ * so the predecessor walk carries its own.
8
+ * @param rowPtr - The view's nodeCount + 1 row offsets
9
+ * @param arc - An arc index in `[0, arcCount)`
10
+ * @returns The node index whose row contains the arc
11
+ * @public
12
+ */
13
+ export function arcSourceIn(rowPtr: U32, arc: number): number {
14
+ let lo = 0;
15
+ let hi = rowPtr.length - 1; // nodeCount
16
+ while (lo < hi) {
17
+ const mid = (lo + hi) >> 1;
18
+ if (rowPtr[mid + 1] <= arc) {
19
+ lo = mid + 1;
20
+ } else {
21
+ hi = mid;
22
+ }
23
+ }
24
+ return lo;
25
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Scratch structures for the index-based ports (graph-format design 14.2, section 14.6's
3
+ * helper-ownership sentence at `graph-format-design.md:4270`).
4
+ * @module
5
+ */
6
+
7
+ export { arcSourceIn } from "./arc-source.js";
8
+ export { IndexedMinHeap } from "./min-heap.js";
9
+ export { IntUnionFind } from "./union-find.js";
@@ -0,0 +1,122 @@
1
+ import { type F64, INVALID_INDEX, type U32 } from "@graphty/graph-format";
2
+
3
+ /**
4
+ * A binary min-heap over node indices with O(log n) decrease-key, keyed by `Float64Array` values
5
+ * (graph-format design 14.2, `graph-format-design.md:3771`). `pushOrDecrease` is the only operation
6
+ * Dijkstra's inner loop needs: it inserts an absent node and decreases a present one, so the heap
7
+ * never holds a stale duplicate and `pop()` needs no "is this entry current" check.
8
+ * @public
9
+ */
10
+ export class IndexedMinHeap {
11
+ private readonly heap: U32; // slot -> node
12
+ private readonly slot: U32; // node -> slot, INVALID_INDEX when absent
13
+ private readonly key: F64; // node -> key
14
+ private size = 0;
15
+
16
+ /**
17
+ * Create an empty heap over node indices in `[0, capacity)`.
18
+ * @param capacity - The number of distinct node indices the heap may hold
19
+ */
20
+ constructor(capacity: number) {
21
+ this.heap = new Uint32Array(capacity);
22
+ this.slot = new Uint32Array(capacity).fill(INVALID_INDEX);
23
+ this.key = new Float64Array(capacity);
24
+ }
25
+
26
+ /**
27
+ * Whether the heap holds no entries.
28
+ * @returns True when the heap holds no entries
29
+ */
30
+ isEmpty(): boolean {
31
+ return this.size === 0;
32
+ }
33
+
34
+ /**
35
+ * Insert a node that is not in the heap.
36
+ * @param node - The node index
37
+ * @param key - Its key
38
+ */
39
+ push(node: number, key: number): void {
40
+ this.key[node] = key;
41
+ this.heap[this.size] = node;
42
+ this.slot[node] = this.size;
43
+ this.size++;
44
+ this.siftUp(this.size - 1);
45
+ }
46
+
47
+ /**
48
+ * Insert the node, or lower its key if it is already present and the new key is smaller.
49
+ * @param node - The node index
50
+ * @param key - Its candidate key
51
+ */
52
+ pushOrDecrease(node: number, key: number): void {
53
+ const at = this.slot[node];
54
+ if (at === INVALID_INDEX) {
55
+ this.push(node, key);
56
+ return;
57
+ }
58
+ if (key < this.key[node]) {
59
+ this.key[node] = key;
60
+ this.siftUp(at);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Remove and return the node with the smallest key. Undefined behaviour on an empty heap;
66
+ * callers guard with `isEmpty()`.
67
+ * @returns The node index
68
+ */
69
+ pop(): number {
70
+ const top = this.heap[0];
71
+ this.slot[top] = INVALID_INDEX;
72
+ this.size--;
73
+ if (this.size > 0) {
74
+ const moved = this.heap[this.size];
75
+ this.heap[0] = moved;
76
+ this.slot[moved] = 0;
77
+ this.siftDown(0);
78
+ }
79
+ return top;
80
+ }
81
+
82
+ private siftUp(from: number): void {
83
+ let at = from;
84
+ const node = this.heap[at];
85
+ const key = this.key[node];
86
+ while (at > 0) {
87
+ const parent = (at - 1) >> 1;
88
+ const parentNode = this.heap[parent];
89
+ if (this.key[parentNode] <= key) {
90
+ break;
91
+ }
92
+ this.heap[at] = parentNode;
93
+ this.slot[parentNode] = at;
94
+ at = parent;
95
+ }
96
+ this.heap[at] = node;
97
+ this.slot[node] = at;
98
+ }
99
+
100
+ private siftDown(from: number): void {
101
+ let at = from;
102
+ const node = this.heap[at];
103
+ const key = this.key[node];
104
+ for (;;) {
105
+ const left = 2 * at + 1;
106
+ if (left >= this.size) {
107
+ break;
108
+ }
109
+ const right = left + 1;
110
+ const child = right < this.size && this.key[this.heap[right]] < this.key[this.heap[left]] ? right : left;
111
+ const childNode = this.heap[child];
112
+ if (key <= this.key[childNode]) {
113
+ break;
114
+ }
115
+ this.heap[at] = childNode;
116
+ this.slot[childNode] = at;
117
+ at = child;
118
+ }
119
+ this.heap[at] = node;
120
+ this.slot[node] = at;
121
+ }
122
+ }
@@ -0,0 +1,74 @@
1
+ import { renumberPartition, type U32 } from "@graphty/graph-format";
2
+
3
+ /**
4
+ * Index-keyed union-find over `[0, size)` with union by rank and path halving. The legacy
5
+ * `UnionFind` (`src/data-structures/union-find.ts`) is NodeId-keyed and stays where it is.
6
+ * @public
7
+ */
8
+ export class IntUnionFind {
9
+ private readonly parent: U32;
10
+ private readonly rank: U32;
11
+
12
+ /**
13
+ * Create a union-find whose every element is a singleton set.
14
+ * @param size - The number of elements, each initially its own singleton set
15
+ */
16
+ constructor(size: number) {
17
+ this.parent = new Uint32Array(size);
18
+ this.rank = new Uint32Array(size);
19
+ for (let i = 0; i < size; i++) {
20
+ this.parent[i] = i;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Find the representative of an element's set, halving the path on the way up.
26
+ * @param x - An element index
27
+ * @returns The representative of x's set
28
+ */
29
+ find(x: number): number {
30
+ let node = x;
31
+ while (this.parent[node] !== node) {
32
+ this.parent[node] = this.parent[this.parent[node]]; // path halving
33
+ node = this.parent[node];
34
+ }
35
+ return node;
36
+ }
37
+
38
+ /**
39
+ * Merge the sets of two elements, attaching the shallower tree under the deeper one.
40
+ * @param a - An element index
41
+ * @param b - An element index
42
+ * @returns True when the two sets were distinct and have now been merged
43
+ */
44
+ union(a: number, b: number): boolean {
45
+ const ra = this.find(a);
46
+ const rb = this.find(b);
47
+ if (ra === rb) {
48
+ return false;
49
+ }
50
+ if (this.rank[ra] < this.rank[rb]) {
51
+ this.parent[ra] = rb;
52
+ } else if (this.rank[ra] > this.rank[rb]) {
53
+ this.parent[rb] = ra;
54
+ } else {
55
+ this.parent[rb] = ra;
56
+ this.rank[ra]++;
57
+ }
58
+ return true;
59
+ }
60
+
61
+ /**
62
+ * Dense labels in FIRST-SEEN order, which is what makes `groups()` identical to the legacy
63
+ * iteration order and identical to the GPU's after its own `renumberPartition` readback
64
+ * (design 9.7's connectedComponents row).
65
+ * @returns The labels and the number of distinct sets
66
+ */
67
+ toLabels(): { readonly labels: U32; readonly count: number } {
68
+ const roots = new Uint32Array(this.parent.length);
69
+ for (let i = 0; i < roots.length; i++) {
70
+ roots[i] = this.find(i);
71
+ }
72
+ return renumberPartition(roots);
73
+ }
74
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Conversion from the legacy `Graph` class to a frozen graph-format snapshot
3
+ * (graph-format design 14.1 rule 4, 14.6 row A1). This is the ONLY bridge between the legacy
4
+ * Map-of-Maps surface and `indexed.*`; nothing under `src/indexed/` other than this file imports
5
+ * `../core/graph.js`.
6
+ * @module
7
+ */
8
+
9
+ import { GraphBuilder, type GraphSnapshot } from "@graphty/graph-format";
10
+
11
+ import type { Graph } from "../core/graph.js";
12
+
13
+ /** Options of {@link toSnapshot}. @public */
14
+ export interface ToSnapshotOptions {
15
+ /**
16
+ * Record FNV-1a checksums at freeze so a caller can assert `snapshot.validate({ checksum: true })`
17
+ * (graph-format design 14.2's first port rule: views are shared, so a port that writes into one
18
+ * has to fail a test rather than corrupt the next call). Default false; the test suites set it.
19
+ */
20
+ readonly checksum?: boolean | undefined;
21
+ }
22
+
23
+ interface CacheEntry {
24
+ readonly mutationCount: number;
25
+ readonly checksum: boolean;
26
+ readonly snapshot: GraphSnapshot;
27
+ }
28
+
29
+ /**
30
+ * One entry per graph, REPLACED rather than appended to on a mutation. The entry holds the
31
+ * mutationCount it was built at, which is what makes a stale hit impossible -- the bug the old
32
+ * `WeakMap<Graph, CSRGraph>` cache had (graph-format design 14.1 rule 4).
33
+ */
34
+ const SNAPSHOT_CACHE = new WeakMap<Graph, CacheEntry>();
35
+
36
+ /**
37
+ * Freeze a legacy `Graph` into a `GraphSnapshot`, memoised on the graph's `mutationCount`.
38
+ *
39
+ * The builder is created with `weightDtype: "f64"` so a legacy graph's double weights survive
40
+ * exactly: at freeze, graph-format keeps the original values in an f64 edge column with role
41
+ * `weight` (the "shadow") whenever at least one of them is not f32-exact, and costs nothing when
42
+ * they all are (graph-format design section 3.7, `graph-format/src/builder/freeze.ts:332-360`).
43
+ * A weighted `indexed.*` port reproduces legacy f64 results by passing
44
+ * `expandEdges(s, shadow.data)` as its per-arc `weights` override.
45
+ *
46
+ * Every legacy edge carries a weight (`Graph.addEdge` defaults it to 1), so the builder's
47
+ * `weighted: "auto"` always allocates the arc weight array -- 4 bytes per arc. That is truthful
48
+ * rather than wasteful: the legacy graph really does store the value.
49
+ * @param graph - The legacy graph to convert
50
+ * @param options - Conversion options
51
+ * @returns A frozen snapshot of the graph's current topology and weights
52
+ * @public
53
+ */
54
+ export function toSnapshot(graph: Graph, options: ToSnapshotOptions = {}): GraphSnapshot {
55
+ const checksum = options.checksum === true;
56
+ const cached = SNAPSHOT_CACHE.get(graph);
57
+ // A checksummed snapshot answers a plain request; a plain one cannot answer a checksummed
58
+ // request -- validate({ checksum: true }) throws E_INVALID_SNAPSHOT ("no-checksum") when none
59
+ // were recorded (graph-format/src/types/snapshot.ts:401-405).
60
+ if (cached !== undefined && cached.mutationCount === graph.mutationCount && (cached.checksum || !checksum)) {
61
+ return cached.snapshot;
62
+ }
63
+ const builder = new GraphBuilder({
64
+ directed: graph.isDirected,
65
+ weightDtype: "f64",
66
+ expectedNodes: graph.nodeCount,
67
+ expectedEdges: graph.totalEdgeCount,
68
+ });
69
+ for (const node of graph.nodes()) {
70
+ builder.addNode(node.id);
71
+ }
72
+ for (const edge of graph.edges()) {
73
+ builder.addEdge(edge.source, edge.target, edge.weight);
74
+ }
75
+ const snapshot = builder.freeze({ label: "algorithms.toSnapshot", checksum });
76
+ SNAPSHOT_CACHE.set(graph, { mutationCount: graph.mutationCount, checksum, snapshot });
77
+ return snapshot;
78
+ }