@graphty/graph-format 0.0.0 → 0.2.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.
Files changed (224) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -43
  3. package/dist/graph-format.d.ts +1 -0
  4. package/dist/graph-format.d.ts.map +1 -0
  5. package/dist/graph-format.js +15798 -0
  6. package/dist/graph-format.js.map +1 -0
  7. package/dist/src/builder/arena.d.ts +85 -0
  8. package/dist/src/builder/arena.d.ts.map +1 -0
  9. package/dist/src/builder/arena.js +154 -0
  10. package/dist/src/builder/arena.js.map +1 -0
  11. package/dist/src/builder/compact.d.ts +515 -0
  12. package/dist/src/builder/compact.d.ts.map +1 -0
  13. package/dist/src/builder/compact.js +1553 -0
  14. package/dist/src/builder/compact.js.map +1 -0
  15. package/dist/src/builder/counting-sort.d.ts +96 -0
  16. package/dist/src/builder/counting-sort.d.ts.map +1 -0
  17. package/dist/src/builder/counting-sort.js +430 -0
  18. package/dist/src/builder/counting-sort.js.map +1 -0
  19. package/dist/src/builder/freeze.d.ts +72 -0
  20. package/dist/src/builder/freeze.d.ts.map +1 -0
  21. package/dist/src/builder/freeze.js +548 -0
  22. package/dist/src/builder/freeze.js.map +1 -0
  23. package/dist/src/builder/graph-builder.d.ts +602 -0
  24. package/dist/src/builder/graph-builder.d.ts.map +1 -0
  25. package/dist/src/builder/graph-builder.js +1940 -0
  26. package/dist/src/builder/graph-builder.js.map +1 -0
  27. package/dist/src/builder/options.d.ts +16 -0
  28. package/dist/src/builder/options.d.ts.map +1 -0
  29. package/dist/src/builder/options.js +21 -0
  30. package/dist/src/builder/options.js.map +1 -0
  31. package/dist/src/columns/bitmap.d.ts +96 -0
  32. package/dist/src/columns/bitmap.d.ts.map +1 -0
  33. package/dist/src/columns/bitmap.js +184 -0
  34. package/dist/src/columns/bitmap.js.map +1 -0
  35. package/dist/src/columns/column.d.ts +220 -0
  36. package/dist/src/columns/column.d.ts.map +1 -0
  37. package/dist/src/columns/column.js +2109 -0
  38. package/dist/src/columns/column.js.map +1 -0
  39. package/dist/src/columns/dictionary.d.ts +60 -0
  40. package/dist/src/columns/dictionary.d.ts.map +1 -0
  41. package/dist/src/columns/dictionary.js +102 -0
  42. package/dist/src/columns/dictionary.js.map +1 -0
  43. package/dist/src/columns/growable.d.ts +198 -0
  44. package/dist/src/columns/growable.d.ts.map +1 -0
  45. package/dist/src/columns/growable.js +296 -0
  46. package/dist/src/columns/growable.js.map +1 -0
  47. package/dist/src/columns/infer.d.ts +121 -0
  48. package/dist/src/columns/infer.d.ts.map +1 -0
  49. package/dist/src/columns/infer.js +268 -0
  50. package/dist/src/columns/infer.js.map +1 -0
  51. package/dist/src/columns/remap.d.ts +143 -0
  52. package/dist/src/columns/remap.d.ts.map +1 -0
  53. package/dist/src/columns/remap.js +588 -0
  54. package/dist/src/columns/remap.js.map +1 -0
  55. package/dist/src/columns/table.d.ts +184 -0
  56. package/dist/src/columns/table.d.ts.map +1 -0
  57. package/dist/src/columns/table.js +485 -0
  58. package/dist/src/columns/table.js.map +1 -0
  59. package/dist/src/constants.d.ts +34 -0
  60. package/dist/src/constants.d.ts.map +1 -0
  61. package/dist/src/constants.js +79 -0
  62. package/dist/src/constants.js.map +1 -0
  63. package/dist/src/errors.d.ts +76 -0
  64. package/dist/src/errors.d.ts.map +1 -0
  65. package/dist/src/errors.js +32 -0
  66. package/dist/src/errors.js.map +1 -0
  67. package/dist/src/ids/edge-id-index.d.ts +77 -0
  68. package/dist/src/ids/edge-id-index.d.ts.map +1 -0
  69. package/dist/src/ids/edge-id-index.js +155 -0
  70. package/dist/src/ids/edge-id-index.js.map +1 -0
  71. package/dist/src/ids/node-id-map.d.ts +280 -0
  72. package/dist/src/ids/node-id-map.d.ts.map +1 -0
  73. package/dist/src/ids/node-id-map.js +1139 -0
  74. package/dist/src/ids/node-id-map.js.map +1 -0
  75. package/dist/src/ids/string-store.d.ts +219 -0
  76. package/dist/src/ids/string-store.d.ts.map +1 -0
  77. package/dist/src/ids/string-store.js +526 -0
  78. package/dist/src/ids/string-store.js.map +1 -0
  79. package/dist/src/index.d.ts +30 -0
  80. package/dist/src/index.d.ts.map +1 -0
  81. package/dist/src/index.js +29 -0
  82. package/dist/src/index.js.map +1 -0
  83. package/dist/src/populate/from-csr.d.ts +61 -0
  84. package/dist/src/populate/from-csr.d.ts.map +1 -0
  85. package/dist/src/populate/from-csr.js +706 -0
  86. package/dist/src/populate/from-csr.js.map +1 -0
  87. package/dist/src/populate/from-edge-arrays.d.ts +51 -0
  88. package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
  89. package/dist/src/populate/from-edge-arrays.js +217 -0
  90. package/dist/src/populate/from-edge-arrays.js.map +1 -0
  91. package/dist/src/populate/from-records.d.ts +51 -0
  92. package/dist/src/populate/from-records.d.ts.map +1 -0
  93. package/dist/src/populate/from-records.js +447 -0
  94. package/dist/src/populate/from-records.js.map +1 -0
  95. package/dist/src/snapshot/derived.d.ts +211 -0
  96. package/dist/src/snapshot/derived.d.ts.map +1 -0
  97. package/dist/src/snapshot/derived.js +1254 -0
  98. package/dist/src/snapshot/derived.js.map +1 -0
  99. package/dist/src/snapshot/graph-meta.d.ts +23 -0
  100. package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
  101. package/dist/src/snapshot/graph-meta.js +159 -0
  102. package/dist/src/snapshot/graph-meta.js.map +1 -0
  103. package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
  104. package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
  105. package/dist/src/snapshot/graph-snapshot.js +1247 -0
  106. package/dist/src/snapshot/graph-snapshot.js.map +1 -0
  107. package/dist/src/snapshot/hash.d.ts +90 -0
  108. package/dist/src/snapshot/hash.d.ts.map +1 -0
  109. package/dist/src/snapshot/hash.js +228 -0
  110. package/dist/src/snapshot/hash.js.map +1 -0
  111. package/dist/src/snapshot/queries.d.ts +74 -0
  112. package/dist/src/snapshot/queries.d.ts.map +1 -0
  113. package/dist/src/snapshot/queries.js +131 -0
  114. package/dist/src/snapshot/queries.js.map +1 -0
  115. package/dist/src/snapshot/validate.d.ts +192 -0
  116. package/dist/src/snapshot/validate.d.ts.map +1 -0
  117. package/dist/src/snapshot/validate.js +990 -0
  118. package/dist/src/snapshot/validate.js.map +1 -0
  119. package/dist/src/snapshot/views.d.ts +274 -0
  120. package/dist/src/snapshot/views.d.ts.map +1 -0
  121. package/dist/src/snapshot/views.js +856 -0
  122. package/dist/src/snapshot/views.js.map +1 -0
  123. package/dist/src/types/builder.d.ts +456 -0
  124. package/dist/src/types/builder.d.ts.map +1 -0
  125. package/dist/src/types/builder.js +11 -0
  126. package/dist/src/types/builder.js.map +1 -0
  127. package/dist/src/types/columns.d.ts +615 -0
  128. package/dist/src/types/columns.d.ts.map +1 -0
  129. package/dist/src/types/columns.js +14 -0
  130. package/dist/src/types/columns.js.map +1 -0
  131. package/dist/src/types/index.d.ts +14 -0
  132. package/dist/src/types/index.d.ts.map +1 -0
  133. package/dist/src/types/index.js +11 -0
  134. package/dist/src/types/index.js.map +1 -0
  135. package/dist/src/types/internal.d.ts +172 -0
  136. package/dist/src/types/internal.d.ts.map +1 -0
  137. package/dist/src/types/internal.js +13 -0
  138. package/dist/src/types/internal.js.map +1 -0
  139. package/dist/src/types/snapshot.d.ts +801 -0
  140. package/dist/src/types/snapshot.d.ts.map +1 -0
  141. package/dist/src/types/snapshot.js +14 -0
  142. package/dist/src/types/snapshot.js.map +1 -0
  143. package/dist/src/types/wire.d.ts +196 -0
  144. package/dist/src/types/wire.d.ts.map +1 -0
  145. package/dist/src/types/wire.js +8 -0
  146. package/dist/src/types/wire.js.map +1 -0
  147. package/dist/src/util/mask.d.ts +55 -0
  148. package/dist/src/util/mask.d.ts.map +1 -0
  149. package/dist/src/util/mask.js +71 -0
  150. package/dist/src/util/mask.js.map +1 -0
  151. package/dist/src/util/options.d.ts +22 -0
  152. package/dist/src/util/options.d.ts.map +1 -0
  153. package/dist/src/util/options.js +49 -0
  154. package/dist/src/util/options.js.map +1 -0
  155. package/dist/src/util/shared-buffers.d.ts +42 -0
  156. package/dist/src/util/shared-buffers.d.ts.map +1 -0
  157. package/dist/src/util/shared-buffers.js +52 -0
  158. package/dist/src/util/shared-buffers.js.map +1 -0
  159. package/dist/src/util/typed-array.d.ts +205 -0
  160. package/dist/src/util/typed-array.d.ts.map +1 -0
  161. package/dist/src/util/typed-array.js +289 -0
  162. package/dist/src/util/typed-array.js.map +1 -0
  163. package/dist/src/wire/bytes.d.ts +69 -0
  164. package/dist/src/wire/bytes.d.ts.map +1 -0
  165. package/dist/src/wire/bytes.js +361 -0
  166. package/dist/src/wire/bytes.js.map +1 -0
  167. package/dist/src/wire/carried-views.d.ts +111 -0
  168. package/dist/src/wire/carried-views.d.ts.map +1 -0
  169. package/dist/src/wire/carried-views.js +286 -0
  170. package/dist/src/wire/carried-views.js.map +1 -0
  171. package/dist/src/wire/from-wire.d.ts +144 -0
  172. package/dist/src/wire/from-wire.d.ts.map +1 -0
  173. package/dist/src/wire/from-wire.js +1425 -0
  174. package/dist/src/wire/from-wire.js.map +1 -0
  175. package/dist/src/wire/to-wire.d.ts +171 -0
  176. package/dist/src/wire/to-wire.d.ts.map +1 -0
  177. package/dist/src/wire/to-wire.js +652 -0
  178. package/dist/src/wire/to-wire.js.map +1 -0
  179. package/dist/tsconfig.build.tsbuildinfo +1 -0
  180. package/package.json +72 -7
  181. package/src/builder/arena.ts +192 -0
  182. package/src/builder/compact.ts +1811 -0
  183. package/src/builder/counting-sort.ts +506 -0
  184. package/src/builder/freeze.ts +689 -0
  185. package/src/builder/graph-builder.ts +2264 -0
  186. package/src/builder/options.ts +29 -0
  187. package/src/columns/bitmap.ts +196 -0
  188. package/src/columns/column.ts +2574 -0
  189. package/src/columns/dictionary.ts +114 -0
  190. package/src/columns/growable.ts +350 -0
  191. package/src/columns/infer.ts +285 -0
  192. package/src/columns/remap.ts +663 -0
  193. package/src/columns/table.ts +568 -0
  194. package/src/constants.ts +89 -0
  195. package/src/errors.ts +115 -0
  196. package/src/ids/edge-id-index.ts +179 -0
  197. package/src/ids/node-id-map.ts +1340 -0
  198. package/src/ids/string-store.ts +589 -0
  199. package/src/index.ts +139 -0
  200. package/src/lib-resizable-array-buffer.d.ts +34 -0
  201. package/src/populate/from-csr.ts +802 -0
  202. package/src/populate/from-edge-arrays.ts +257 -0
  203. package/src/populate/from-records.ts +536 -0
  204. package/src/snapshot/derived.ts +1486 -0
  205. package/src/snapshot/graph-meta.ts +167 -0
  206. package/src/snapshot/graph-snapshot.ts +1503 -0
  207. package/src/snapshot/hash.ts +241 -0
  208. package/src/snapshot/queries.ts +136 -0
  209. package/src/snapshot/validate.ts +1108 -0
  210. package/src/snapshot/views.ts +943 -0
  211. package/src/types/builder.ts +480 -0
  212. package/src/types/columns.ts +727 -0
  213. package/src/types/index.ts +14 -0
  214. package/src/types/internal.ts +195 -0
  215. package/src/types/snapshot.ts +886 -0
  216. package/src/types/wire.ts +207 -0
  217. package/src/util/mask.ts +89 -0
  218. package/src/util/options.ts +57 -0
  219. package/src/util/shared-buffers.ts +56 -0
  220. package/src/util/typed-array.ts +371 -0
  221. package/src/wire/bytes.ts +440 -0
  222. package/src/wire/carried-views.ts +327 -0
  223. package/src/wire/from-wire.ts +1690 -0
  224. package/src/wire/to-wire.ts +755 -0
