@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,1486 @@
1
+ /**
2
+ * Derived graphs (design section 7.3): `toUndirected`, `transpose`, `simplified`, `withoutSelfLoops`,
3
+ * `filterEdges`, `inducedSubgraph`, `contract`, `relabel` and `withColumns`, each returning the
4
+ * `SnapshotParts` of a NEW snapshot plus the index maps back to the source (`DerivedParts`), and the
5
+ * `renumberPartition` helper of design section 7.5. GraphSnapshot wraps the parts with
6
+ * `createSnapshot()`; this module imports the class type only, so there is no runtime cycle.
7
+ *
8
+ * Every derived core is built by `buildCore()`, the two stable counting-sort passes of design section
9
+ * 6.3 steps 3-6 over a per-logical-edge list (src, dst, weight) in declared orientation, into a fresh
10
+ * 256-aligned arena (section 10.3), so every derived snapshot inherits invariants I1-I10 by
11
+ * construction and `flags` are computed by the one predicate set in validate.ts (I9).
12
+ *
13
+ * Index-map conventions (design section 7.3): `nodeOrigin` / `edgeOrigin` are NEW -> SOURCE,
14
+ * `nodeRemap` / `edgeRemap` SOURCE -> NEW with INVALID_INDEX for a dropped row and the SURVIVOR's new
15
+ * index for a merged edge; each map is null exactly when its index space is unchanged (same length,
16
+ * identity). New edges are numbered in ascending order of their survivor's source index, so relative
17
+ * edge order is preserved (I14). Attribute tables follow the propagation table of design section
18
+ * 5.11: same node space -> the SAME `AttributeTable` instance; gathered otherwise; `refersTo` values
19
+ * rewritten through the matching remap.
20
+ */
21
+
22
+ import { sortIntoCore } from "../builder/counting-sort.js";
23
+ import { bitmapClear, makeBitmap } from "../columns/bitmap.js";
24
+ import { createColumn, emptyParts, resolveColumnMeta } from "../columns/column.js";
25
+ import { gatherColumn, gatherTable, remapTable } from "../columns/remap.js";
26
+ import { AttributeTable as AttributeTableClass, tableWithColumns } from "../columns/table.js";
27
+ import { INVALID_INDEX } from "../constants.js";
28
+ import { GraphFormatError } from "../errors.js";
29
+ import { gatherNodeIdMap, identityNodeIdMap } from "../ids/node-id-map.js";
30
+ import {
31
+ type ArenaLayout,
32
+ type AttributeTable,
33
+ type Column,
34
+ type ColumnInput,
35
+ type ColumnReducer,
36
+ type ContractOptions,
37
+ type EdgeListView,
38
+ type EdgeMask,
39
+ type F32,
40
+ type GraphSnapshot,
41
+ type NodeIdMap,
42
+ type NodeMask,
43
+ type ReverseView,
44
+ type SimplifyOptions,
45
+ type SnapshotFlags,
46
+ type ToUndirectedOptions,
47
+ type TypedArrayData,
48
+ type U32,
49
+ type WeightReducer,
50
+ } from "../types/index.js";
51
+ import { type SnapshotParts } from "../types/internal.js";
52
+ import { checkMaskLength, maskTest, maskToIndices } from "../util/mask.js";
53
+ import { assertOneOf, COLUMN_REDUCERS, WEIGHT_REDUCERS } from "../util/options.js";
54
+ import { noteShared } from "../util/shared-buffers.js";
55
+ import { arcRangeIn } from "./queries.js";
56
+ import { computeFlags, isIdentity } from "./validate.js";
57
+
58
+ // ============================================================ result shape
59
+
60
+ /** The report of a derived graph: how many source edges were dropped and how many merged into a survivor. */
61
+ interface DerivedReport {
62
+ /** Source edges with no counterpart in the derived graph. */
63
+ readonly droppedEdges: number;
64
+ /** Source edges collapsed into another edge (the survivor). */
65
+ readonly mergedEdges: number;
66
+ }
67
+
68
+ /**
69
+ * What a derive function returns: the parts of the new snapshot (null when the operation is the
70
+ * identity and the caller returns the source itself) and the maps of `DerivedGraph`.
71
+ */
72
+ export interface DerivedParts {
73
+ /** The parts of the new snapshot, or null for "return this". */
74
+ readonly parts: SnapshotParts | null;
75
+ /** New node index -> source node index, or null when the node space is unchanged. */
76
+ readonly nodeOrigin: U32 | null;
77
+ /** New edge index -> source edge index (survivor), or null when the edge space is unchanged. */
78
+ readonly edgeOrigin: U32 | null;
79
+ /** Source node index -> new node index or INVALID_INDEX, or null when unchanged. */
80
+ readonly nodeRemap: U32 | null;
81
+ /** Source edge index -> new edge index (survivor) or INVALID_INDEX, or null when unchanged. */
82
+ readonly edgeRemap: U32 | null;
83
+ /** contract only: source nodes per block. */
84
+ readonly blockSizes: U32 | null;
85
+ /** Dropped and merged edge counts. */
86
+ readonly report: DerivedReport;
87
+ }
88
+
89
+ const NO_REPORT: DerivedReport = Object.freeze({ droppedEdges: 0, mergedEdges: 0 });
90
+
91
+ /**
92
+ * The DerivedParts of an identity operation: the caller returns the source snapshot itself.
93
+ * @returns parts null, every map null, an empty report
94
+ */
95
+ export function identityDerived(): DerivedParts {
96
+ return {
97
+ parts: null,
98
+ nodeOrigin: null,
99
+ edgeOrigin: null,
100
+ nodeRemap: null,
101
+ edgeRemap: null,
102
+ blockSizes: null,
103
+ report: NO_REPORT,
104
+ };
105
+ }
106
+
107
+ // ============================================================ core builder (6.3 steps 3-6, 10.3)
108
+
109
+ /** A per-logical-edge description of a graph in declared orientation: the input of `buildCore()`. */
110
+ interface EdgeArrays {
111
+ /** Whether the graph is directed. */
112
+ readonly directed: boolean;
113
+ /** The node count. */
114
+ readonly nodeCount: number;
115
+ /** The logical edge count. */
116
+ readonly edgeCount: number;
117
+ /** Declared source of every edge. */
118
+ readonly src: U32;
119
+ /** Declared target of every edge. */
120
+ readonly dst: U32;
121
+ /** Per-edge f32 weights, or null when unweighted. */
122
+ readonly weights: F32 | null;
123
+ }
124
+
125
+ /** The core a `buildCore()` call produces: exactly the core fields of SnapshotParts. */
126
+ interface BuiltCore {
127
+ /** colIdx.length. */
128
+ readonly arcCount: number;
129
+ /** Logical edges with source === target. */
130
+ readonly selfLoopCount: number;
131
+ /** nodeCount + 1 row offsets. */
132
+ readonly rowPtr: U32;
133
+ /** Sorted targets. */
134
+ readonly colIdx: U32;
135
+ /** Per-arc weights, or null. */
136
+ readonly weights: F32 | null;
137
+ /** arcCount entries, or null when the permutation is the identity. */
138
+ readonly arcToEdge: U32 | null;
139
+ /** edgeCount entries, or null when the permutation is the identity. */
140
+ readonly edgeToArc: U32 | null;
141
+ /** The truthful flags (I9). */
142
+ readonly flags: SnapshotFlags;
143
+ /** The arena the arrays are views into, or null when `arena: false`. */
144
+ readonly arena: ArenaLayout | null;
145
+ }
146
+
147
+ /** Options of `buildCore()`. */
148
+ interface BuildCoreOptions {
149
+ /** Allocate the arrays inside one 256-aligned arena (default true) or as separate buffers. */
150
+ readonly arena?: boolean | undefined;
151
+ }
152
+
153
+ /**
154
+ * Build the core of a derived graph from a per-logical-edge list in declared orientation: the two
155
+ * stable counting-sort passes of design section 6.3 steps 3-6 (`sortIntoCore` of the builder module,
156
+ * so derived graphs and freezes share one implementation), into a fresh 256-aligned arena unless
157
+ * `arena: false`. Invariants I1-I10 hold by construction and the flags are computed from the stored
158
+ * values (I9). A NaN weight (a reducer summing +Infinity and -Infinity) is E_INVALID_WEIGHT (I8).
159
+ * @param edges - the per-edge arrays; only the first `edgeCount` entries are read
160
+ * @param options - arena or separate buffers
161
+ * @returns the core, the counts and the flags
162
+ */
163
+ export function buildCore(edges: EdgeArrays, options: BuildCoreOptions = {}): BuiltCore {
164
+ const result = sortIntoCore(edges, options.arena !== false);
165
+ const { core } = result;
166
+ return {
167
+ arcCount: result.arcCount,
168
+ selfLoopCount: result.selfLoopCount,
169
+ rowPtr: core.rowPtr,
170
+ colIdx: core.colIdx,
171
+ weights: core.weights,
172
+ arcToEdge: core.arcToEdge,
173
+ edgeToArc: core.edgeToArc,
174
+ flags: result.flags,
175
+ arena: core.arena,
176
+ };
177
+ }
178
+
179
+ // ============================================================ assembling parts
180
+
181
+ /** The side structures of a derived snapshot. */
182
+ interface DerivedSides {
183
+ readonly directed: boolean;
184
+ readonly nodeCount: number;
185
+ readonly edgeCount: number;
186
+ readonly ids: NodeIdMap;
187
+ readonly nodes: AttributeTable;
188
+ readonly edges: AttributeTable;
189
+ readonly extensions: ReadonlyMap<string, AttributeTable>;
190
+ }
191
+
192
+ /**
193
+ * Combine a built core with the side structures into SnapshotParts (serial null: a new core identity;
194
+ * checksum false: the caller substitutes its own setting).
195
+ * @param source - the source snapshot (label, meta and graph table are carried over)
196
+ * @param core - the built core
197
+ * @param sides - the side structures
198
+ * @returns the parts
199
+ */
200
+ function assembleParts(source: GraphSnapshot, core: BuiltCore, sides: DerivedSides): SnapshotParts {
201
+ return {
202
+ label: source.label,
203
+ serial: null,
204
+ directed: sides.directed,
205
+ nodeCount: sides.nodeCount,
206
+ edgeCount: sides.edgeCount,
207
+ arcCount: core.arcCount,
208
+ selfLoopCount: core.selfLoopCount,
209
+ rowPtr: core.rowPtr,
210
+ colIdx: core.colIdx,
211
+ weights: core.weights,
212
+ arcToEdge: core.arcToEdge,
213
+ edgeToArc: core.edgeToArc,
214
+ flags: core.flags,
215
+ ids: sides.ids,
216
+ nodes: sides.nodes,
217
+ edges: sides.edges,
218
+ graph: source.graph,
219
+ extensions: sides.extensions,
220
+ meta: source.meta,
221
+ arena: core.arena,
222
+ checksum: false,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Extension tables with refersTo values rewritten through the remaps; the source map itself when
228
+ * neither space changed.
229
+ * @param source - the source snapshot
230
+ * @param nodeRemap - the node remap or null
231
+ * @param edgeRemap - the edge remap or null
232
+ * @returns the extension map to use
233
+ */
234
+ function remapExtensions(
235
+ source: GraphSnapshot,
236
+ nodeRemap: U32 | null,
237
+ edgeRemap: U32 | null,
238
+ ): ReadonlyMap<string, AttributeTable> {
239
+ if ((nodeRemap === null && edgeRemap === null) || source.extensions.size === 0) {
240
+ return source.extensions;
241
+ }
242
+ const out = new Map<string, AttributeTable>();
243
+ for (const [name, table] of source.extensions) {
244
+ out.set(name, remapTable(table, null, table.rowCount, { node: nodeRemap, edge: edgeRemap }));
245
+ }
246
+ return out;
247
+ }
248
+
249
+ // ============================================================ edge selection and grouping
250
+
251
+ /**
252
+ * A grouping of source edges into new edges: `remap` (source -> new or INVALID_INDEX), `origin` (new
253
+ * -> survivor = lowest source index of the group) and the new count. Null maps mean "unchanged".
254
+ */
255
+ interface EdgeGrouping {
256
+ readonly count: number;
257
+ readonly remap: U32 | null;
258
+ readonly origin: U32 | null;
259
+ readonly dropped: number;
260
+ readonly merged: number;
261
+ }
262
+
263
+ /**
264
+ * Build the new -> survivor map and count from a source -> new remap whose survivors are numbered in
265
+ * ascending source order.
266
+ * @param remap - source edge -> new edge or INVALID_INDEX, survivors ascending
267
+ * @param count - the new edge count
268
+ * @returns the origin map
269
+ */
270
+ function originOf(remap: U32, count: number): U32 {
271
+ const origin = new Uint32Array(count).fill(INVALID_INDEX);
272
+ for (let e = 0; e < remap.length; e++) {
273
+ const d = remap[e];
274
+ if (d !== INVALID_INDEX && origin[d] === INVALID_INDEX) {
275
+ origin[d] = e;
276
+ }
277
+ }
278
+ return origin;
279
+ }
280
+
281
+ /**
282
+ * Turn a per-edge "survivor of e" array (`INVALID_INDEX` = dropped, `e` itself = survivor, another
283
+ * index = merged into that survivor) into an EdgeGrouping with new indices in ascending survivor order.
284
+ * @param survivorOf - source edge -> its survivor, or INVALID_INDEX
285
+ * @returns the grouping; maps null when every edge survives on its own
286
+ */
287
+ function groupingFromSurvivors(survivorOf: U32): EdgeGrouping {
288
+ const edgeCount = survivorOf.length;
289
+ const newIndex = new Uint32Array(edgeCount).fill(INVALID_INDEX);
290
+ let count = 0;
291
+ let dropped = 0;
292
+ let merged = 0;
293
+ for (let e = 0; e < edgeCount; e++) {
294
+ if (survivorOf[e] === e) {
295
+ newIndex[e] = count++;
296
+ } else if (survivorOf[e] === INVALID_INDEX) {
297
+ dropped++;
298
+ } else {
299
+ merged++;
300
+ }
301
+ }
302
+ if (dropped === 0 && merged === 0) {
303
+ return { count, remap: null, origin: null, dropped, merged };
304
+ }
305
+ const remap = new Uint32Array(edgeCount);
306
+ for (let e = 0; e < edgeCount; e++) {
307
+ const s = survivorOf[e];
308
+ remap[e] = s === INVALID_INDEX ? INVALID_INDEX : newIndex[s];
309
+ }
310
+ return { count, remap, origin: originOf(remap, count), dropped, merged };
311
+ }
312
+
313
+ /**
314
+ * Whether two arrays of node indices are the identity selection.
315
+ * @param origin - new -> source
316
+ * @param nodeCount - the source node count
317
+ * @returns true when origin is 0..nodeCount-1
318
+ */
319
+ function isIdentitySelection(origin: U32, nodeCount: number): boolean {
320
+ return origin.length === nodeCount && isIdentity(origin);
321
+ }
322
+
323
+ /**
324
+ * The declared endpoints and weights of the surviving edges, endpoints mapped through `nodeRemap`
325
+ * when given, in new-index order.
326
+ * @param list - the source edge list
327
+ * @param grouping - the grouping (origin null = all edges in order)
328
+ * @param nodeRemap - source node -> new node, or null
329
+ * @returns the edge arrays without weights (weights are reduced separately)
330
+ */
331
+ function gatherEndpoints(list: EdgeListView, grouping: EdgeGrouping, nodeRemap: U32 | null): { src: U32; dst: U32 } {
332
+ const { count, origin } = grouping;
333
+ const src = new Uint32Array(count);
334
+ const dst = new Uint32Array(count);
335
+ for (let d = 0; d < count; d++) {
336
+ const e = origin === null ? d : origin[d];
337
+ src[d] = nodeRemap === null ? list.src[e] : nodeRemap[list.src[e]];
338
+ dst[d] = nodeRemap === null ? list.dst[e] : nodeRemap[list.dst[e]];
339
+ }
340
+ return { src, dst };
341
+ }
342
+
343
+ /**
344
+ * Check every reducer of a per-column reducer record (design section 7.3) before anything is built.
345
+ * @param field - the option name
346
+ * @param reducers - the record, or undefined
347
+ */
348
+ function checkReducers(field: string, reducers: Readonly<Record<string, unknown>> | undefined): void {
349
+ if (reducers === undefined) {
350
+ return;
351
+ }
352
+ for (const name of Object.keys(reducers)) {
353
+ assertOneOf(`${field}.${name}`, reducers[name], COLUMN_REDUCERS);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Reduce per-edge weights over a grouping (design section 7.3): `"first"` keeps the survivor's
359
+ * (lowest index) weight, `"last"` the highest index's, `"sum"` / `"min"` / `"max"` combine the group.
360
+ * On an unweighted source `"sum"` yields the multiplicities as an F32 and every other reducer null.
361
+ * @param weights - the per-edge source weights, or null
362
+ * @param grouping - the grouping
363
+ * @param reducer - the weight reducer
364
+ * @returns the per-new-edge weights, or null
365
+ */
366
+ function reduceWeights(weights: F32 | null, grouping: EdgeGrouping, reducer: WeightReducer): F32 | null {
367
+ const { count, remap, origin } = grouping;
368
+ if (weights === null) {
369
+ if (reducer !== "sum") {
370
+ return null;
371
+ }
372
+ const out = new Float32Array(count);
373
+ if (remap === null) {
374
+ out.fill(1);
375
+ return out;
376
+ }
377
+ for (let e = 0; e < remap.length; e++) {
378
+ if (remap[e] !== INVALID_INDEX) {
379
+ out[remap[e]]++;
380
+ }
381
+ }
382
+ return out;
383
+ }
384
+ if (remap === null || origin === null) {
385
+ return weights;
386
+ }
387
+ const out = new Float32Array(count);
388
+ for (let d = 0; d < count; d++) {
389
+ out[d] = weights[origin[d]];
390
+ }
391
+ if (reducer === "first") {
392
+ return out;
393
+ }
394
+ for (let e = 0; e < remap.length; e++) {
395
+ const d = remap[e];
396
+ if (d === INVALID_INDEX || origin[d] === e) {
397
+ continue;
398
+ }
399
+ const w = weights[e];
400
+ switch (reducer) {
401
+ case "last":
402
+ out[d] = w;
403
+ break;
404
+ case "sum":
405
+ out[d] += w;
406
+ break;
407
+ case "min":
408
+ if (w < out[d]) {
409
+ out[d] = w;
410
+ }
411
+ break;
412
+ case "max":
413
+ if (w > out[d]) {
414
+ out[d] = w;
415
+ }
416
+ break;
417
+ default: {
418
+ const unknown: never = reducer;
419
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown weight reducer ${String(unknown)}`, {
420
+ reducer: unknown,
421
+ });
422
+ }
423
+ }
424
+ }
425
+ return out;
426
+ }
427
+
428
+ /**
429
+ * The grouping of a per-edge keep predicate: kept edges survive on their own in order, others are
430
+ * dropped.
431
+ * @param edgeCount - the source edge count
432
+ * @param keep - whether edge e is kept
433
+ * @returns the grouping
434
+ */
435
+ function groupingFromPredicate(edgeCount: number, keep: (e: number) => boolean): EdgeGrouping {
436
+ const survivorOf = new Uint32Array(edgeCount);
437
+ for (let e = 0; e < edgeCount; e++) {
438
+ survivorOf[e] = keep(e) ? e : INVALID_INDEX;
439
+ }
440
+ return groupingFromSurvivors(survivorOf);
441
+ }
442
+
443
+ // ============================================================ column reduction
444
+
445
+ /** The five flat numeric column shapes the group reducers accumulate. */
446
+ type NumericColumn = Extract<Column, { dtype: "f32" | "f64" | "i32" | "u32" | "u8" }>;
447
+
448
+ /**
449
+ * A group reducer's numeric accumulation for one column into a new row space.
450
+ * @param column - a numeric column
451
+ * @param grouping - the grouping (remap non-null)
452
+ * @param reducer - sum / min / max / mean
453
+ * @returns the reduced column
454
+ */
455
+ function reduceNumericColumn(
456
+ column: NumericColumn,
457
+ grouping: EdgeGrouping,
458
+ reducer: "sum" | "min" | "max" | "mean",
459
+ ): Column {
460
+ const { count, remap } = grouping;
461
+ const { components } = column.meta;
462
+ const width = components;
463
+ const acc = new Float64Array(count * width);
464
+ const members = new Uint32Array(count);
465
+ const seen = new Uint8Array(count);
466
+ const source = column.data;
467
+ const rows = remap === null ? column.length : remap.length;
468
+ for (let r = 0; r < rows; r++) {
469
+ const d = remap === null ? r : remap[r];
470
+ if (d === INVALID_INDEX || !column.isSet(r)) {
471
+ continue;
472
+ }
473
+ members[d]++;
474
+ for (let k = 0; k < width; k++) {
475
+ const value = source[r * width + k];
476
+ const at = d * width + k;
477
+ if (seen[d] === 0) {
478
+ acc[at] = value;
479
+ } else {
480
+ switch (reducer) {
481
+ case "sum":
482
+ case "mean":
483
+ acc[at] += value;
484
+ break;
485
+ case "min":
486
+ if (value < acc[at]) {
487
+ acc[at] = value;
488
+ }
489
+ break;
490
+ case "max":
491
+ if (value > acc[at]) {
492
+ acc[at] = value;
493
+ }
494
+ break;
495
+ default: {
496
+ const unknown: never = reducer;
497
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown reducer ${String(unknown)}`, {
498
+ reducer: unknown,
499
+ });
500
+ }
501
+ }
502
+ }
503
+ }
504
+ seen[d] = 1;
505
+ }
506
+ if (reducer === "mean") {
507
+ for (let d = 0; d < count; d++) {
508
+ if (members[d] > 1) {
509
+ for (let k = 0; k < width; k++) {
510
+ acc[d * width + k] /= members[d];
511
+ }
512
+ }
513
+ }
514
+ }
515
+ // "mean" is f64; a "sum" over an integer dtype widens to f64 too, so a total can never wrap
516
+ // silently in a typed array that every write path would have refused (design section 5.1)
517
+ const widen = reducer === "mean" || (reducer === "sum" && column.dtype !== "f32" && column.dtype !== "f64");
518
+ const dtype = widen ? "f64" : column.dtype;
519
+ let data: TypedArrayData;
520
+ switch (dtype) {
521
+ case "f64":
522
+ data = acc;
523
+ break;
524
+ case "f32":
525
+ data = new Float32Array(acc);
526
+ break;
527
+ case "i32":
528
+ data = new Int32Array(acc);
529
+ break;
530
+ case "u32":
531
+ data = new Uint32Array(acc);
532
+ break;
533
+ case "u8":
534
+ data = new Uint8Array(new ArrayBuffer(Math.ceil((count * width) / 4) * 4), 0, count * width);
535
+ data.set(acc);
536
+ break;
537
+ default: {
538
+ const unknown: never = dtype;
539
+ throw new GraphFormatError("E_COLUMN_TYPE", `cannot reduce a ${String(unknown)} column`, {
540
+ dtype: unknown,
541
+ });
542
+ }
543
+ }
544
+ let validity: U32 | null = null;
545
+ let nullCount = 0;
546
+ for (let d = 0; d < count; d++) {
547
+ if (seen[d] === 0) {
548
+ validity ??= makeBitmap(count, true);
549
+ bitmapClear(validity, d);
550
+ nullCount++;
551
+ }
552
+ }
553
+ const meta = resolveColumnMeta(column.meta.name, column.meta.domain, {
554
+ dtype,
555
+ components,
556
+ nullable: column.meta.nullable || nullCount > 0,
557
+ role: column.meta.role ?? undefined,
558
+ extra: column.meta.extra,
559
+ });
560
+ return createColumn({ ...emptyParts(meta, count, validity), data, nullCount });
561
+ }
562
+
563
+ /**
564
+ * The u32 column of group sizes ("count").
565
+ * @param column - the source column (name, domain, role and extra are kept)
566
+ * @param grouping - the grouping
567
+ * @returns the count column
568
+ */
569
+ function countColumn(column: Column, grouping: EdgeGrouping): Column {
570
+ const { count, remap } = grouping;
571
+ const sizes = new Uint32Array(count);
572
+ if (remap === null) {
573
+ sizes.fill(1);
574
+ } else {
575
+ for (let r = 0; r < remap.length; r++) {
576
+ if (remap[r] !== INVALID_INDEX) {
577
+ sizes[remap[r]]++;
578
+ }
579
+ }
580
+ }
581
+ const meta = resolveColumnMeta(column.meta.name, column.meta.domain, {
582
+ dtype: "u32",
583
+ nullable: false,
584
+ role: column.meta.role ?? undefined,
585
+ extra: column.meta.extra,
586
+ });
587
+ return createColumn({ ...emptyParts(meta, count, null), data: sizes });
588
+ }
589
+
590
+ /**
591
+ * Reduce one column over a grouping with a ColumnReducer (design section 7.3): `"first"` / `"last"`
592
+ * gather the lowest / highest member's row (any dtype), `"count"` yields the group sizes, `"sum"` /
593
+ * `"min"` / `"max"` / `"mean"` combine the set members of a numeric column (`"mean"` as f64, and so
594
+ * is a `"sum"` over an i32 / u32 / u8 column, which would otherwise wrap; `"min"` / `"max"` keep
595
+ * the dtype since the result is a member), `"drop"` omits the column. Index-valued (refersTo)
596
+ * columns are always dropped: their values would refer to the source space.
597
+ * @param column - the source column
598
+ * @param grouping - the grouping
599
+ * @param reducer - the reducer
600
+ * @returns the reduced column, or null to drop it; E_COLUMN_TYPE for a numeric reducer on a non-numeric column
601
+ */
602
+ function reduceColumn(column: Column, grouping: EdgeGrouping, reducer: ColumnReducer): Column | null {
603
+ if (reducer === "drop" || column.meta.refersTo !== null) {
604
+ return null;
605
+ }
606
+ const { count, remap, origin } = grouping;
607
+ switch (reducer) {
608
+ case "first":
609
+ return origin === null ? column : gatherColumn(column, origin);
610
+ case "last": {
611
+ if (remap === null) {
612
+ return column;
613
+ }
614
+ const last = new Uint32Array(count);
615
+ for (let r = 0; r < remap.length; r++) {
616
+ if (remap[r] !== INVALID_INDEX) {
617
+ last[remap[r]] = r;
618
+ }
619
+ }
620
+ return gatherColumn(column, last);
621
+ }
622
+ case "count":
623
+ return countColumn(column, grouping);
624
+ case "sum":
625
+ case "min":
626
+ case "max":
627
+ case "mean": {
628
+ const { dtype } = column;
629
+ switch (dtype) {
630
+ case "f32":
631
+ case "f64":
632
+ case "i32":
633
+ case "u32":
634
+ case "u8":
635
+ return reduceNumericColumn(column, grouping, reducer);
636
+ case "bool":
637
+ case "dict":
638
+ case "string":
639
+ case "list":
640
+ case "json":
641
+ throw new GraphFormatError(
642
+ "E_COLUMN_TYPE",
643
+ `reducer "${reducer}" needs a numeric column; "${column.meta.name}" is ${dtype}`,
644
+ { column: column.meta.name, dtype, reducer },
645
+ );
646
+ default: {
647
+ const unknown: never = dtype;
648
+ throw new GraphFormatError("E_COLUMN_TYPE", `unknown dtype ${String(unknown)}`, { dtype: unknown });
649
+ }
650
+ }
651
+ }
652
+ default: {
653
+ const unknown: never = reducer;
654
+ throw new GraphFormatError("E_UNSUPPORTED", `unknown column reducer ${String(unknown)}`, {
655
+ reducer: unknown,
656
+ });
657
+ }
658
+ }
659
+ }
660
+
661
+ /**
662
+ * The edge table of a merging derived graph: named columns reduced per `reducers`, every other column
663
+ * gathered from the survivor's row (the default of design section 5.11), refersTo values rewritten.
664
+ * @param source - the source snapshot
665
+ * @param grouping - the edge grouping
666
+ * @param reducers - per-column reducers, or undefined for survivor rows throughout
667
+ * @param nodeRemap - the node remap for refersTo node columns, or null
668
+ * @returns the new edge table (the source table when the edge space is unchanged and no reducer applies)
669
+ */
670
+ function reduceEdgeTable(
671
+ source: GraphSnapshot,
672
+ grouping: EdgeGrouping,
673
+ reducers: Readonly<Record<string, ColumnReducer>> | undefined,
674
+ nodeRemap: U32 | null,
675
+ ): AttributeTable {
676
+ const refs = { node: nodeRemap, edge: grouping.remap };
677
+ if (reducers === undefined || Object.keys(reducers).length === 0) {
678
+ if (grouping.origin === null) {
679
+ return nodeRemap === null ? source.edges : remapTable(source.edges, null, source.edgeCount, refs);
680
+ }
681
+ return gatherTable(source.edges, grouping.origin, refs);
682
+ }
683
+ const columns: Column[] = [];
684
+ for (const column of source.edges) {
685
+ const reducer = reducers[column.meta.name];
686
+ if (reducer === undefined) {
687
+ const gathered = grouping.origin === null ? column : gatherColumn(column, grouping.origin);
688
+ columns.push(gathered);
689
+ continue;
690
+ }
691
+ const reduced = reduceColumn(column, grouping, reducer);
692
+ if (reduced !== null) {
693
+ columns.push(reduced);
694
+ }
695
+ }
696
+ const table = new AttributeTableClass({ domain: "edge", rowCount: grouping.count, columns });
697
+ return remapTable(table, null, grouping.count, refs);
698
+ }
699
+
700
+ // ============================================================ filterEdges, withoutSelfLoops
701
+
702
+ /**
703
+ * The derived graph keeping the edges a predicate accepts, in the same node space (design section
704
+ * 7.3: `filterEdges`, `withoutSelfLoops`). Always a new snapshot; the maps are null when every edge
705
+ * is kept.
706
+ * @param source - the source snapshot
707
+ * @param list - the source edge list
708
+ * @param keep - whether edge e is kept
709
+ * @returns the derived parts
710
+ */
711
+ function deriveKeptEdges(source: GraphSnapshot, list: EdgeListView, keep: (e: number) => boolean): DerivedParts {
712
+ const grouping = groupingFromPredicate(source.edgeCount, keep);
713
+ const { src, dst } = gatherEndpoints(list, grouping, null);
714
+ const weights = reduceWeights(list.weights, grouping, "first");
715
+ const core = buildCore({
716
+ directed: source.directed,
717
+ nodeCount: source.nodeCount,
718
+ edgeCount: grouping.count,
719
+ src,
720
+ dst,
721
+ weights,
722
+ });
723
+ const edges = reduceEdgeTable(source, grouping, undefined, null);
724
+ const parts = assembleParts(source, core, {
725
+ directed: source.directed,
726
+ nodeCount: source.nodeCount,
727
+ edgeCount: grouping.count,
728
+ ids: source.ids,
729
+ nodes: source.nodes,
730
+ edges,
731
+ extensions: remapExtensions(source, null, grouping.remap),
732
+ });
733
+ return {
734
+ parts,
735
+ nodeOrigin: null,
736
+ edgeOrigin: grouping.origin,
737
+ nodeRemap: null,
738
+ edgeRemap: grouping.remap,
739
+ blockSizes: null,
740
+ report: { droppedEdges: grouping.dropped, mergedEdges: 0 },
741
+ };
742
+ }
743
+
744
+ /**
745
+ * `filterEdges(keep)`: the logical edges whose mask bit is set (design section 7.3).
746
+ * @param source - the source snapshot
747
+ * @param list - the source edge list
748
+ * @param keep - packed bitmap over logical edges; E_MASK_LENGTH when short
749
+ * @returns the derived parts
750
+ */
751
+ export function deriveFilterEdges(source: GraphSnapshot, list: EdgeListView, keep: EdgeMask): DerivedParts {
752
+ checkMaskLength(keep, source.edgeCount, "edges");
753
+ return deriveKeptEdges(source, list, (e) => maskTest(keep, e));
754
+ }
755
+
756
+ /**
757
+ * `withoutSelfLoops()`: every edge with source !== target (design section 7.3).
758
+ * @param source - the source snapshot
759
+ * @param list - the source edge list
760
+ * @returns the derived parts
761
+ */
762
+ export function deriveWithoutSelfLoops(source: GraphSnapshot, list: EdgeListView): DerivedParts {
763
+ return deriveKeptEdges(source, list, (e) => list.src[e] !== list.dst[e]);
764
+ }
765
+
766
+ // ============================================================ inducedSubgraph, relabel
767
+
768
+ /**
769
+ * The E_INDEX_RANGE error of a bad inducedSubgraph index list entry.
770
+ * @param position - the position in the list
771
+ * @param value - the offending value
772
+ * @param nodeCount - the node count
773
+ * @param reason - "out of range" or "repeated"
774
+ * @returns the error
775
+ */
776
+ function selectionError(position: number, value: number, nodeCount: number, reason: string): GraphFormatError {
777
+ return new GraphFormatError(
778
+ "E_INDEX_RANGE",
779
+ `inducedSubgraph selection[${position}] = ${value} is ${reason} (nodeCount ${nodeCount})`,
780
+ { index: position, found: value, nodeCount, reason },
781
+ );
782
+ }
783
+
784
+ /**
785
+ * Resolve an inducedSubgraph selection to the new -> source node list (design section 7.3): an
786
+ * index list in the given order (E_INDEX_RANGE for an out-of-range or repeated index) or a packed
787
+ * mask in ascending order (E_MASK_LENGTH when short).
788
+ * @param source - the source snapshot
789
+ * @param selection - the index list or mask
790
+ * @returns the node origin list (a fresh array)
791
+ */
792
+ function resolveSelection(source: GraphSnapshot, selection: U32 | { readonly mask: NodeMask }): U32 {
793
+ const { nodeCount } = source;
794
+ if (selection instanceof Uint32Array) {
795
+ const seen = new Uint8Array(nodeCount);
796
+ const origin = new Uint32Array(selection.length);
797
+ for (let i = 0; i < selection.length; i++) {
798
+ const u = selection[i];
799
+ if (u >= nodeCount) {
800
+ throw selectionError(i, u, nodeCount, "out of range");
801
+ }
802
+ if (seen[u] === 1) {
803
+ throw selectionError(i, u, nodeCount, "repeated");
804
+ }
805
+ seen[u] = 1;
806
+ origin[i] = u;
807
+ }
808
+ return origin;
809
+ }
810
+ checkMaskLength(selection.mask, nodeCount, "nodes");
811
+ return maskToIndices(selection.mask, nodeCount);
812
+ }
813
+
814
+ /**
815
+ * The inverse of a new -> source node list: source -> new or INVALID_INDEX.
816
+ * @param origin - new -> source
817
+ * @param nodeCount - the source node count
818
+ * @returns the remap
819
+ */
820
+ function invertOrigin(origin: U32, nodeCount: number): U32 {
821
+ const remap = new Uint32Array(nodeCount).fill(INVALID_INDEX);
822
+ for (let i = 0; i < origin.length; i++) {
823
+ remap[origin[i]] = i;
824
+ }
825
+ return remap;
826
+ }
827
+
828
+ /**
829
+ * Assemble a derived graph over a gathered node space (inducedSubgraph, relabel): the node table and
830
+ * id map gathered through `nodeOrigin`, the edge table gathered through the edge grouping (or the
831
+ * source table with refersTo node values rewritten when the edge space is unchanged).
832
+ * @param source - the source snapshot
833
+ * @param list - the source edge list
834
+ * @param nodeOrigin - new -> source node, or null for the identity
835
+ * @param grouping - the edge grouping
836
+ * @returns the derived parts
837
+ */
838
+ function deriveOverNodeSpace(
839
+ source: GraphSnapshot,
840
+ list: EdgeListView,
841
+ nodeOrigin: U32 | null,
842
+ grouping: EdgeGrouping,
843
+ ): DerivedParts {
844
+ const nodeCount = nodeOrigin === null ? source.nodeCount : nodeOrigin.length;
845
+ const nodeRemap = nodeOrigin === null ? null : invertOrigin(nodeOrigin, source.nodeCount);
846
+ const { src, dst } = gatherEndpoints(list, grouping, nodeRemap);
847
+ const weights = reduceWeights(list.weights, grouping, "first");
848
+ const core = buildCore({ directed: source.directed, nodeCount, edgeCount: grouping.count, src, dst, weights });
849
+ const refs = { node: nodeRemap, edge: grouping.remap };
850
+ const nodes = nodeOrigin === null ? source.nodes : gatherTable(source.nodes, nodeOrigin, refs);
851
+ const ids = nodeOrigin === null ? source.ids : gatherNodeIdMap(source.ids, nodeOrigin);
852
+ const edges = reduceEdgeTable(source, grouping, undefined, nodeRemap);
853
+ const parts = assembleParts(source, core, {
854
+ directed: source.directed,
855
+ nodeCount,
856
+ edgeCount: grouping.count,
857
+ ids,
858
+ nodes,
859
+ edges,
860
+ extensions: remapExtensions(source, nodeRemap, grouping.remap),
861
+ });
862
+ return {
863
+ parts,
864
+ nodeOrigin,
865
+ edgeOrigin: grouping.origin,
866
+ nodeRemap,
867
+ edgeRemap: grouping.remap,
868
+ blockSizes: null,
869
+ report: { droppedEdges: grouping.dropped, mergedEdges: grouping.merged },
870
+ };
871
+ }
872
+
873
+ /**
874
+ * `inducedSubgraph(selection)`: the subgraph on a node selection with every edge whose endpoints are
875
+ * both kept (design section 7.3); a compact new node space unless the selection is the identity.
876
+ * @param source - the source snapshot
877
+ * @param list - the source edge list
878
+ * @param selection - an index list (new index order) or a packed mask (ascending)
879
+ * @returns the derived parts
880
+ */
881
+ export function deriveInducedSubgraph(
882
+ source: GraphSnapshot,
883
+ list: EdgeListView,
884
+ selection: U32 | { readonly mask: NodeMask },
885
+ ): DerivedParts {
886
+ const origin = resolveSelection(source, selection);
887
+ const identity = isIdentitySelection(origin, source.nodeCount);
888
+ const nodeOrigin = identity ? null : origin;
889
+ const nodeRemap = identity ? null : invertOrigin(origin, source.nodeCount);
890
+ const grouping =
891
+ nodeRemap === null
892
+ ? groupingFromPredicate(source.edgeCount, () => true)
893
+ : groupingFromPredicate(
894
+ source.edgeCount,
895
+ (e) => nodeRemap[list.src[e]] !== INVALID_INDEX && nodeRemap[list.dst[e]] !== INVALID_INDEX,
896
+ );
897
+ return deriveOverNodeSpace(source, list, nodeOrigin, grouping);
898
+ }
899
+
900
+ /**
901
+ * `relabel(perm)`: the node space permuted with `perm[newIndex] = oldIndex`, edge order preserved
902
+ * (design section 7.3); E_INVALID_PERMUTATION unless perm is a permutation of 0..n-1.
903
+ * @param source - the source snapshot
904
+ * @param list - the source edge list
905
+ * @param perm - the permutation
906
+ * @returns the derived parts
907
+ */
908
+ export function deriveRelabel(source: GraphSnapshot, list: EdgeListView, perm: U32): DerivedParts {
909
+ const { nodeCount } = source;
910
+ if (perm.length !== nodeCount) {
911
+ throw new GraphFormatError("E_INVALID_PERMUTATION", `perm has ${perm.length} entries, expected ${nodeCount}`, {
912
+ expected: nodeCount,
913
+ found: perm.length,
914
+ });
915
+ }
916
+ const seen = new Uint8Array(nodeCount);
917
+ for (let i = 0; i < nodeCount; i++) {
918
+ const u = perm[i];
919
+ if (u >= nodeCount || seen[u] === 1) {
920
+ throw new GraphFormatError(
921
+ "E_INVALID_PERMUTATION",
922
+ `perm[${i}] = ${u} is out of range or repeated (nodeCount ${nodeCount})`,
923
+ { index: i, found: u, nodeCount },
924
+ );
925
+ }
926
+ seen[u] = 1;
927
+ }
928
+ const nodeOrigin = isIdentity(perm) ? null : perm.slice();
929
+ const grouping = groupingFromPredicate(source.edgeCount, () => true);
930
+ return deriveOverNodeSpace(source, list, nodeOrigin, grouping);
931
+ }
932
+
933
+ // ============================================================ toUndirected, transpose
934
+
935
+ /**
936
+ * `toUndirected(options)` on a directed snapshot (design section 7.3): every edge becomes undirected;
937
+ * reciprocal pairs u -> v / v -> u (paired k-th to k-th among parallels, in edge order) collapse to
938
+ * one edge keeping the lower index's row and the reduced weight (default "first"); with
939
+ * `reciprocal: true` only paired edges survive. Self-loops are kept.
940
+ * @param source - a directed snapshot
941
+ * @param list - the source edge list
942
+ * @param options - reciprocal filtering and the weight reducer
943
+ * @returns the derived parts
944
+ */
945
+ export function deriveToUndirected(
946
+ source: GraphSnapshot,
947
+ list: EdgeListView,
948
+ options: ToUndirectedOptions = {},
949
+ ): DerivedParts {
950
+ const reciprocal = options.reciprocal === true;
951
+ const reducer = assertOneOf("weights", options.weights, WEIGHT_REDUCERS) ?? "first";
952
+ const { nodeCount, edgeCount, rowPtr, colIdx, arcToEdge } = source;
953
+ const survivorOf = new Uint32Array(edgeCount);
954
+ for (let e = 0; e < edgeCount; e++) {
955
+ survivorOf[e] = reciprocal && list.src[e] !== list.dst[e] ? INVALID_INDEX : e;
956
+ }
957
+ for (let u = 0; u < nodeCount; u++) {
958
+ const end = rowPtr[u + 1];
959
+ let a = rowPtr[u];
960
+ while (a < end) {
961
+ const v = colIdx[a];
962
+ let g = a + 1;
963
+ while (g < end && colIdx[g] === v) {
964
+ g++;
965
+ }
966
+ if (v > u) {
967
+ const [lo, hi] = arcRangeIn(rowPtr, colIdx, v, u);
968
+ const pairs = Math.min(g - a, hi - lo);
969
+ for (let i = 0; i < pairs; i++) {
970
+ const e1 = arcToEdge[a + i];
971
+ const e2 = arcToEdge[lo + i];
972
+ const low = Math.min(e1, e2);
973
+ const high = Math.max(e1, e2);
974
+ survivorOf[low] = low;
975
+ survivorOf[high] = low;
976
+ }
977
+ }
978
+ a = g;
979
+ }
980
+ }
981
+ const grouping = groupingFromSurvivors(survivorOf);
982
+ const { src, dst } = gatherEndpoints(list, grouping, null);
983
+ const weights = reduceWeights(list.weights, grouping, reducer);
984
+ const core = buildCore({ directed: false, nodeCount, edgeCount: grouping.count, src, dst, weights });
985
+ const edges = reduceEdgeTable(source, grouping, undefined, null);
986
+ const parts = assembleParts(source, core, {
987
+ directed: false,
988
+ nodeCount,
989
+ edgeCount: grouping.count,
990
+ ids: source.ids,
991
+ nodes: source.nodes,
992
+ edges,
993
+ extensions: remapExtensions(source, null, grouping.remap),
994
+ });
995
+ return {
996
+ parts,
997
+ nodeOrigin: null,
998
+ edgeOrigin: grouping.origin,
999
+ nodeRemap: null,
1000
+ edgeRemap: grouping.remap,
1001
+ blockSizes: null,
1002
+ report: { droppedEdges: grouping.dropped, mergedEdges: grouping.merged },
1003
+ };
1004
+ }
1005
+
1006
+ /**
1007
+ * `transpose()` on a directed snapshot (design section 7.3): the orientation of every edge swapped by
1008
+ * adopting the reverse view's arrays as the core (zero copy, so `arena` is null); node and edge
1009
+ * spaces unchanged, tables shared.
1010
+ * @param source - a directed snapshot
1011
+ * @param reverse - its reverse view
1012
+ * @returns the derived parts
1013
+ */
1014
+ export function deriveTranspose(source: GraphSnapshot, reverse: ReverseView): DerivedParts {
1015
+ const { nodeCount, edgeCount, arcCount, selfLoopCount } = source;
1016
+ const revArcToEdge = reverse.arcToEdge;
1017
+ const identity = isIdentity(revArcToEdge);
1018
+ let arcToEdge: U32 | null = null;
1019
+ let edgeToArc: U32 | null = null;
1020
+ if (!identity) {
1021
+ arcToEdge = revArcToEdge;
1022
+ edgeToArc = new Uint32Array(edgeCount);
1023
+ for (let k = 0; k < arcCount; k++) {
1024
+ edgeToArc[revArcToEdge[k]] = k;
1025
+ }
1026
+ }
1027
+ const flags = computeFlags({
1028
+ directed: true,
1029
+ nodeCount,
1030
+ rowPtr: reverse.rowPtr,
1031
+ colIdx: reverse.colIdx,
1032
+ weights: reverse.weights,
1033
+ arcToEdge,
1034
+ selfLoopCount,
1035
+ });
1036
+ // the adopted arrays stay held by the source's cached reverse view (design section 9.1)
1037
+ noteShared(reverse.rowPtr.buffer);
1038
+ noteShared(reverse.colIdx.buffer);
1039
+ if (reverse.weights !== null) {
1040
+ noteShared(reverse.weights.buffer);
1041
+ }
1042
+ if (arcToEdge !== null) {
1043
+ noteShared(arcToEdge.buffer);
1044
+ }
1045
+ const parts: SnapshotParts = {
1046
+ label: source.label,
1047
+ serial: null,
1048
+ directed: true,
1049
+ nodeCount,
1050
+ edgeCount,
1051
+ arcCount,
1052
+ selfLoopCount,
1053
+ rowPtr: reverse.rowPtr,
1054
+ colIdx: reverse.colIdx,
1055
+ weights: reverse.weights,
1056
+ arcToEdge,
1057
+ edgeToArc,
1058
+ flags,
1059
+ ids: source.ids,
1060
+ nodes: source.nodes,
1061
+ edges: source.edges,
1062
+ graph: source.graph,
1063
+ extensions: source.extensions,
1064
+ meta: source.meta,
1065
+ arena: null,
1066
+ checksum: false,
1067
+ };
1068
+ return {
1069
+ parts,
1070
+ nodeOrigin: null,
1071
+ edgeOrigin: null,
1072
+ nodeRemap: null,
1073
+ edgeRemap: null,
1074
+ blockSizes: null,
1075
+ report: NO_REPORT,
1076
+ };
1077
+ }
1078
+
1079
+ // ============================================================ simplified
1080
+
1081
+ /**
1082
+ * `simplified(options)` (design section 7.3): one edge per (u, v) group (parallels are adjacent by
1083
+ * I4; undirected groups are unordered pairs), survivor = lowest index, weights per `weights`
1084
+ * (default "first"), self-loops kept or dropped, other edge columns per `edgeReducers` (default: the
1085
+ * survivor's row). `flags.multigraph` is false afterwards.
1086
+ * @param source - the source snapshot
1087
+ * @param list - the source edge list
1088
+ * @param options - reducers and self-loop policy
1089
+ * @returns the derived parts
1090
+ */
1091
+ export function deriveSimplified(
1092
+ source: GraphSnapshot,
1093
+ list: EdgeListView,
1094
+ options: SimplifyOptions = {},
1095
+ ): DerivedParts {
1096
+ const reducer = assertOneOf("weights", options.weights, WEIGHT_REDUCERS) ?? "first";
1097
+ const dropLoops = assertOneOf("selfLoops", options.selfLoops, ["keep", "drop"] as const) === "drop";
1098
+ checkReducers("edgeReducers", options.edgeReducers);
1099
+ const { nodeCount, edgeCount, rowPtr, colIdx, arcToEdge } = source;
1100
+ const survivorOf = new Uint32Array(edgeCount).fill(INVALID_INDEX);
1101
+ for (let u = 0; u < nodeCount; u++) {
1102
+ const end = rowPtr[u + 1];
1103
+ let a = rowPtr[u];
1104
+ while (a < end) {
1105
+ const v = colIdx[a];
1106
+ let g = a + 1;
1107
+ while (g < end && colIdx[g] === v) {
1108
+ g++;
1109
+ }
1110
+ if (source.directed || v >= u) {
1111
+ if (!(v === u && dropLoops)) {
1112
+ const survivor = arcToEdge[a];
1113
+ for (let i = a; i < g; i++) {
1114
+ survivorOf[arcToEdge[i]] = survivor;
1115
+ }
1116
+ }
1117
+ }
1118
+ a = g;
1119
+ }
1120
+ }
1121
+ const grouping = groupingFromSurvivors(survivorOf);
1122
+ const { src, dst } = gatherEndpoints(list, grouping, null);
1123
+ const weights = reduceWeights(list.weights, grouping, reducer);
1124
+ const core = buildCore({ directed: source.directed, nodeCount, edgeCount: grouping.count, src, dst, weights });
1125
+ const edges = reduceEdgeTable(source, grouping, options.edgeReducers, null);
1126
+ const parts = assembleParts(source, core, {
1127
+ directed: source.directed,
1128
+ nodeCount,
1129
+ edgeCount: grouping.count,
1130
+ ids: source.ids,
1131
+ nodes: source.nodes,
1132
+ edges,
1133
+ extensions: remapExtensions(source, null, grouping.remap),
1134
+ });
1135
+ return {
1136
+ parts,
1137
+ nodeOrigin: null,
1138
+ edgeOrigin: grouping.origin,
1139
+ nodeRemap: null,
1140
+ edgeRemap: grouping.remap,
1141
+ blockSizes: null,
1142
+ report: { droppedEdges: grouping.dropped, mergedEdges: grouping.merged },
1143
+ };
1144
+ }
1145
+
1146
+ // ============================================================ contract
1147
+
1148
+ /**
1149
+ * Renumber an arbitrary u32 labelling to dense 0..k-1 in first-seen order (design section 7.5; what
1150
+ * `contract()` does with non-dense labels). INVALID_INDEX is not a legal label (E_PARTITION).
1151
+ * @param labels - the labels
1152
+ * @param out - an optional destination of the same length (may be `labels` itself)
1153
+ * @returns the dense labels and the block count
1154
+ */
1155
+ export function renumberPartition(labels: U32, out?: U32): { readonly labels: U32; readonly count: number } {
1156
+ const n = labels.length;
1157
+ const result = out ?? new Uint32Array(n);
1158
+ if (result.length !== n) {
1159
+ throw new GraphFormatError("E_COLUMN_LENGTH", `out has ${result.length} entries, expected ${n}`, {
1160
+ expected: n,
1161
+ found: result.length,
1162
+ });
1163
+ }
1164
+ let maxLabel = 0;
1165
+ for (let i = 0; i < n; i++) {
1166
+ const label = labels[i];
1167
+ if (label === INVALID_INDEX) {
1168
+ throw new GraphFormatError("E_PARTITION", `labels[${i}] is INVALID_INDEX`, { index: i });
1169
+ }
1170
+ if (label > maxLabel) {
1171
+ maxLabel = label;
1172
+ }
1173
+ }
1174
+ let count = 0;
1175
+ if (maxLabel < 4 * n + 1024) {
1176
+ const lookup = new Uint32Array(maxLabel + 1).fill(INVALID_INDEX);
1177
+ for (let i = 0; i < n; i++) {
1178
+ const label = labels[i];
1179
+ let dense = lookup[label];
1180
+ if (dense === INVALID_INDEX) {
1181
+ dense = count++;
1182
+ lookup[label] = dense;
1183
+ }
1184
+ result[i] = dense;
1185
+ }
1186
+ } else {
1187
+ const lookup = new Map<number, number>();
1188
+ for (let i = 0; i < n; i++) {
1189
+ const label = labels[i];
1190
+ let dense = lookup.get(label);
1191
+ if (dense === undefined) {
1192
+ dense = count++;
1193
+ lookup.set(label, dense);
1194
+ }
1195
+ result[i] = dense;
1196
+ }
1197
+ }
1198
+ return { labels: result, count };
1199
+ }
1200
+
1201
+ /**
1202
+ * The dense block labels of a contract partition (design section 7.3): labels already forming the
1203
+ * set 0..k-1 are kept, any other labelling is renumbered in first-seen order. E_PARTITION for a wrong
1204
+ * length or an INVALID_INDEX label.
1205
+ * @param partition - the caller's labels
1206
+ * @param nodeCount - the node count
1207
+ * @returns the dense labels (a fresh array) and the block count
1208
+ */
1209
+ function denseBlocks(partition: U32, nodeCount: number): { readonly labels: U32; readonly count: number } {
1210
+ if (partition.length !== nodeCount) {
1211
+ throw new GraphFormatError(
1212
+ "E_PARTITION",
1213
+ `partition has ${partition.length} entries, expected nodeCount ${nodeCount}`,
1214
+ { expected: nodeCount, found: partition.length },
1215
+ );
1216
+ }
1217
+ let maxLabel = -1;
1218
+ for (let i = 0; i < nodeCount; i++) {
1219
+ const label = partition[i];
1220
+ if (label === INVALID_INDEX) {
1221
+ throw new GraphFormatError("E_PARTITION", `partition[${i}] is INVALID_INDEX`, { index: i });
1222
+ }
1223
+ if (label > maxLabel) {
1224
+ maxLabel = label;
1225
+ }
1226
+ }
1227
+ if (maxLabel < nodeCount) {
1228
+ const seen = new Uint8Array(maxLabel + 1);
1229
+ let distinct = 0;
1230
+ for (let i = 0; i < nodeCount; i++) {
1231
+ if (seen[partition[i]] === 0) {
1232
+ seen[partition[i]] = 1;
1233
+ distinct++;
1234
+ }
1235
+ }
1236
+ if (distinct === maxLabel + 1) {
1237
+ return { labels: partition.slice(), count: maxLabel + 1 };
1238
+ }
1239
+ }
1240
+ return renumberPartition(partition);
1241
+ }
1242
+
1243
+ /**
1244
+ * Reduce the node columns of a contracted graph per `nodeReducers` over the blocks; columns not named
1245
+ * are dropped (design section 7.3).
1246
+ * @param source - the source snapshot
1247
+ * @param blocks - the block labels (source node -> block)
1248
+ * @param blockCount - the number of blocks
1249
+ * @param blockOrigin - block -> lowest source node
1250
+ * @param reducers - the named reducers
1251
+ * @returns the new node table
1252
+ */
1253
+ function contractNodeTable(
1254
+ source: GraphSnapshot,
1255
+ blocks: U32,
1256
+ blockCount: number,
1257
+ blockOrigin: U32,
1258
+ reducers: Readonly<Record<string, ColumnReducer>> | undefined,
1259
+ ): AttributeTable {
1260
+ const columns: Column[] = [];
1261
+ if (reducers !== undefined) {
1262
+ const grouping: EdgeGrouping = { count: blockCount, remap: blocks, origin: blockOrigin, dropped: 0, merged: 0 };
1263
+ for (const column of source.nodes) {
1264
+ const reducer = reducers[column.meta.name];
1265
+ if (reducer === undefined) {
1266
+ continue;
1267
+ }
1268
+ const reduced = reduceColumn(column, grouping, reducer);
1269
+ if (reduced !== null) {
1270
+ columns.push(reduced);
1271
+ }
1272
+ }
1273
+ }
1274
+ return new AttributeTableClass({ domain: "node", rowCount: blockCount, columns });
1275
+ }
1276
+
1277
+ /**
1278
+ * Group the kept contracted edges by block pair with two stable counting-sort passes (by second key,
1279
+ * then by first key), so each run of equal pairs is one group and the first member of a run is the
1280
+ * lowest source edge index.
1281
+ * @param first - the first key of every source edge (INVALID_INDEX = dropped)
1282
+ * @param second - the second key of every source edge
1283
+ * @param blockCount - the key range
1284
+ * @returns source edge -> survivor (INVALID_INDEX when dropped)
1285
+ */
1286
+ function groupByBlockPair(first: U32, second: U32, blockCount: number): U32 {
1287
+ const edgeCount = first.length;
1288
+ const kept: number[] = [];
1289
+ for (let e = 0; e < edgeCount; e++) {
1290
+ if (first[e] !== INVALID_INDEX) {
1291
+ kept.push(e);
1292
+ }
1293
+ }
1294
+ const start = new Uint32Array(blockCount + 1);
1295
+ for (const e of kept) {
1296
+ start[second[e] + 1]++;
1297
+ }
1298
+ for (let b = 0; b < blockCount; b++) {
1299
+ start[b + 1] += start[b];
1300
+ }
1301
+ const bySecond = new Uint32Array(kept.length);
1302
+ for (const e of kept) {
1303
+ bySecond[start[second[e]]++] = e;
1304
+ }
1305
+ start.fill(0);
1306
+ for (const e of kept) {
1307
+ start[first[e] + 1]++;
1308
+ }
1309
+ for (let b = 0; b < blockCount; b++) {
1310
+ start[b + 1] += start[b];
1311
+ }
1312
+ const sorted = new Uint32Array(kept.length);
1313
+ for (let k = 0; k < bySecond.length; k++) {
1314
+ const e = bySecond[k];
1315
+ sorted[start[first[e]]++] = e;
1316
+ }
1317
+ const survivorOf = new Uint32Array(edgeCount).fill(INVALID_INDEX);
1318
+ let runStart = 0;
1319
+ while (runStart < sorted.length) {
1320
+ const leader = sorted[runStart];
1321
+ let runEnd = runStart + 1;
1322
+ while (
1323
+ runEnd < sorted.length &&
1324
+ first[sorted[runEnd]] === first[leader] &&
1325
+ second[sorted[runEnd]] === second[leader]
1326
+ ) {
1327
+ runEnd++;
1328
+ }
1329
+ for (let k = runStart; k < runEnd; k++) {
1330
+ survivorOf[sorted[k]] = leader;
1331
+ }
1332
+ runStart = runEnd;
1333
+ }
1334
+ return survivorOf;
1335
+ }
1336
+
1337
+ /**
1338
+ * `contract(partition, options)` (design section 7.3): the nodes of each block become one node (block
1339
+ * indices as labels, ids 0..k-1), every source edge contributes once: an inter-block edge becomes an
1340
+ * edge block(u) -> block(v), an intra-block edge one self-loop with weight w, kept or dropped per
1341
+ * `selfLoops`; parallels merge per `parallel` with the `weights` reducer (default "sum"; on an
1342
+ * unweighted source "sum" materialises multiplicities). Node and edge columns are kept only through
1343
+ * `nodeReducers` / `edgeReducers`.
1344
+ * @param source - the source snapshot
1345
+ * @param list - the source edge list
1346
+ * @param partition - one label per node; E_PARTITION for a wrong length or an INVALID_INDEX label
1347
+ * @param options - the contraction options
1348
+ * @returns the derived parts with blockSizes
1349
+ */
1350
+ export function deriveContract(
1351
+ source: GraphSnapshot,
1352
+ list: EdgeListView,
1353
+ partition: U32,
1354
+ options: ContractOptions = {},
1355
+ ): DerivedParts {
1356
+ const reducer = assertOneOf("weights", options.weights, WEIGHT_REDUCERS) ?? "sum";
1357
+ const dropLoops = assertOneOf("selfLoops", options.selfLoops, ["keep", "drop"] as const) === "drop";
1358
+ const merge = (assertOneOf("parallel", options.parallel, ["merge", "keep"] as const) ?? "merge") === "merge";
1359
+ checkReducers("nodeReducers", options.nodeReducers);
1360
+ checkReducers("edgeReducers", options.edgeReducers);
1361
+ const { nodeCount, edgeCount } = source;
1362
+ const { labels: blocks, count: blockCount } = denseBlocks(partition, nodeCount);
1363
+ const blockSizes = new Uint32Array(blockCount);
1364
+ const blockOrigin = new Uint32Array(blockCount).fill(INVALID_INDEX);
1365
+ for (let u = 0; u < nodeCount; u++) {
1366
+ const b = blocks[u];
1367
+ blockSizes[b]++;
1368
+ if (blockOrigin[b] === INVALID_INDEX) {
1369
+ blockOrigin[b] = u;
1370
+ }
1371
+ }
1372
+ const first = new Uint32Array(edgeCount);
1373
+ const second = new Uint32Array(edgeCount);
1374
+ for (let e = 0; e < edgeCount; e++) {
1375
+ const bu = blocks[list.src[e]];
1376
+ const bv = blocks[list.dst[e]];
1377
+ if (bu === bv && dropLoops) {
1378
+ first[e] = INVALID_INDEX;
1379
+ second[e] = INVALID_INDEX;
1380
+ } else if (source.directed || bu <= bv) {
1381
+ first[e] = bu;
1382
+ second[e] = bv;
1383
+ } else {
1384
+ first[e] = bv;
1385
+ second[e] = bu;
1386
+ }
1387
+ }
1388
+ let survivorOf: U32;
1389
+ if (merge) {
1390
+ survivorOf = groupByBlockPair(first, second, blockCount);
1391
+ } else {
1392
+ survivorOf = new Uint32Array(edgeCount);
1393
+ for (let e = 0; e < edgeCount; e++) {
1394
+ survivorOf[e] = first[e] === INVALID_INDEX ? INVALID_INDEX : e;
1395
+ }
1396
+ }
1397
+ const grouping = groupingFromSurvivors(survivorOf);
1398
+ const { src, dst } = gatherEndpoints(list, grouping, blocks);
1399
+ const weights = reduceWeights(list.weights, grouping, reducer);
1400
+ const core = buildCore({
1401
+ directed: source.directed,
1402
+ nodeCount: blockCount,
1403
+ edgeCount: grouping.count,
1404
+ src,
1405
+ dst,
1406
+ weights,
1407
+ });
1408
+ const nodes = contractNodeTable(source, blocks, blockCount, blockOrigin, options.nodeReducers);
1409
+ const edgeColumns: Column[] = [];
1410
+ if (options.edgeReducers !== undefined) {
1411
+ for (const column of source.edges) {
1412
+ const columnReducer = options.edgeReducers[column.meta.name];
1413
+ if (columnReducer === undefined) {
1414
+ continue;
1415
+ }
1416
+ const reduced = reduceColumn(column, grouping, columnReducer);
1417
+ if (reduced !== null) {
1418
+ edgeColumns.push(reduced);
1419
+ }
1420
+ }
1421
+ }
1422
+ const edges = new AttributeTableClass({ domain: "edge", rowCount: grouping.count, columns: edgeColumns });
1423
+ const parts = assembleParts(source, core, {
1424
+ directed: source.directed,
1425
+ nodeCount: blockCount,
1426
+ edgeCount: grouping.count,
1427
+ ids: identityNodeIdMap(blockCount),
1428
+ nodes,
1429
+ edges,
1430
+ extensions: remapExtensions(source, blocks, grouping.remap),
1431
+ });
1432
+ return {
1433
+ parts,
1434
+ nodeOrigin: blockOrigin,
1435
+ edgeOrigin: grouping.origin,
1436
+ nodeRemap: blocks,
1437
+ edgeRemap: grouping.remap,
1438
+ blockSizes,
1439
+ report: { droppedEdges: grouping.dropped, mergedEdges: grouping.merged },
1440
+ };
1441
+ }
1442
+
1443
+ // ============================================================ withColumns
1444
+
1445
+ /**
1446
+ * `withColumns(nodes, edges)` (design section 7.3): the parts of a snapshot sharing the core, the id
1447
+ * map and the SERIAL of the source with a CLONED column set plus the given columns (the graph table is
1448
+ * cloned too; extension tables are shared).
1449
+ * @param source - the source snapshot
1450
+ * @param nodes - node columns to add, keyed by name
1451
+ * @param edges - edge columns to add, keyed by name
1452
+ * @param arcToEdge - the source's permutation arrays as the constructor holds them (null when identity)
1453
+ * @param edgeToArc - see arcToEdge
1454
+ * @returns the parts
1455
+ */
1456
+ export function withColumnsParts(
1457
+ source: GraphSnapshot,
1458
+ nodes: Readonly<Record<string, TypedArrayData | ColumnInput>> | undefined,
1459
+ edges: Readonly<Record<string, TypedArrayData | ColumnInput>> | undefined,
1460
+ arcToEdge: U32 | null,
1461
+ edgeToArc: U32 | null,
1462
+ ): SnapshotParts {
1463
+ return {
1464
+ label: source.label,
1465
+ serial: source.serial,
1466
+ directed: source.directed,
1467
+ nodeCount: source.nodeCount,
1468
+ edgeCount: source.edgeCount,
1469
+ arcCount: source.arcCount,
1470
+ selfLoopCount: source.selfLoopCount,
1471
+ rowPtr: source.rowPtr,
1472
+ colIdx: source.colIdx,
1473
+ weights: source.weights,
1474
+ arcToEdge,
1475
+ edgeToArc,
1476
+ flags: source.flags,
1477
+ ids: source.ids,
1478
+ nodes: tableWithColumns(source.nodes, nodes ?? {}),
1479
+ edges: tableWithColumns(source.edges, edges ?? {}),
1480
+ graph: source.graph.clone(),
1481
+ extensions: source.extensions,
1482
+ meta: source.meta,
1483
+ arena: source.arena,
1484
+ checksum: false,
1485
+ };
1486
+ }