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