@@ -0,0 +1,506 @@
1
+ /**
2
+ * The two stable counting-sort passes of the freeze pipeline (design section 6.3 steps 3-6, 8): arc
3
+ * materialisation (doubled storage for undirected graphs, self-loops once, invariant I7), the
4
+ * identity check for directed input that is already grouped by source and sorted by target (step 4),
5
+ * pass 1 by target, pass 2 by source into rowPtr with the scatter writing colIdx / arcToEdge /
6
+ * edgeToArc straight into the target arrays (the arena, for a "keep" freeze), and the flag
7
+ * predicates of design section 3.8 computed on the way (I9). There is no comparator sort anywhere;
8
+ * the cost is O(n + A).
9
+ *
10
+ * Because pass 2 is stable and consumes arcs in pass-1 order, every row ends sorted by target with
11
+ * parallel arcs in ascending logical-edge order (I4), and the k-th `u -> v` arc in row u and the
12
+ * k-th `v -> u` arc in row v come from the same edge (design section 6.4).
13
+ *
14
+ * Layout of the passes, chosen from the memory profile of the 100k-node / 1M-edge benchmark (the
15
+ * scatters are cache-miss bound, so the rule is: as few random streams per loop as possible, and no
16
+ * random LOAD whose value a branch depends on inside a scatter):
17
+ *
18
+ * - One sequential pass over the edges counts arcs per target and per source (both prefix sums come
19
+ * out of it), counts the self-loops, and scans the weights (NaN refusal, I8; the min / max of the
20
+ * stored f32 values, from which the three weight flags follow, I9). Every edge lands on at least
21
+ * one arc, so predicates over the edges equal predicates over the arcs.
22
+ * - Pass 1 scatters (source, edge) PAIRS into target order, one 8-byte write per arc into a single
23
+ * `byTarget` transient of 2A words; for an undirected graph a bitmap over the pass-1 positions
24
+ * marks the arc whose stored orientation is the declared one (the mirror arc of an edge is
25
+ * emitted right after it, so arcs stay in edge order within a bucket).
26
+ * - Pass 2 reads `byTarget` sequentially bucket by bucket and scatters ONLY colIdx and arcToEdge (and
27
+ * edgeToArc when undirected, where the declared bit is at hand).
28
+ * - Sequential post-passes derive edgeToArc from arcToEdge (directed), gather the arc weights
29
+ * through arcToEdge, and test adjacent targets within each row for the multigraph flag.
30
+ *
31
+ * Every loop lives in its own small function: V8 optimises a hot loop by on-stack replacement of the
32
+ * function that contains it, and a function invoked once per freeze with a dozen loops in it is
33
+ * re-entered through stale OSR code that deoptimises at every loop exit.
34
+ *
35
+ * Transients of the non-identity path: 2 x U32(n + 1), U32(2A), and for an undirected graph a bitmap
36
+ * of A bits.
37
+ */
38
+
39
+ import { GraphFormatError } from "../errors.js";
40
+ import { type F32, type F64, type SnapshotFlags, type U32 } from "../types/index.js";
41
+ import { allocateCoreArrays, type CoreArrays } from "./arena.js";
42
+
43
+ /** The per-logical-edge input of the sort: views over the (compacted) staging arrays. */
44
+ export interface SortInput {
45
+ /** Whether the graph is directed. */
46
+ readonly directed: boolean;
47
+ /** The node count n; every endpoint is below it. */
48
+ readonly nodeCount: number;
49
+ /** The logical edge count E; only the first E entries of src / dst / weights are read. */
50
+ readonly edgeCount: number;
51
+ /** Declared source of every edge. */
52
+ readonly src: U32;
53
+ /** Declared target of every edge. */
54
+ readonly dst: U32;
55
+ /** Per-edge staging weights (f32 or f64), or null when unweighted. */
56
+ readonly weights: F32 | F64 | null;
57
+ }
58
+
59
+ /** What the sort produces: the core arrays, the counts and the truthful flags. */
60
+ export interface SortResult {
61
+ /** The core arrays (in the arena when requested). */
62
+ readonly core: CoreArrays;
63
+ /** colIdx.length. */
64
+ readonly arcCount: number;
65
+ /** Logical edges with source === target. */
66
+ readonly selfLoopCount: number;
67
+ /** The flags of design section 3.8, computed from the arrays (invariant I9). */
68
+ readonly flags: SnapshotFlags;
69
+ }
70
+
71
+ /**
72
+ * The number of self-loop edges among the first `edgeCount` entries.
73
+ * @param src - sources
74
+ * @param dst - targets
75
+ * @param edgeCount - how many edges to inspect
76
+ * @returns the loop count
77
+ */
78
+ export function countSelfLoops(src: U32, dst: U32, edgeCount: number): number {
79
+ let loops = 0;
80
+ for (let e = 0; e < edgeCount; e++) {
81
+ if (src[e] === dst[e]) {
82
+ loops++;
83
+ }
84
+ }
85
+ return loops;
86
+ }
87
+
88
+ /**
89
+ * The identity check of design section 6.3 step 4: whether (src[e], dst[e]) is non-decreasing
90
+ * lexicographically, so that a stable sort by (source, target) moves nothing and the CSR permutation is
91
+ * the identity.
92
+ * @param src - sources
93
+ * @param dst - targets
94
+ * @param edgeCount - how many edges to inspect
95
+ * @returns true when the edges are already in CSR order
96
+ */
97
+ export function isSortedEdgeList(src: U32, dst: U32, edgeCount: number): boolean {
98
+ for (let e = 1; e < edgeCount; e++) {
99
+ const u = src[e];
100
+ const prev = src[e - 1];
101
+ if (u < prev || (u === prev && dst[e] < dst[e - 1])) {
102
+ return false;
103
+ }
104
+ }
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * The E_INVALID_WEIGHT error for a NaN staging weight (invariant I8).
110
+ * @param e - the logical edge
111
+ * @param w - the value
112
+ * @returns the error
113
+ */
114
+ function nanWeight(e: number, w: number): GraphFormatError {
115
+ return new GraphFormatError("E_INVALID_WEIGHT", `edge ${e} has a NaN weight`, { edge: e, weight: w });
116
+ }
117
+
118
+ // ============================================================ the sequential edge pass
119
+
120
+ /**
121
+ * The weight scan of design section 6.3 step 8 over the first `edgeCount` staging weights: NaN is
122
+ * refused (invariant I8, the bulk-input re-check, naming the lowest such edge), and the smallest and
123
+ * largest STORED values -- the values as the f32 arc array holds them (invariant I9: an f64 that
124
+ * rounds to 1, to -0 or to Infinity counts as its rounded value) -- are left in `bounds[0]` and
125
+ * `bounds[1]`. They are written from inside the loop whenever they change, so the loop's optimised
126
+ * code has no exit path without type feedback.
127
+ * @param edgeWeights - the staging weights
128
+ * @param edgeCount - how many edges to inspect; at least 1
129
+ * @param bounds - two slots initialised to +Infinity / -Infinity; receive the minimum and the maximum
130
+ */
131
+ function scanStoredWeights(edgeWeights: F32 | F64, edgeCount: number, bounds: F64): void {
132
+ let min = Infinity;
133
+ let max = -Infinity;
134
+ for (let e = 0; e < edgeCount; e++) {
135
+ const w = Math.fround(edgeWeights[e]);
136
+ if (w !== w) {
137
+ throw nanWeight(e, edgeWeights[e]);
138
+ }
139
+ if (w < min) {
140
+ min = w;
141
+ bounds[0] = w;
142
+ }
143
+ if (w > max) {
144
+ max = w;
145
+ bounds[1] = w;
146
+ }
147
+ }
148
+ }
149
+
150
+ /**
151
+ * The counting half of the directed sort (design section 6.3 steps 5 and 6, the `cnt` arrays):
152
+ * arcs per target into `targetCount[v + 1]` and arcs per source into `rowCount[u + 1]`, ready for the
153
+ * exclusive prefix sums.
154
+ * @param src - sources
155
+ * @param dst - targets
156
+ * @param edgeCount - the logical edge count
157
+ * @param targetCount - n + 1 zeroed counters, by target
158
+ * @param rowCount - n + 1 zeroed counters, by source
159
+ * @returns the self-loop count
160
+ */
161
+ function countDirectedArcs(src: U32, dst: U32, edgeCount: number, targetCount: U32, rowCount: U32): number {
162
+ let loops = 0;
163
+ for (let e = 0; e < edgeCount; e++) {
164
+ const u = src[e];
165
+ const v = dst[e];
166
+ targetCount[v + 1]++;
167
+ rowCount[u + 1]++;
168
+ if (u === v) {
169
+ loops++;
170
+ }
171
+ }
172
+ return loops;
173
+ }
174
+
175
+ /**
176
+ * The counting half of the undirected sort (design section 6.3 steps 3, 5 and 6): every non-loop
177
+ * edge contributes an arc in each direction, a self-loop one arc (invariant I7).
178
+ * @param src - sources
179
+ * @param dst - targets
180
+ * @param edgeCount - the logical edge count
181
+ * @param targetCount - n + 1 zeroed counters, by target
182
+ * @param rowCount - n + 1 zeroed counters, by source
183
+ * @returns the self-loop count
184
+ */
185
+ function countUndirectedArcs(src: U32, dst: U32, edgeCount: number, targetCount: U32, rowCount: U32): number {
186
+ let loops = 0;
187
+ for (let e = 0; e < edgeCount; e++) {
188
+ const u = src[e];
189
+ const v = dst[e];
190
+ targetCount[v + 1]++;
191
+ rowCount[u + 1]++;
192
+ if (u === v) {
193
+ loops++;
194
+ } else {
195
+ targetCount[u + 1]++;
196
+ rowCount[v + 1]++;
197
+ }
198
+ }
199
+ return loops;
200
+ }
201
+
202
+ /**
203
+ * Turn per-node counts stored at index + 1 into exclusive prefix sums in place: afterwards
204
+ * `counts[v]` is the start of v's bucket and `counts[nodeCount]` the total.
205
+ * @param counts - n + 1 counters with `counts[0] === 0`
206
+ * @param nodeCount - n
207
+ */
208
+ function prefixSum(counts: U32, nodeCount: number): void {
209
+ for (let v = 0; v < nodeCount; v++) {
210
+ counts[v + 1] += counts[v];
211
+ }
212
+ }
213
+
214
+ // ============================================================ the identity path
215
+
216
+ /**
217
+ * The identity path of design section 6.3 step 4 (directed input already in CSR order): colIdx is a
218
+ * copy of dst, and the multigraph flag is one comparison of adjacent edges.
219
+ * @param src - sources, grouped
220
+ * @param dst - targets, sorted within each group
221
+ * @param edgeCount - the logical edge count
222
+ * @param colIdx - receives dst
223
+ * @returns true when two adjacent edges connect the same pair
224
+ */
225
+ function copySortedTargets(src: U32, dst: U32, edgeCount: number, colIdx: U32): boolean {
226
+ let multigraph = false;
227
+ for (let e = 0; e < edgeCount; e++) {
228
+ const v = dst[e];
229
+ colIdx[e] = v;
230
+ if (e > 0 && src[e] === src[e - 1] && dst[e - 1] === v) {
231
+ multigraph = true;
232
+ }
233
+ }
234
+ return multigraph;
235
+ }
236
+
237
+ // ============================================================ pass 1: by target
238
+
239
+ /**
240
+ * Pass 1 of a directed sort (design section 6.3 step 5): scatter every edge into target order as a
241
+ * (source, edge) pair, `byTarget[2p] = src[e]`, `byTarget[2p + 1] = e`.
242
+ * @param src - sources
243
+ * @param dst - targets
244
+ * @param edgeCount - the logical edge count
245
+ * @param cursor - the target bucket starts; advanced in place
246
+ * @param byTarget - 2A words receiving the pairs
247
+ */
248
+ function scatterDirectedByTarget(src: U32, dst: U32, edgeCount: number, cursor: U32, byTarget: U32): void {
249
+ for (let e = 0; e < edgeCount; e++) {
250
+ const p = 2 * cursor[dst[e]]++;
251
+ byTarget[p] = src[e];
252
+ byTarget[p + 1] = e;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Pass 1 of an undirected sort (design section 6.3 steps 3 and 5, fused): the declared arc
258
+ * `(src, dst, e)` goes into bucket dst and, for a non-loop, the mirror `(dst, src, e)` into bucket
259
+ * src right after it, so arcs stay in edge order within every bucket. The pass-1 position of every
260
+ * declared arc is marked in `declared` (bit p).
261
+ * @param src - sources
262
+ * @param dst - targets
263
+ * @param edgeCount - the logical edge count
264
+ * @param cursor - the target bucket starts; advanced in place
265
+ * @param byTarget - 2A words receiving the (source, edge) pairs
266
+ * @param declared - a zeroed bitmap of A bits
267
+ */
268
+ function scatterUndirectedByTarget(
269
+ src: U32,
270
+ dst: U32,
271
+ edgeCount: number,
272
+ cursor: U32,
273
+ byTarget: U32,
274
+ declared: U32,
275
+ ): void {
276
+ for (let e = 0; e < edgeCount; e++) {
277
+ const u = src[e];
278
+ const v = dst[e];
279
+ const p = cursor[v]++;
280
+ byTarget[2 * p] = u;
281
+ byTarget[2 * p + 1] = e;
282
+ declared[p >>> 5] |= 1 << (p & 31);
283
+ if (u !== v) {
284
+ const q = 2 * cursor[u]++;
285
+ byTarget[q] = v;
286
+ byTarget[q + 1] = e;
287
+ }
288
+ }
289
+ }
290
+
291
+ // ============================================================ pass 2: by source
292
+
293
+ /**
294
+ * Pass 2 of a directed sort (design section 6.3 step 6): walk the target buckets in order and
295
+ * scatter every arc into its row, writing colIdx and arcToEdge. Stable, so rows end sorted by target
296
+ * with parallels in edge order (I4).
297
+ * @param nodeCount - n
298
+ * @param targetStart - the target bucket starts (pass-1 layout)
299
+ * @param byTarget - the pass-1 (source, edge) pairs
300
+ * @param cursor - the row starts (a copy of rowPtr); advanced in place
301
+ * @param colIdx - receives the targets
302
+ * @param arcToEdge - receives the edges
303
+ */
304
+ function scatterDirectedBySource(
305
+ nodeCount: number,
306
+ targetStart: U32,
307
+ byTarget: U32,
308
+ cursor: U32,
309
+ colIdx: U32,
310
+ arcToEdge: U32,
311
+ ): void {
312
+ for (let v = 0; v < nodeCount; v++) {
313
+ const end = 2 * targetStart[v + 1];
314
+ for (let p = 2 * targetStart[v]; p < end; p += 2) {
315
+ const a = cursor[byTarget[p]]++;
316
+ colIdx[a] = v;
317
+ arcToEdge[a] = byTarget[p + 1];
318
+ }
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Pass 2 of an undirected sort (design section 6.3 step 6): as the directed pass, plus
324
+ * `edgeToArc[e] = a` for the arc whose stored orientation is the declared one (for a self-loop the
325
+ * single arc), which the pass-1 bitmap identifies.
326
+ * @param nodeCount - n
327
+ * @param targetStart - the target bucket starts (pass-1 layout)
328
+ * @param byTarget - the pass-1 (source, edge) pairs
329
+ * @param declared - bit p set when pass-1 position p holds a declared arc
330
+ * @param cursor - the row starts (a copy of rowPtr); advanced in place
331
+ * @param colIdx - receives the targets
332
+ * @param arcToEdge - receives the edges
333
+ * @param edgeToArc - receives the declared arc of every edge
334
+ */
335
+ function scatterUndirectedBySource(
336
+ nodeCount: number,
337
+ targetStart: U32,
338
+ byTarget: U32,
339
+ declared: U32,
340
+ cursor: U32,
341
+ colIdx: U32,
342
+ arcToEdge: U32,
343
+ edgeToArc: U32,
344
+ ): void {
345
+ for (let v = 0; v < nodeCount; v++) {
346
+ const end = targetStart[v + 1];
347
+ for (let p = targetStart[v]; p < end; p++) {
348
+ const e = byTarget[2 * p + 1];
349
+ const a = cursor[byTarget[2 * p]]++;
350
+ colIdx[a] = v;
351
+ arcToEdge[a] = e;
352
+ if (((declared[p >>> 5] >>> (p & 31)) & 1) === 1) {
353
+ edgeToArc[e] = a;
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ // ============================================================ sequential post-passes
360
+
361
+ /**
362
+ * The inverse of a directed permutation: `edgeToArc[arcToEdge[a]] = a` (every arc is its edge's only
363
+ * arc).
364
+ * @param arcToEdge - the arc permutation
365
+ * @param edgeToArc - receives the inverse
366
+ */
367
+ function invertPermutation(arcToEdge: U32, edgeToArc: U32): void {
368
+ const arcCount = arcToEdge.length;
369
+ for (let a = 0; a < arcCount; a++) {
370
+ edgeToArc[arcToEdge[a]] = a;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * The arc weights (design section 3.7): `weights[a] = f32(edgeWeights[arcToEdge[a]])`, a sequential
376
+ * write with one independent random read per arc.
377
+ * @param arcToEdge - the arc permutation
378
+ * @param edgeWeights - the staging weights
379
+ * @param weights - receives the f32 arc weights
380
+ */
381
+ function gatherWeights(arcToEdge: U32, edgeWeights: F32 | F64, weights: F32): void {
382
+ const arcCount = arcToEdge.length;
383
+ for (let a = 0; a < arcCount; a++) {
384
+ weights[a] = edgeWeights[arcToEdge[a]];
385
+ }
386
+ }
387
+
388
+ /**
389
+ * The multigraph flag (design section 3.8): whether some row holds two adjacent arcs with the same
390
+ * target; rows are sorted by target, so parallels are adjacent (I4).
391
+ * @param rowPtr - the row starts
392
+ * @param colIdx - the sorted targets
393
+ * @param nodeCount - n
394
+ * @returns true when a parallel pair exists
395
+ */
396
+ function hasAdjacentParallels(rowPtr: U32, colIdx: U32, nodeCount: number): boolean {
397
+ for (let u = 0; u < nodeCount; u++) {
398
+ const end = rowPtr[u + 1];
399
+ for (let a = rowPtr[u] + 1; a < end; a++) {
400
+ if (colIdx[a] === colIdx[a - 1]) {
401
+ return true;
402
+ }
403
+ }
404
+ }
405
+ return false;
406
+ }
407
+
408
+ // ============================================================ the sort
409
+
410
+ /**
411
+ * Sort a per-edge list into a CSR core (design section 6.3 steps 3-6 and 8). Directed input that is
412
+ * already in CSR order takes the identity path (one count + scan + copy, no permutation arrays,
413
+ * `flags.arcToEdgeIsIdentity`); everything else takes the two counting-sort passes. Undirected input
414
+ * emits the mirror arc of every non-loop edge (I7). Weights are stored as f32 (design section 3.7) and
415
+ * the flags are computed from the stored values (I9). A NaN weight is E_INVALID_WEIGHT (I8) naming
416
+ * the lowest such edge, thrown before anything is allocated.
417
+ * @param input - the per-edge arrays
418
+ * @param useArena - allocate the core inside one 256-aligned arena (true) or as separate buffers
419
+ * @returns the core, the counts and the flags
420
+ */
421
+ export function sortIntoCore(input: SortInput, useArena: boolean): SortResult {
422
+ const { directed, nodeCount, edgeCount, src, dst } = input;
423
+ const edgeWeights = input.weights;
424
+
425
+ // step 8's weight predicates, over the edges: every edge lands on at least one arc, so the
426
+ // per-value predicates over the stored arc weights equal those over the f32-rounded edge weights
427
+ let allOne = true;
428
+ let nonNegative = true;
429
+ let finite = true;
430
+ if (edgeWeights !== null && edgeCount > 0) {
431
+ const bounds = new Float64Array([Infinity, -Infinity]);
432
+ scanStoredWeights(edgeWeights, edgeCount, bounds);
433
+ const min = bounds[0];
434
+ const max = bounds[1];
435
+ allOne = min === 1 && max === 1;
436
+ nonNegative = min >= 0;
437
+ finite = min > -Infinity && max < Infinity;
438
+ }
439
+
440
+ // steps 3-6 counts: one sequential pass over the edges yields both prefix sums and the loop count
441
+ const targetStart = new Uint32Array(nodeCount + 1);
442
+ const rowCount = new Uint32Array(nodeCount + 1);
443
+ const selfLoopCount = directed
444
+ ? countDirectedArcs(src, dst, edgeCount, targetStart, rowCount)
445
+ : countUndirectedArcs(src, dst, edgeCount, targetStart, rowCount);
446
+ const arcCount = directed ? edgeCount : 2 * edgeCount - selfLoopCount;
447
+ const identity = directed && isSortedEdgeList(src, dst, edgeCount);
448
+ const core = allocateCoreArrays(
449
+ { nodeCount, arcCount, edgeCount, weighted: edgeWeights !== null, identity },
450
+ useArena,
451
+ );
452
+ const { rowPtr, colIdx, weights, arcToEdge, edgeToArc } = core;
453
+ prefixSum(rowCount, nodeCount);
454
+ rowPtr.set(rowCount);
455
+ let multigraph: boolean;
456
+
457
+ if (identity) {
458
+ // step 4: the edge arrays are the arc arrays -- copy, and round the weights to f32
459
+ multigraph = copySortedTargets(src, dst, edgeCount, colIdx);
460
+ if (weights !== null && edgeWeights !== null) {
461
+ weights.set(edgeWeights.subarray(0, edgeCount));
462
+ }
463
+ } else if (arcToEdge !== null && edgeToArc !== null) {
464
+ // step 5: pass 1 by target; `rowCount` is reused as the pass-1 cursor
465
+ prefixSum(targetStart, nodeCount);
466
+ const cursor = rowCount;
467
+ cursor.set(targetStart);
468
+ const byTarget = new Uint32Array(2 * arcCount);
469
+ let declared: U32 | null = null;
470
+ if (directed) {
471
+ scatterDirectedByTarget(src, dst, edgeCount, cursor, byTarget);
472
+ } else {
473
+ declared = new Uint32Array((arcCount + 31) >>> 5);
474
+ scatterUndirectedByTarget(src, dst, edgeCount, cursor, byTarget, declared);
475
+ }
476
+ // step 6: pass 2 by source into the rows; the cursor now walks rowPtr
477
+ cursor.set(rowPtr);
478
+ if (declared === null) {
479
+ scatterDirectedBySource(nodeCount, targetStart, byTarget, cursor, colIdx, arcToEdge);
480
+ invertPermutation(arcToEdge, edgeToArc);
481
+ } else {
482
+ scatterUndirectedBySource(nodeCount, targetStart, byTarget, declared, cursor, colIdx, arcToEdge, edgeToArc);
483
+ }
484
+ if (weights !== null && edgeWeights !== null) {
485
+ gatherWeights(arcToEdge, edgeWeights, weights);
486
+ }
487
+ multigraph = hasAdjacentParallels(rowPtr, colIdx, nodeCount);
488
+ } else {
489
+ // allocateCoreArrays materialises both permutations whenever the sort is not the identity
490
+ throw new GraphFormatError("E_UNSUPPORTED", "unreachable: a non-identity core without permutation arrays", {
491
+ directed,
492
+ arcCount,
493
+ });
494
+ }
495
+
496
+ const flags: SnapshotFlags = Object.freeze({
497
+ multigraph,
498
+ hasSelfLoops: selfLoopCount > 0,
499
+ arcToEdgeIsIdentity: identity,
500
+ weighted: weights !== null,
501
+ allWeightsOne: allOne,
502
+ nonNegativeWeights: nonNegative,
503
+ finiteWeights: finite,
504
+ });
505
+ return { core, arcCount, selfLoopCount, flags };
506
+ }