@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,241 @@
1
+ /**
2
+ * Content hashing and checksums of @graphty/graph-format (design sections 5.8 and 9.3).
3
+ *
4
+ * `contentHash()` is a 64-bit hash of the core arrays computed as two independent 32-bit FNV-1a-style
5
+ * lanes over u32 words with `Math.imul`, returned as 16 hex characters: the key a cache uses for a
6
+ * snapshot's topology. The same hasher produces the per-array checksums that `freeze({ checksum:
7
+ * true })` records and `validate({ checksum: true })` compares, so a consumer that wrote into a frozen
8
+ * array or a shared view is caught by the tests that opt in (invariant I17).
9
+ *
10
+ * The hash is a pure function of the topology: it covers `directed`, the four counts, `rowPtr`,
11
+ * `colIdx`, `weights` (as raw f32 bit patterns), `arcToEdge` and `edgeToArc`. An identity permutation
12
+ * is hashed as the sequence 0..len-1 WITHOUT materialising it, so two snapshots with the same
13
+ * topology hash identically whether or not their getters were touched (P11). Nothing here depends on
14
+ * the arena, the id map or the columns.
15
+ */
16
+
17
+ import { isColumnDetached } from "../columns/column.js";
18
+ import { GraphFormatError } from "../errors.js";
19
+ import { type Column, type GraphSnapshot } from "../types/index.js";
20
+ import { unsupportedDtype } from "../util/typed-array.js";
21
+
22
+ /** Lane A: the 32-bit FNV-1a offset basis. */
23
+ const LANE_A_BASIS = 0x811c9dc5;
24
+ /** Lane A: the 32-bit FNV prime. */
25
+ const LANE_A_PRIME = 0x01000193;
26
+ /** Lane B: the high word of the 64-bit FNV offset basis (0xcbf29ce484222325), as an independent seed. */
27
+ const LANE_B_BASIS = 0xcbf29ce4;
28
+ /** Lane B: an odd multiplier unrelated to the FNV prime (the MurmurHash2 constant), so the lanes decorrelate. */
29
+ const LANE_B_MULTIPLIER = 0x5bd1e995;
30
+
31
+ /**
32
+ * Two-lane FNV-1a-style hasher over u32 words (design section 9.3). Lane A is standard 32-bit FNV-1a
33
+ * applied word-wise (`h = imul(h ^ w, 0x01000193)`); lane B uses a different basis and multiplier
34
+ * (`h = imul(h ^ w, 0x5bd1e995)`) and additionally rotates the word by 16 bits before mixing so the
35
+ * two lanes never agree on which bits they weight. `digest()` renders both lanes as 8 zero-padded hex
36
+ * characters each, lane A first.
37
+ */
38
+ export class Fnv1aHasher {
39
+ private laneA: number;
40
+ private laneB: number;
41
+
42
+ /** Start a fresh hasher at the two offset bases. */
43
+ constructor() {
44
+ this.laneA = LANE_A_BASIS;
45
+ this.laneB = LANE_B_BASIS;
46
+ }
47
+
48
+ /**
49
+ * Mix one u32 word (any number is coerced with `>>> 0`).
50
+ * @param word - the word to mix
51
+ * @returns this hasher
52
+ */
53
+ word(word: number): this {
54
+ const w = word >>> 0;
55
+ this.laneA = Math.imul(this.laneA ^ w, LANE_A_PRIME) >>> 0;
56
+ const rotated = ((w << 16) | (w >>> 16)) >>> 0;
57
+ this.laneB = Math.imul(this.laneB ^ rotated, LANE_B_MULTIPLIER) >>> 0;
58
+ return this;
59
+ }
60
+
61
+ /**
62
+ * Mix a length prefix followed by every word of a u32 array, so arrays of different lengths with a
63
+ * common prefix hash differently and consecutive arrays cannot be re-split.
64
+ * @param words - the words to mix
65
+ * @returns this hasher
66
+ */
67
+ words(words: Uint32Array): this {
68
+ this.word(words.length);
69
+ for (let i = 0; i < words.length; i++) {
70
+ this.word(words[i]);
71
+ }
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Mix the sequence 0..length-1 (an identity permutation) with the same framing as `words()`, without
77
+ * allocating it.
78
+ * @param length - the permutation length
79
+ * @returns this hasher
80
+ */
81
+ identity(length: number): this {
82
+ this.word(length);
83
+ for (let i = 0; i < length; i++) {
84
+ this.word(i);
85
+ }
86
+ return this;
87
+ }
88
+
89
+ /**
90
+ * Mix a byte length prefix followed by the bytes of any ArrayBufferView, four bytes per word
91
+ * (little-endian packing, the last word zero-padded). A view whose byte range is 4-byte aligned
92
+ * is read as u32 words directly.
93
+ * @param view - the bytes to mix
94
+ * @returns this hasher
95
+ */
96
+ bytes(view: ArrayBufferView): this {
97
+ const { byteLength, byteOffset } = view;
98
+ this.word(byteLength);
99
+ if (byteOffset % 4 === 0 && byteLength % 4 === 0) {
100
+ const words = new Uint32Array(view.buffer, byteOffset, byteLength / 4);
101
+ for (let i = 0; i < words.length; i++) {
102
+ this.word(words[i]);
103
+ }
104
+ return this;
105
+ }
106
+ const bytes = new Uint8Array(view.buffer, byteOffset, byteLength);
107
+ let i = 0;
108
+ for (; i + 4 <= byteLength; i += 4) {
109
+ this.word(bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24));
110
+ }
111
+ if (i < byteLength) {
112
+ let tail = 0;
113
+ for (let k = 0; i + k < byteLength; k++) {
114
+ tail |= bytes[i + k] << (8 * k);
115
+ }
116
+ this.word(tail);
117
+ }
118
+ return this;
119
+ }
120
+
121
+ /**
122
+ * Mix a JS string as its length followed by its UTF-16 code units.
123
+ * @param text - the string to mix
124
+ * @returns this hasher
125
+ */
126
+ text(text: string): this {
127
+ this.word(text.length);
128
+ for (let i = 0; i < text.length; i++) {
129
+ this.word(text.charCodeAt(i));
130
+ }
131
+ return this;
132
+ }
133
+
134
+ /**
135
+ * The 16-character lowercase hex digest: lane A then lane B, each 8 zero-padded characters.
136
+ * @returns the digest
137
+ */
138
+ digest(): string {
139
+ return this.laneA.toString(16).padStart(8, "0") + this.laneB.toString(16).padStart(8, "0");
140
+ }
141
+ }
142
+
143
+ /**
144
+ * The digest of one typed array's bytes (a core array, a view array, a column buffer).
145
+ * @param view - the array to hash
146
+ * @returns the 16-character hex digest
147
+ */
148
+ export function hashTypedArray(view: ArrayBufferView): string {
149
+ return new Fnv1aHasher().bytes(view).digest();
150
+ }
151
+
152
+ /**
153
+ * Mix everything that determines a column's contents: dtype, length, null count, validity words,
154
+ * and per dtype the data / codes / dictionary / offsets / utf8 / child / json text. Used for the
155
+ * immutable-column checksums of design section 5.8.
156
+ * @param hasher - the hasher to mix into
157
+ * @param column - the column
158
+ */
159
+ function mixColumn(hasher: Fnv1aHasher, column: Column): void {
160
+ hasher.text(column.dtype).word(column.length).word(column.nullCount);
161
+ if (column.validity !== null) {
162
+ hasher.words(column.validity);
163
+ } else {
164
+ hasher.word(0);
165
+ }
166
+ const { dtype } = column;
167
+ switch (dtype) {
168
+ case "f32":
169
+ case "f64":
170
+ case "i32":
171
+ case "u32":
172
+ case "u8":
173
+ case "bool":
174
+ hasher.bytes(column.data);
175
+ break;
176
+ case "dict":
177
+ hasher.words(column.codes).word(column.dictionary.length);
178
+ for (const entry of column.dictionary) {
179
+ hasher.text(entry);
180
+ }
181
+ break;
182
+ case "string":
183
+ hasher.words(column.offsets).bytes(column.utf8);
184
+ break;
185
+ case "list":
186
+ hasher.words(column.offsets);
187
+ mixColumn(hasher, column.child);
188
+ break;
189
+ case "json":
190
+ hasher.text(JSON.stringify(column.values));
191
+ break;
192
+ default:
193
+ unsupportedDtype(dtype);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * The digest of a column's contents (data, validity, dictionary, strings, child, json text).
199
+ * @param column - the column to hash
200
+ * @returns the 16-character hex digest
201
+ */
202
+ export function hashColumn(column: Column): string {
203
+ if (isColumnDetached(column)) {
204
+ throw new GraphFormatError("E_DETACHED", `column "${column.meta.name}" was transferred away`, {
205
+ column: column.meta.name,
206
+ });
207
+ }
208
+ const hasher = new Fnv1aHasher();
209
+ mixColumn(hasher, column);
210
+ return hasher.digest();
211
+ }
212
+
213
+ /**
214
+ * The content hash of a snapshot's topology (design section 9.3): `directed`, the four counts, then
215
+ * `rowPtr`, `colIdx`, `weights` (raw f32 bit patterns; an absent array hashes as length 0),
216
+ * `arcToEdge` and `edgeToArc`, with an identity permutation hashed as 0..len-1 without being
217
+ * materialised (P11: the digest does not depend on which getters were touched).
218
+ * @param snapshot - the snapshot to hash
219
+ * @returns the 16-character hex digest
220
+ */
221
+ export function contentHashOf(snapshot: GraphSnapshot): string {
222
+ const hasher = new Fnv1aHasher();
223
+ hasher
224
+ .word(snapshot.directed ? 1 : 0)
225
+ .word(snapshot.nodeCount)
226
+ .word(snapshot.edgeCount)
227
+ .word(snapshot.arcCount)
228
+ .word(snapshot.selfLoopCount);
229
+ hasher.words(snapshot.rowPtr).words(snapshot.colIdx);
230
+ if (snapshot.weights === null) {
231
+ hasher.word(0);
232
+ } else {
233
+ hasher.bytes(snapshot.weights);
234
+ }
235
+ if (snapshot.flags.arcToEdgeIsIdentity) {
236
+ hasher.identity(snapshot.arcCount).identity(snapshot.edgeCount);
237
+ } else {
238
+ hasher.words(snapshot.arcToEdge).words(snapshot.edgeToArc);
239
+ }
240
+ return hasher.digest();
241
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The row-ordering queries of design section 3.9 as pure functions over the core arrays: binary
3
+ * searches over sorted rows (invariant I4) for `findArc` / `hasArc` / `arcsBetween` /
4
+ * `multiplicity` / `selfLoopsAt`, and the binary search over `rowPtr` behind `arcSource`. Every
5
+ * function is total for in-range arguments and unchecked for out-of-range ones (design section
6
+ * 11.1: typed-array semantics, no bounds checks in hot paths). GraphSnapshot's methods of the same
7
+ * names delegate here; the views and the derived graphs reuse the searches directly.
8
+ *
9
+ * Multigraph semantics (design section 3.5): parallel arcs are adjacent and ordered by logical edge
10
+ * index, so the first arc of `[lo, hi)` is the lowest logical edge index among the parallels.
11
+ */
12
+
13
+ import { INVALID_INDEX } from "../constants.js";
14
+ import { type U32 } from "../types/index.js";
15
+
16
+ /**
17
+ * The first index in [lo, hi) whose value is >= v, or hi when none is (the sorted-row lower bound).
18
+ * @param colIdx - the sorted targets
19
+ * @param lo - the start of the row
20
+ * @param hi - one past the end of the row
21
+ * @param v - the target to search for
22
+ * @returns the lower bound
23
+ */
24
+ export function lowerBound(colIdx: U32, lo: number, hi: number, v: number): number {
25
+ let low = lo;
26
+ let high = hi;
27
+ while (low < high) {
28
+ const mid = low + Math.floor((high - low) / 2);
29
+ if (colIdx[mid] < v) {
30
+ low = mid + 1;
31
+ } else {
32
+ high = mid;
33
+ }
34
+ }
35
+ return low;
36
+ }
37
+
38
+ /**
39
+ * The first index in [lo, hi) whose value is > v, or hi when none is (the sorted-row upper bound).
40
+ * @param colIdx - the sorted targets
41
+ * @param lo - the start of the row
42
+ * @param hi - one past the end of the row
43
+ * @param v - the target to search for
44
+ * @returns the upper bound
45
+ */
46
+ export function upperBound(colIdx: U32, lo: number, hi: number, v: number): number {
47
+ let low = lo;
48
+ let high = hi;
49
+ while (low < high) {
50
+ const mid = low + Math.floor((high - low) / 2);
51
+ if (colIdx[mid] <= v) {
52
+ low = mid + 1;
53
+ } else {
54
+ high = mid;
55
+ }
56
+ }
57
+ return low;
58
+ }
59
+
60
+ /**
61
+ * The first arc u -> v (the lowest logical edge index among parallels), or INVALID_INDEX.
62
+ * @param rowPtr - the row offsets
63
+ * @param colIdx - the sorted targets
64
+ * @param u - the source node index
65
+ * @param v - the target node index
66
+ * @returns the arc index or INVALID_INDEX
67
+ */
68
+ export function findArcIn(rowPtr: U32, colIdx: U32, u: number, v: number): number {
69
+ const hi = rowPtr[u + 1];
70
+ const at = lowerBound(colIdx, rowPtr[u], hi, v);
71
+ return at < hi && colIdx[at] === v ? at : INVALID_INDEX;
72
+ }
73
+
74
+ /**
75
+ * The half-open arc range [lo, hi) of every arc u -> v; empty (lo === hi) when there is none.
76
+ * @param rowPtr - the row offsets
77
+ * @param colIdx - the sorted targets
78
+ * @param u - the source node index
79
+ * @param v - the target node index
80
+ * @returns the range as a fresh two-element tuple
81
+ */
82
+ export function arcRangeIn(rowPtr: U32, colIdx: U32, u: number, v: number): [lo: number, hi: number] {
83
+ const start = rowPtr[u];
84
+ const end = rowPtr[u + 1];
85
+ const lo = lowerBound(colIdx, start, end, v);
86
+ const hi = upperBound(colIdx, lo, end, v);
87
+ return [lo, hi];
88
+ }
89
+
90
+ /**
91
+ * The number of parallel arcs u -> v.
92
+ * @param rowPtr - the row offsets
93
+ * @param colIdx - the sorted targets
94
+ * @param u - the source node index
95
+ * @param v - the target node index
96
+ * @returns hi - lo of the arc range
97
+ */
98
+ export function multiplicityIn(rowPtr: U32, colIdx: U32, u: number, v: number): number {
99
+ const start = rowPtr[u];
100
+ const end = rowPtr[u + 1];
101
+ const lo = lowerBound(colIdx, start, end, v);
102
+ return upperBound(colIdx, lo, end, v) - lo;
103
+ }
104
+
105
+ /**
106
+ * The row containing an arc: the largest u with rowPtr[u] <= a < rowPtr[u + 1], found by binary
107
+ * search over rowPtr (empty rows are skipped because their two offsets are equal).
108
+ * @param rowPtr - the row offsets, nodeCount + 1 entries
109
+ * @param a - the arc index, below rowPtr[nodeCount]
110
+ * @returns the source node index
111
+ */
112
+ export function arcSourceIn(rowPtr: U32, a: number): number {
113
+ // first index i with rowPtr[i] > a; the row is i - 1
114
+ let low = 0;
115
+ let high = rowPtr.length;
116
+ while (low < high) {
117
+ const mid = low + Math.floor((high - low) / 2);
118
+ if (rowPtr[mid] <= a) {
119
+ low = mid + 1;
120
+ } else {
121
+ high = mid;
122
+ }
123
+ }
124
+ return low - 1;
125
+ }
126
+
127
+ /**
128
+ * The number of self-loop arcs at a node: the multiplicity of u -> u.
129
+ * @param rowPtr - the row offsets
130
+ * @param colIdx - the sorted targets
131
+ * @param u - the node index
132
+ * @returns the loop count
133
+ */
134
+ export function selfLoopsAtIn(rowPtr: U32, colIdx: U32, u: number): number {
135
+ return multiplicityIn(rowPtr, colIdx, u, u);
136
+ }