@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.
- package/LICENSE +21 -0
- package/README.md +216 -43
- package/dist/graph-format.d.ts +1 -0
- package/dist/graph-format.d.ts.map +1 -0
- package/dist/graph-format.js +15798 -0
- package/dist/graph-format.js.map +1 -0
- package/dist/src/builder/arena.d.ts +85 -0
- package/dist/src/builder/arena.d.ts.map +1 -0
- package/dist/src/builder/arena.js +154 -0
- package/dist/src/builder/arena.js.map +1 -0
- package/dist/src/builder/compact.d.ts +515 -0
- package/dist/src/builder/compact.d.ts.map +1 -0
- package/dist/src/builder/compact.js +1553 -0
- package/dist/src/builder/compact.js.map +1 -0
- package/dist/src/builder/counting-sort.d.ts +96 -0
- package/dist/src/builder/counting-sort.d.ts.map +1 -0
- package/dist/src/builder/counting-sort.js +430 -0
- package/dist/src/builder/counting-sort.js.map +1 -0
- package/dist/src/builder/freeze.d.ts +72 -0
- package/dist/src/builder/freeze.d.ts.map +1 -0
- package/dist/src/builder/freeze.js +548 -0
- package/dist/src/builder/freeze.js.map +1 -0
- package/dist/src/builder/graph-builder.d.ts +602 -0
- package/dist/src/builder/graph-builder.d.ts.map +1 -0
- package/dist/src/builder/graph-builder.js +1940 -0
- package/dist/src/builder/graph-builder.js.map +1 -0
- package/dist/src/builder/options.d.ts +16 -0
- package/dist/src/builder/options.d.ts.map +1 -0
- package/dist/src/builder/options.js +21 -0
- package/dist/src/builder/options.js.map +1 -0
- package/dist/src/columns/bitmap.d.ts +96 -0
- package/dist/src/columns/bitmap.d.ts.map +1 -0
- package/dist/src/columns/bitmap.js +184 -0
- package/dist/src/columns/bitmap.js.map +1 -0
- package/dist/src/columns/column.d.ts +220 -0
- package/dist/src/columns/column.d.ts.map +1 -0
- package/dist/src/columns/column.js +2109 -0
- package/dist/src/columns/column.js.map +1 -0
- package/dist/src/columns/dictionary.d.ts +60 -0
- package/dist/src/columns/dictionary.d.ts.map +1 -0
- package/dist/src/columns/dictionary.js +102 -0
- package/dist/src/columns/dictionary.js.map +1 -0
- package/dist/src/columns/growable.d.ts +198 -0
- package/dist/src/columns/growable.d.ts.map +1 -0
- package/dist/src/columns/growable.js +296 -0
- package/dist/src/columns/growable.js.map +1 -0
- package/dist/src/columns/infer.d.ts +121 -0
- package/dist/src/columns/infer.d.ts.map +1 -0
- package/dist/src/columns/infer.js +268 -0
- package/dist/src/columns/infer.js.map +1 -0
- package/dist/src/columns/remap.d.ts +143 -0
- package/dist/src/columns/remap.d.ts.map +1 -0
- package/dist/src/columns/remap.js +588 -0
- package/dist/src/columns/remap.js.map +1 -0
- package/dist/src/columns/table.d.ts +184 -0
- package/dist/src/columns/table.d.ts.map +1 -0
- package/dist/src/columns/table.js +485 -0
- package/dist/src/columns/table.js.map +1 -0
- package/dist/src/constants.d.ts +34 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +79 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/errors.d.ts +76 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +32 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/ids/edge-id-index.d.ts +77 -0
- package/dist/src/ids/edge-id-index.d.ts.map +1 -0
- package/dist/src/ids/edge-id-index.js +155 -0
- package/dist/src/ids/edge-id-index.js.map +1 -0
- package/dist/src/ids/node-id-map.d.ts +280 -0
- package/dist/src/ids/node-id-map.d.ts.map +1 -0
- package/dist/src/ids/node-id-map.js +1139 -0
- package/dist/src/ids/node-id-map.js.map +1 -0
- package/dist/src/ids/string-store.d.ts +219 -0
- package/dist/src/ids/string-store.d.ts.map +1 -0
- package/dist/src/ids/string-store.js +526 -0
- package/dist/src/ids/string-store.js.map +1 -0
- package/dist/src/index.d.ts +30 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +29 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/populate/from-csr.d.ts +61 -0
- package/dist/src/populate/from-csr.d.ts.map +1 -0
- package/dist/src/populate/from-csr.js +706 -0
- package/dist/src/populate/from-csr.js.map +1 -0
- package/dist/src/populate/from-edge-arrays.d.ts +51 -0
- package/dist/src/populate/from-edge-arrays.d.ts.map +1 -0
- package/dist/src/populate/from-edge-arrays.js +217 -0
- package/dist/src/populate/from-edge-arrays.js.map +1 -0
- package/dist/src/populate/from-records.d.ts +51 -0
- package/dist/src/populate/from-records.d.ts.map +1 -0
- package/dist/src/populate/from-records.js +447 -0
- package/dist/src/populate/from-records.js.map +1 -0
- package/dist/src/snapshot/derived.d.ts +211 -0
- package/dist/src/snapshot/derived.d.ts.map +1 -0
- package/dist/src/snapshot/derived.js +1254 -0
- package/dist/src/snapshot/derived.js.map +1 -0
- package/dist/src/snapshot/graph-meta.d.ts +23 -0
- package/dist/src/snapshot/graph-meta.d.ts.map +1 -0
- package/dist/src/snapshot/graph-meta.js +159 -0
- package/dist/src/snapshot/graph-meta.js.map +1 -0
- package/dist/src/snapshot/graph-snapshot.d.ts +509 -0
- package/dist/src/snapshot/graph-snapshot.d.ts.map +1 -0
- package/dist/src/snapshot/graph-snapshot.js +1247 -0
- package/dist/src/snapshot/graph-snapshot.js.map +1 -0
- package/dist/src/snapshot/hash.d.ts +90 -0
- package/dist/src/snapshot/hash.d.ts.map +1 -0
- package/dist/src/snapshot/hash.js +228 -0
- package/dist/src/snapshot/hash.js.map +1 -0
- package/dist/src/snapshot/queries.d.ts +74 -0
- package/dist/src/snapshot/queries.d.ts.map +1 -0
- package/dist/src/snapshot/queries.js +131 -0
- package/dist/src/snapshot/queries.js.map +1 -0
- package/dist/src/snapshot/validate.d.ts +192 -0
- package/dist/src/snapshot/validate.d.ts.map +1 -0
- package/dist/src/snapshot/validate.js +990 -0
- package/dist/src/snapshot/validate.js.map +1 -0
- package/dist/src/snapshot/views.d.ts +274 -0
- package/dist/src/snapshot/views.d.ts.map +1 -0
- package/dist/src/snapshot/views.js +856 -0
- package/dist/src/snapshot/views.js.map +1 -0
- package/dist/src/types/builder.d.ts +456 -0
- package/dist/src/types/builder.d.ts.map +1 -0
- package/dist/src/types/builder.js +11 -0
- package/dist/src/types/builder.js.map +1 -0
- package/dist/src/types/columns.d.ts +615 -0
- package/dist/src/types/columns.d.ts.map +1 -0
- package/dist/src/types/columns.js +14 -0
- package/dist/src/types/columns.js.map +1 -0
- package/dist/src/types/index.d.ts +14 -0
- package/dist/src/types/index.d.ts.map +1 -0
- package/dist/src/types/index.js +11 -0
- package/dist/src/types/index.js.map +1 -0
- package/dist/src/types/internal.d.ts +172 -0
- package/dist/src/types/internal.d.ts.map +1 -0
- package/dist/src/types/internal.js +13 -0
- package/dist/src/types/internal.js.map +1 -0
- package/dist/src/types/snapshot.d.ts +801 -0
- package/dist/src/types/snapshot.d.ts.map +1 -0
- package/dist/src/types/snapshot.js +14 -0
- package/dist/src/types/snapshot.js.map +1 -0
- package/dist/src/types/wire.d.ts +196 -0
- package/dist/src/types/wire.d.ts.map +1 -0
- package/dist/src/types/wire.js +8 -0
- package/dist/src/types/wire.js.map +1 -0
- package/dist/src/util/mask.d.ts +55 -0
- package/dist/src/util/mask.d.ts.map +1 -0
- package/dist/src/util/mask.js +71 -0
- package/dist/src/util/mask.js.map +1 -0
- package/dist/src/util/options.d.ts +22 -0
- package/dist/src/util/options.d.ts.map +1 -0
- package/dist/src/util/options.js +49 -0
- package/dist/src/util/options.js.map +1 -0
- package/dist/src/util/shared-buffers.d.ts +42 -0
- package/dist/src/util/shared-buffers.d.ts.map +1 -0
- package/dist/src/util/shared-buffers.js +52 -0
- package/dist/src/util/shared-buffers.js.map +1 -0
- package/dist/src/util/typed-array.d.ts +205 -0
- package/dist/src/util/typed-array.d.ts.map +1 -0
- package/dist/src/util/typed-array.js +289 -0
- package/dist/src/util/typed-array.js.map +1 -0
- package/dist/src/wire/bytes.d.ts +69 -0
- package/dist/src/wire/bytes.d.ts.map +1 -0
- package/dist/src/wire/bytes.js +361 -0
- package/dist/src/wire/bytes.js.map +1 -0
- package/dist/src/wire/carried-views.d.ts +111 -0
- package/dist/src/wire/carried-views.d.ts.map +1 -0
- package/dist/src/wire/carried-views.js +286 -0
- package/dist/src/wire/carried-views.js.map +1 -0
- package/dist/src/wire/from-wire.d.ts +144 -0
- package/dist/src/wire/from-wire.d.ts.map +1 -0
- package/dist/src/wire/from-wire.js +1425 -0
- package/dist/src/wire/from-wire.js.map +1 -0
- package/dist/src/wire/to-wire.d.ts +171 -0
- package/dist/src/wire/to-wire.d.ts.map +1 -0
- package/dist/src/wire/to-wire.js +652 -0
- package/dist/src/wire/to-wire.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +72 -7
- package/src/builder/arena.ts +192 -0
- package/src/builder/compact.ts +1811 -0
- package/src/builder/counting-sort.ts +506 -0
- package/src/builder/freeze.ts +689 -0
- package/src/builder/graph-builder.ts +2264 -0
- package/src/builder/options.ts +29 -0
- package/src/columns/bitmap.ts +196 -0
- package/src/columns/column.ts +2574 -0
- package/src/columns/dictionary.ts +114 -0
- package/src/columns/growable.ts +350 -0
- package/src/columns/infer.ts +285 -0
- package/src/columns/remap.ts +663 -0
- package/src/columns/table.ts +568 -0
- package/src/constants.ts +89 -0
- package/src/errors.ts +115 -0
- package/src/ids/edge-id-index.ts +179 -0
- package/src/ids/node-id-map.ts +1340 -0
- package/src/ids/string-store.ts +589 -0
- package/src/index.ts +139 -0
- package/src/lib-resizable-array-buffer.d.ts +34 -0
- package/src/populate/from-csr.ts +802 -0
- package/src/populate/from-edge-arrays.ts +257 -0
- package/src/populate/from-records.ts +536 -0
- package/src/snapshot/derived.ts +1486 -0
- package/src/snapshot/graph-meta.ts +167 -0
- package/src/snapshot/graph-snapshot.ts +1503 -0
- package/src/snapshot/hash.ts +241 -0
- package/src/snapshot/queries.ts +136 -0
- package/src/snapshot/validate.ts +1108 -0
- package/src/snapshot/views.ts +943 -0
- package/src/types/builder.ts +480 -0
- package/src/types/columns.ts +727 -0
- package/src/types/index.ts +14 -0
- package/src/types/internal.ts +195 -0
- package/src/types/snapshot.ts +886 -0
- package/src/types/wire.ts +207 -0
- package/src/util/mask.ts +89 -0
- package/src/util/options.ts +57 -0
- package/src/util/shared-buffers.ts +56 -0
- package/src/util/typed-array.ts +371 -0
- package/src/wire/bytes.ts +440 -0
- package/src/wire/carried-views.ts +327 -0
- package/src/wire/from-wire.ts +1690 -0
- package/src/wire/to-wire.ts +755 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fromCsr()` (design section 8.1, entry point 2; decision C12): adopt prebuilt CSR arrays -- from
|
|
3
|
+
* a file, a worker, another package or a generator that emits CSR directly -- without copying by
|
|
4
|
+
* default (GraphBLAS pack semantics: the caller transfers ownership and must not mutate them
|
|
5
|
+
* afterwards), with full validation by default because adopted arrays typically come from a file or
|
|
6
|
+
* the network (design section 9.5).
|
|
7
|
+
*
|
|
8
|
+
* The pipeline: resolve the optional arrays (an absent `arcToEdge` is the identity of a directed
|
|
9
|
+
* graph, an absent `edgeToArc` is derived in one O(m) pass, `edgeCount` defaults to the arc count or
|
|
10
|
+
* is derived from `arcToEdge`); adopt or copy; detect an already-aligned shared buffer as the arena
|
|
11
|
+
* (design section 10.3); build a candidate snapshot; check the rows (invariant I4) when `sortRows`
|
|
12
|
+
* is set and, when some row is unsorted, rebuild a fresh sorted core through the freeze pipeline's
|
|
13
|
+
* counting sorts over the declared edge list (the caller's arrays are not modified; the cost is a
|
|
14
|
+
* freeze); validate at the requested level; verify the caller's flag claims (invariant I9).
|
|
15
|
+
*
|
|
16
|
+
* `detectArena()` is exported for `fromWire`, which applies the same detection when a manifest
|
|
17
|
+
* carries no arena descriptor (design section 8.1).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { copyCoreIntoArena, CORE_ORDER, type CoreArrays } from "../builder/arena.js";
|
|
21
|
+
import { sortIntoCore } from "../builder/counting-sort.js";
|
|
22
|
+
import { createTable, tableWithColumns } from "../columns/table.js";
|
|
23
|
+
import { ALIGNMENT, INVALID_INDEX, MAX_COUNT } from "../constants.js";
|
|
24
|
+
import { GraphFormatError } from "../errors.js";
|
|
25
|
+
import { identityNodeIdMap, nodeIdMapFromF64, nodeIdMapFromIds } from "../ids/node-id-map.js";
|
|
26
|
+
import { EMPTY_GRAPH_META, resolveGraphMeta as resolveSharedGraphMeta } from "../snapshot/graph-meta.js";
|
|
27
|
+
import { createSnapshot, type GraphSnapshot } from "../snapshot/graph-snapshot.js";
|
|
28
|
+
import {
|
|
29
|
+
checkI5Orientation,
|
|
30
|
+
checkI6,
|
|
31
|
+
checkI8NaN,
|
|
32
|
+
checkI11Bijection,
|
|
33
|
+
checkUniqueColumn,
|
|
34
|
+
computeFlags,
|
|
35
|
+
countLoopArcs,
|
|
36
|
+
invariantViolation,
|
|
37
|
+
isIdentity,
|
|
38
|
+
validateFull,
|
|
39
|
+
validateStructure,
|
|
40
|
+
} from "../snapshot/validate.js";
|
|
41
|
+
import {
|
|
42
|
+
type ArenaLayout,
|
|
43
|
+
type ArenaSegment,
|
|
44
|
+
type AttributeTable,
|
|
45
|
+
type ColumnInput,
|
|
46
|
+
type CoreArrayName,
|
|
47
|
+
type CsrInput,
|
|
48
|
+
type F32,
|
|
49
|
+
type F64,
|
|
50
|
+
type FlagClaims,
|
|
51
|
+
type FromCsrOptions,
|
|
52
|
+
type GraphMeta,
|
|
53
|
+
type GraphMetaPatch,
|
|
54
|
+
type NodeIdMap,
|
|
55
|
+
type SnapshotFlags,
|
|
56
|
+
type TypedArrayData,
|
|
57
|
+
type U32,
|
|
58
|
+
type ValidationLevel,
|
|
59
|
+
} from "../types/index.js";
|
|
60
|
+
import { type SnapshotParts } from "../types/internal.js";
|
|
61
|
+
import { isOverPlainBuffer } from "../util/typed-array.js";
|
|
62
|
+
|
|
63
|
+
// ============================================================ constants
|
|
64
|
+
|
|
65
|
+
/** The flag names, for claim verification. */
|
|
66
|
+
const FLAG_NAMES: readonly (keyof SnapshotFlags)[] = [
|
|
67
|
+
"multigraph",
|
|
68
|
+
"hasSelfLoops",
|
|
69
|
+
"arcToEdgeIsIdentity",
|
|
70
|
+
"weighted",
|
|
71
|
+
"allWeightsOne",
|
|
72
|
+
"nonNegativeWeights",
|
|
73
|
+
"finiteWeights",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const VALIDATION_LEVELS: ReadonlySet<string> = new Set(["none", "structure", "full"]);
|
|
77
|
+
// ============================================================ input shape
|
|
78
|
+
|
|
79
|
+
/** The core arrays after the optional ones are resolved; `arcToEdge` / `edgeToArc` are null when identity. */
|
|
80
|
+
interface ResolvedCore {
|
|
81
|
+
readonly rowPtr: U32;
|
|
82
|
+
readonly colIdx: U32;
|
|
83
|
+
readonly weights: F32 | null;
|
|
84
|
+
readonly arcToEdge: U32 | null;
|
|
85
|
+
readonly edgeToArc: U32 | null;
|
|
86
|
+
readonly edgeCount: number;
|
|
87
|
+
readonly arcCount: number;
|
|
88
|
+
readonly selfLoopCount: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The E_INVALID_SNAPSHOT error for an input array of the wrong class.
|
|
93
|
+
* @param name - the array
|
|
94
|
+
* @param expected - the class name expected
|
|
95
|
+
* @returns the error
|
|
96
|
+
*/
|
|
97
|
+
function arrayTypeError(name: string, expected: string): GraphFormatError {
|
|
98
|
+
return new GraphFormatError("E_INVALID_SNAPSHOT", `${name} must be a ${expected}`, {
|
|
99
|
+
array: name,
|
|
100
|
+
expected,
|
|
101
|
+
reason: "dtype",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check the classes of the input arrays (a Float64Array passed as weights or a plain array passed as
|
|
107
|
+
* colIdx would otherwise produce a snapshot whose bytes are wrong on the GPU) and the node count.
|
|
108
|
+
* @param input - the input
|
|
109
|
+
*/
|
|
110
|
+
function checkShape(input: CsrInput): void {
|
|
111
|
+
if (!Number.isInteger(input.nodeCount) || input.nodeCount < 0 || input.nodeCount > MAX_COUNT) {
|
|
112
|
+
throw new GraphFormatError("E_TOO_LARGE", `nodeCount ${input.nodeCount} is not an integer in [0, MAX_COUNT]`, {
|
|
113
|
+
count: input.nodeCount,
|
|
114
|
+
max: MAX_COUNT,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (!(input.rowPtr instanceof Uint32Array)) {
|
|
118
|
+
throw arrayTypeError("rowPtr", "Uint32Array");
|
|
119
|
+
}
|
|
120
|
+
if (!(input.colIdx instanceof Uint32Array)) {
|
|
121
|
+
throw arrayTypeError("colIdx", "Uint32Array");
|
|
122
|
+
}
|
|
123
|
+
if (input.weights !== undefined && input.weights !== null && !(input.weights instanceof Float32Array)) {
|
|
124
|
+
throw arrayTypeError("weights", "Float32Array");
|
|
125
|
+
}
|
|
126
|
+
if (input.arcToEdge !== undefined && !(input.arcToEdge instanceof Uint32Array)) {
|
|
127
|
+
throw arrayTypeError("arcToEdge", "Uint32Array");
|
|
128
|
+
}
|
|
129
|
+
if (input.edgeToArc !== undefined && !(input.edgeToArc instanceof Uint32Array)) {
|
|
130
|
+
throw arrayTypeError("edgeToArc", "Uint32Array");
|
|
131
|
+
}
|
|
132
|
+
if (input.edgeCount !== undefined && (!Number.isInteger(input.edgeCount) || input.edgeCount < 0)) {
|
|
133
|
+
throw invariantViolation("I3", `edgeCount ${input.edgeCount} is not a non-negative integer`, {
|
|
134
|
+
count: "edgeCount",
|
|
135
|
+
found: input.edgeCount,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (input.rowPtr.length !== input.nodeCount + 1) {
|
|
139
|
+
throw invariantViolation("I1", `rowPtr has ${input.rowPtr.length} entries, expected ${input.nodeCount + 1}`, {
|
|
140
|
+
expected: input.nodeCount + 1,
|
|
141
|
+
found: input.rowPtr.length,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Whether a typed array is backed by a plain, fixed-length ArrayBuffer (a SharedArrayBuffer is never
|
|
148
|
+
* adopted, decision D-SAB; a resizable buffer's view can change length, invariants I10 and I17).
|
|
149
|
+
* @param array - the array
|
|
150
|
+
* @returns true for a plain ArrayBuffer
|
|
151
|
+
*/
|
|
152
|
+
function overPlainBuffer(array: ArrayBufferView): boolean {
|
|
153
|
+
return isOverPlainBuffer(array);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Derive `edgeToArc` from `arcToEdge`: the first (lowest) arc holding each edge, which for a directed
|
|
158
|
+
* graph is the edge's only arc and for an undirected graph the arc in the lower-numbered row
|
|
159
|
+
* (design section 8.1: "derived in one O(m) pass"). An edge no arc holds is left INVALID_INDEX so the
|
|
160
|
+
* I5 range check names it.
|
|
161
|
+
* @param arcToEdge - the arc -> edge map
|
|
162
|
+
* @param edgeCount - the edge count
|
|
163
|
+
* @returns the edge -> arc map
|
|
164
|
+
*/
|
|
165
|
+
function deriveEdgeToArc(arcToEdge: U32, edgeCount: number): U32 {
|
|
166
|
+
const edgeToArc = new Uint32Array(edgeCount).fill(INVALID_INDEX);
|
|
167
|
+
for (let a = 0; a < arcToEdge.length; a++) {
|
|
168
|
+
const e = arcToEdge[a];
|
|
169
|
+
if (e < edgeCount && edgeToArc[e] === INVALID_INDEX) {
|
|
170
|
+
edgeToArc[e] = a;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return edgeToArc;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The E_INVALID_SNAPSHOT error for an identity-flag contradiction between the input arrays.
|
|
178
|
+
* @param message - what is wrong
|
|
179
|
+
* @param details - the location
|
|
180
|
+
* @returns the error
|
|
181
|
+
*/
|
|
182
|
+
function identityError(message: string, details: Readonly<Record<string, unknown>>): GraphFormatError {
|
|
183
|
+
return invariantViolation("I5", message, details);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the optional core arrays and counts (design section 8.1). For a directed input an absent
|
|
188
|
+
* `arcToEdge` is the identity and a supplied one that happens to be the identity is dropped (the
|
|
189
|
+
* snapshot materialises identity permutations lazily, design section 3.1); an undirected input must
|
|
190
|
+
* supply `arcToEdge` (E_INVALID_SNAPSHOT, invariant I5). `edgeCount` defaults to the arc count
|
|
191
|
+
* (directed) or to max(arcToEdge) + 1 (undirected). `selfLoopCount` is the number of loop arcs.
|
|
192
|
+
* @param input - the input
|
|
193
|
+
* @param level - the validation level ("none" skips the edgeToArc identity check)
|
|
194
|
+
* @returns the resolved core
|
|
195
|
+
*/
|
|
196
|
+
function resolveCore(input: CsrInput, level: ValidationLevel): ResolvedCore {
|
|
197
|
+
const { directed, nodeCount, rowPtr, colIdx } = input;
|
|
198
|
+
const arcCount = colIdx.length;
|
|
199
|
+
const weights = input.weights ?? null;
|
|
200
|
+
const selfLoopCount = countLoopArcs(rowPtr, colIdx, nodeCount);
|
|
201
|
+
let arcToEdge: U32 | null = input.arcToEdge ?? null;
|
|
202
|
+
let edgeToArc: U32 | null = input.edgeToArc ?? null;
|
|
203
|
+
let edgeCount: number;
|
|
204
|
+
if (directed) {
|
|
205
|
+
edgeCount = input.edgeCount ?? arcCount;
|
|
206
|
+
if (edgeCount !== arcCount) {
|
|
207
|
+
throw invariantViolation("I6", `directed input has arcCount ${arcCount} but edgeCount ${edgeCount}`, {
|
|
208
|
+
arcCount,
|
|
209
|
+
edgeCount,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// decided from the supplied array at every level (O(m)): a flag claim never drops an array
|
|
213
|
+
const identity = arcToEdge === null || (arcToEdge.length === arcCount && isIdentity(arcToEdge));
|
|
214
|
+
if (identity) {
|
|
215
|
+
if (edgeToArc !== null && level !== "none" && (edgeToArc.length !== edgeCount || !isIdentity(edgeToArc))) {
|
|
216
|
+
throw identityError("arcToEdge is the identity but edgeToArc is not", {
|
|
217
|
+
array: "edgeToArc",
|
|
218
|
+
found: edgeToArc.length,
|
|
219
|
+
expected: edgeCount,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
arcToEdge = null;
|
|
223
|
+
edgeToArc = null;
|
|
224
|
+
} else if (edgeToArc === null) {
|
|
225
|
+
edgeToArc = deriveEdgeToArc(arcToEdge as U32, edgeCount);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
if (arcToEdge === null) {
|
|
229
|
+
throw identityError("an undirected input must supply arcToEdge", {
|
|
230
|
+
array: "arcToEdge",
|
|
231
|
+
reason: "missing arcToEdge",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const { edgeCount: given } = input;
|
|
235
|
+
if (given === undefined) {
|
|
236
|
+
let max = -1;
|
|
237
|
+
for (let a = 0; a < arcToEdge.length; a++) {
|
|
238
|
+
if (arcToEdge[a] > max) {
|
|
239
|
+
max = arcToEdge[a];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
edgeCount = max + 1;
|
|
243
|
+
} else {
|
|
244
|
+
edgeCount = given;
|
|
245
|
+
}
|
|
246
|
+
edgeToArc ??= deriveEdgeToArc(arcToEdge, edgeCount);
|
|
247
|
+
}
|
|
248
|
+
return { rowPtr, colIdx, weights, arcToEdge, edgeToArc, edgeCount, arcCount, selfLoopCount };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ============================================================ adoption and the arena
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The core arrays of a resolved core in the shape the arena helpers take.
|
|
255
|
+
* @param core - the resolved core
|
|
256
|
+
* @param arena - the arena, or null
|
|
257
|
+
* @returns the core arrays
|
|
258
|
+
*/
|
|
259
|
+
function coreArraysOf(core: ResolvedCore, arena: ArenaLayout | null): CoreArrays {
|
|
260
|
+
return {
|
|
261
|
+
rowPtr: core.rowPtr,
|
|
262
|
+
colIdx: core.colIdx,
|
|
263
|
+
weights: core.weights,
|
|
264
|
+
arcToEdge: core.arcToEdge,
|
|
265
|
+
edgeToArc: core.edgeToArc,
|
|
266
|
+
arena,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Detect whether adopted core arrays already form an arena (design sections 8.1 and 10.3): every
|
|
272
|
+
* present, non-empty, non-identity core array is a view over ONE plain ArrayBuffer, laid out hot to
|
|
273
|
+
* cold (rowPtr, colIdx, weights, arcToEdge, edgeToArc) without overlap, each starting at a multiple
|
|
274
|
+
* of 256 bytes relative to `rowPtr`. When they do, that buffer is the arena: `byteOffset` is
|
|
275
|
+
* rowPtr's, `byteLength` spans to the end of the last array and `hotByteLength` ends at the weights
|
|
276
|
+
* (or colIdx, or rowPtr) segment. Otherwise null. Arrays whose segment would be null (a zero-length
|
|
277
|
+
* array, an absent weights array, an identity permutation given as null) do not take part.
|
|
278
|
+
* @param core - the core arrays; `arcToEdge` / `edgeToArc` null when identity
|
|
279
|
+
* @returns the arena layout, or null when the arrays are not an aligned shared buffer
|
|
280
|
+
*/
|
|
281
|
+
export function detectArena(core: Omit<CoreArrays, "arena">): ArenaLayout | null {
|
|
282
|
+
const { buffer } = core.rowPtr;
|
|
283
|
+
if (!(buffer instanceof ArrayBuffer)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const base = core.rowPtr.byteOffset;
|
|
287
|
+
const arrays: readonly (ArrayBufferView | null)[] = [
|
|
288
|
+
core.rowPtr,
|
|
289
|
+
core.colIdx,
|
|
290
|
+
core.weights,
|
|
291
|
+
core.arcToEdge,
|
|
292
|
+
core.edgeToArc,
|
|
293
|
+
];
|
|
294
|
+
const segments: Record<CoreArrayName, ArenaSegment | null> = {
|
|
295
|
+
rowPtr: null,
|
|
296
|
+
colIdx: null,
|
|
297
|
+
weights: null,
|
|
298
|
+
arcToEdge: null,
|
|
299
|
+
edgeToArc: null,
|
|
300
|
+
};
|
|
301
|
+
let cursor = base;
|
|
302
|
+
let hotEnd = base;
|
|
303
|
+
for (let i = 0; i < CORE_ORDER.length; i++) {
|
|
304
|
+
const array = arrays[i];
|
|
305
|
+
if (array === null || array.byteLength === 0) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (array.buffer !== buffer || array.byteOffset < cursor || (array.byteOffset - base) % ALIGNMENT !== 0) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
segments[CORE_ORDER[i]] = Object.freeze({ byteOffset: array.byteOffset, byteLength: array.byteLength });
|
|
312
|
+
cursor = array.byteOffset + array.byteLength;
|
|
313
|
+
if (i <= 2) {
|
|
314
|
+
hotEnd = cursor;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return Object.freeze({
|
|
318
|
+
buffer,
|
|
319
|
+
byteOffset: base,
|
|
320
|
+
byteLength: cursor - base,
|
|
321
|
+
alignment: ALIGNMENT,
|
|
322
|
+
segments: Object.freeze(segments),
|
|
323
|
+
hotByteLength: hotEnd - base,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Whether any present core array is not over a plain ArrayBuffer, which forces the copy path.
|
|
329
|
+
* @param core - the resolved core
|
|
330
|
+
* @returns true when a copy is required
|
|
331
|
+
*/
|
|
332
|
+
function needsCopy(core: ResolvedCore): boolean {
|
|
333
|
+
const arrays = [core.rowPtr, core.colIdx, core.weights, core.arcToEdge, core.edgeToArc];
|
|
334
|
+
return arrays.some((array) => array !== null && !overPlainBuffer(array));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============================================================ ids, columns, meta
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The id map of a CsrInput (design section 4.2): identity with no storage when no ids are given;
|
|
341
|
+
* otherwise the kind detected from the ids, validated (E_INVALID_ID / E_DUPLICATE_ID) unless the
|
|
342
|
+
* level is "none". A Float64Array is adopted by reference unless `copy` is set.
|
|
343
|
+
* @param input - the input
|
|
344
|
+
* @param level - the validation level
|
|
345
|
+
* @param copy - whether to copy an F64 array
|
|
346
|
+
* @returns the id map; E_COLUMN_LENGTH when ids.length !== nodeCount
|
|
347
|
+
*/
|
|
348
|
+
function idMapOf(input: CsrInput, level: ValidationLevel, copy: boolean): NodeIdMap {
|
|
349
|
+
const { ids, nodeCount } = input;
|
|
350
|
+
if (ids === undefined) {
|
|
351
|
+
return identityNodeIdMap(nodeCount);
|
|
352
|
+
}
|
|
353
|
+
if (ids.length !== nodeCount) {
|
|
354
|
+
throw new GraphFormatError(
|
|
355
|
+
"E_COLUMN_LENGTH",
|
|
356
|
+
`ids has ${ids.length} entries, expected nodeCount ${nodeCount}`,
|
|
357
|
+
{
|
|
358
|
+
field: "ids",
|
|
359
|
+
expected: nodeCount,
|
|
360
|
+
found: ids.length,
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const validate = level !== "none";
|
|
365
|
+
if (ids instanceof Float64Array) {
|
|
366
|
+
const values: F64 = copy || !overPlainBuffer(ids) ? new Float64Array(ids) : ids;
|
|
367
|
+
return nodeIdMapFromF64(values, { validate });
|
|
368
|
+
}
|
|
369
|
+
return nodeIdMapFromIds(copy ? [...ids] : ids, nodeCount, { validate });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* The node or edge table of a CsrInput: the caller's columns attached through the same path as
|
|
374
|
+
* `withColumns()` (typed arrays adopted by reference, JS arrays resolved with inference; design
|
|
375
|
+
* section 5.7), copied first when `copy` is set.
|
|
376
|
+
* @param domain - "node" or "edge"
|
|
377
|
+
* @param rowCount - the table's row count
|
|
378
|
+
* @param columns - the caller's columns, or undefined
|
|
379
|
+
* @param copy - whether to copy typed arrays
|
|
380
|
+
* @returns the table
|
|
381
|
+
*/
|
|
382
|
+
function tableOf(
|
|
383
|
+
domain: "node" | "edge",
|
|
384
|
+
rowCount: number,
|
|
385
|
+
columns: Readonly<Record<string, TypedArrayData | ColumnInput>> | undefined,
|
|
386
|
+
copy: boolean,
|
|
387
|
+
): AttributeTable {
|
|
388
|
+
const table = createTable(domain, rowCount);
|
|
389
|
+
if (columns === undefined) {
|
|
390
|
+
return table;
|
|
391
|
+
}
|
|
392
|
+
// a typed array over a SharedArrayBuffer or a resizable buffer is copied like the core (D-SAB)
|
|
393
|
+
const copied: Record<string, TypedArrayData | ColumnInput> = {};
|
|
394
|
+
for (const name of Object.keys(columns)) {
|
|
395
|
+
const value = columns[name];
|
|
396
|
+
if (ArrayBuffer.isView(value)) {
|
|
397
|
+
copied[name] = copy || !overPlainBuffer(value) ? value.slice() : value;
|
|
398
|
+
} else if (ArrayBuffer.isView(value.data)) {
|
|
399
|
+
copied[name] =
|
|
400
|
+
copy || !overPlainBuffer(value.data) ? { data: value.data.slice(), decl: value.decl } : value;
|
|
401
|
+
} else {
|
|
402
|
+
copied[name] = value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return tableWithColumns(table, copied);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Resolve a GraphMetaPatch into a complete GraphMeta (design section 5.9) with the builder's
|
|
410
|
+
* `setMeta` rules over empty metadata (E_COLUMN_TYPE with details.field on a bad field).
|
|
411
|
+
* @param patch - the patch, or undefined for no metadata
|
|
412
|
+
* @returns the metadata, frozen
|
|
413
|
+
*/
|
|
414
|
+
export function resolveGraphMeta(patch: GraphMetaPatch | undefined): GraphMeta {
|
|
415
|
+
return patch === undefined ? EMPTY_GRAPH_META : resolveSharedGraphMeta(EMPTY_GRAPH_META, patch);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================ flags
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Whether every flag is claimed, so no pass over the arrays is needed to know them.
|
|
422
|
+
* @param claims - the caller's claims
|
|
423
|
+
* @returns true when all seven flags are defined
|
|
424
|
+
*/
|
|
425
|
+
function fullyClaimed(claims: FlagClaims | undefined): claims is SnapshotFlags {
|
|
426
|
+
return claims !== undefined && FLAG_NAMES.every((name) => claims[name] !== undefined);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The truthful flags of a core (design section 3.8) computed from the arrays.
|
|
431
|
+
* @param directed - the direction
|
|
432
|
+
* @param nodeCount - the node count
|
|
433
|
+
* @param core - the core arrays
|
|
434
|
+
* @param selfLoopCount - the loop count
|
|
435
|
+
* @returns the flags
|
|
436
|
+
*/
|
|
437
|
+
function flagsOf(directed: boolean, nodeCount: number, core: CoreArrays, selfLoopCount: number): SnapshotFlags {
|
|
438
|
+
return computeFlags({
|
|
439
|
+
directed,
|
|
440
|
+
nodeCount,
|
|
441
|
+
rowPtr: core.rowPtr,
|
|
442
|
+
colIdx: core.colIdx,
|
|
443
|
+
weights: core.weights,
|
|
444
|
+
arcToEdge: core.arcToEdge,
|
|
445
|
+
selfLoopCount,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Verify the caller's flag claims against the computed flags (invariant I9): every claimed flag must
|
|
451
|
+
* equal the truth.
|
|
452
|
+
* @param claims - the claims, or undefined
|
|
453
|
+
* @param flags - the truthful flags
|
|
454
|
+
*/
|
|
455
|
+
function verifyClaims(claims: FlagClaims | undefined, flags: SnapshotFlags): void {
|
|
456
|
+
if (claims === undefined) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
for (const name of FLAG_NAMES) {
|
|
460
|
+
const claim = claims[name];
|
|
461
|
+
if (claim !== undefined && claim !== flags[name]) {
|
|
462
|
+
throw invariantViolation("I9", `flag ${name} is claimed ${claim} but the arrays say ${flags[name]}`, {
|
|
463
|
+
flag: name,
|
|
464
|
+
found: claim,
|
|
465
|
+
expected: flags[name],
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ============================================================ rows and the rebuild
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Whether every row satisfies invariant I4: colIdx non-decreasing within the row, and parallel arcs in
|
|
475
|
+
* ascending arcToEdge order.
|
|
476
|
+
* @param s - the candidate snapshot
|
|
477
|
+
* @returns true when sorted
|
|
478
|
+
*/
|
|
479
|
+
function rowsSorted(s: GraphSnapshot): boolean {
|
|
480
|
+
const { rowPtr, colIdx, nodeCount } = s;
|
|
481
|
+
const arcToEdge = s.flags.arcToEdgeIsIdentity ? null : s.arcToEdge;
|
|
482
|
+
for (let u = 0; u < nodeCount; u++) {
|
|
483
|
+
const end = rowPtr[u + 1];
|
|
484
|
+
for (let a = rowPtr[u] + 1; a < end; a++) {
|
|
485
|
+
if (colIdx[a] < colIdx[a - 1]) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (colIdx[a] === colIdx[a - 1] && arcToEdge !== null && arcToEdge[a] <= arcToEdge[a - 1]) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The declared edge list of a candidate whose rows are unsorted: for every logical edge the arc
|
|
498
|
+
* `edgeToArc[e]` gives the declared source (its row), target and weight.
|
|
499
|
+
* @param s - the candidate snapshot
|
|
500
|
+
* @returns the per-edge arrays
|
|
501
|
+
*/
|
|
502
|
+
function declaredEdges(s: GraphSnapshot): { readonly src: U32; readonly dst: U32; readonly weights: F32 | null } {
|
|
503
|
+
const { nodeCount, edgeCount, rowPtr, colIdx, weights } = s;
|
|
504
|
+
const identity = s.flags.arcToEdgeIsIdentity;
|
|
505
|
+
const src = new Uint32Array(edgeCount);
|
|
506
|
+
const dst = new Uint32Array(edgeCount);
|
|
507
|
+
const edgeWeights = weights === null ? null : new Float32Array(edgeCount);
|
|
508
|
+
const arcToEdge = identity ? null : s.arcToEdge;
|
|
509
|
+
const edgeToArc = identity ? null : s.edgeToArc;
|
|
510
|
+
for (let u = 0; u < nodeCount; u++) {
|
|
511
|
+
const end = rowPtr[u + 1];
|
|
512
|
+
for (let a = rowPtr[u]; a < end; a++) {
|
|
513
|
+
const e = arcToEdge === null ? a : arcToEdge[a];
|
|
514
|
+
if (edgeToArc !== null && edgeToArc[e] !== a) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
src[e] = u;
|
|
518
|
+
dst[e] = colIdx[a];
|
|
519
|
+
if (edgeWeights !== null && weights !== null) {
|
|
520
|
+
edgeWeights[e] = weights[a];
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { src, dst, weights: edgeWeights };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Compare the rebuilt sorted core with the unsorted input row by row as multisets of (target, edge,
|
|
529
|
+
* weight) (the "full" level's pairing and weight checks of invariant I7 for an input that could not
|
|
530
|
+
* be walked in sorted order): an arc without a mate, a mate on a different edge, or unequal mate
|
|
531
|
+
* weights all leave the rebuilt rows different from the input rows.
|
|
532
|
+
* @param input - the candidate over the caller's arrays
|
|
533
|
+
* @param rebuilt - the rebuilt snapshot
|
|
534
|
+
*/
|
|
535
|
+
function compareRows(input: GraphSnapshot, rebuilt: GraphSnapshot): void {
|
|
536
|
+
const { nodeCount } = input;
|
|
537
|
+
for (let u = 0; u <= nodeCount; u++) {
|
|
538
|
+
if (input.rowPtr[u] !== rebuilt.rowPtr[u]) {
|
|
539
|
+
throw invariantViolation("I7", `row ${u - 1} holds arcs whose mates are missing from their rows`, {
|
|
540
|
+
row: Math.max(0, u - 1),
|
|
541
|
+
found: input.rowPtr[u],
|
|
542
|
+
expected: rebuilt.rowPtr[u],
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const inArcToEdge = input.arcToEdge;
|
|
547
|
+
const outArcToEdge = rebuilt.arcToEdge;
|
|
548
|
+
const inWeights = input.weights;
|
|
549
|
+
const outWeights = rebuilt.weights;
|
|
550
|
+
for (let u = 0; u < nodeCount; u++) {
|
|
551
|
+
const start = input.rowPtr[u];
|
|
552
|
+
const end = input.rowPtr[u + 1];
|
|
553
|
+
const order = new Uint32Array(end - start);
|
|
554
|
+
for (let i = 0; i < order.length; i++) {
|
|
555
|
+
order[i] = start + i;
|
|
556
|
+
}
|
|
557
|
+
order.sort((x, y) => input.colIdx[x] - input.colIdx[y] || inArcToEdge[x] - inArcToEdge[y]);
|
|
558
|
+
for (let i = 0; i < order.length; i++) {
|
|
559
|
+
const a = order[i];
|
|
560
|
+
const b = start + i;
|
|
561
|
+
const sameArc = input.colIdx[a] === rebuilt.colIdx[b] && inArcToEdge[a] === outArcToEdge[b];
|
|
562
|
+
const sameWeight = inWeights === null || outWeights === null || inWeights[a] === outWeights[b];
|
|
563
|
+
if (!sameArc || !sameWeight) {
|
|
564
|
+
throw invariantViolation("I7", `arc ${a} in row ${u} has no matching mate`, {
|
|
565
|
+
row: u,
|
|
566
|
+
arc: a,
|
|
567
|
+
target: input.colIdx[a],
|
|
568
|
+
edge: inArcToEdge[a],
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Rebuild a sorted core from a candidate whose rows are unsorted through the freeze pipeline's
|
|
577
|
+
* counting sorts (design section 6.3 steps 3-6 and 8) over the declared edge list, into a fresh arena
|
|
578
|
+
* as `freeze()` would. Logical edge indices are preserved; the caller's arrays are not modified.
|
|
579
|
+
* @param candidate - the candidate over the caller's arrays
|
|
580
|
+
* @param parts - the candidate's parts (ids, tables, meta are reused)
|
|
581
|
+
* @returns the rebuilt parts, with the truthful flags of the sorted core
|
|
582
|
+
*/
|
|
583
|
+
function rebuildSorted(candidate: GraphSnapshot, parts: SnapshotParts): SnapshotParts {
|
|
584
|
+
const edges = declaredEdges(candidate);
|
|
585
|
+
const sorted = sortIntoCore(
|
|
586
|
+
{
|
|
587
|
+
directed: candidate.directed,
|
|
588
|
+
nodeCount: candidate.nodeCount,
|
|
589
|
+
edgeCount: candidate.edgeCount,
|
|
590
|
+
src: edges.src,
|
|
591
|
+
dst: edges.dst,
|
|
592
|
+
weights: edges.weights,
|
|
593
|
+
},
|
|
594
|
+
true,
|
|
595
|
+
);
|
|
596
|
+
return {
|
|
597
|
+
...parts,
|
|
598
|
+
arcCount: sorted.arcCount,
|
|
599
|
+
selfLoopCount: sorted.selfLoopCount,
|
|
600
|
+
rowPtr: sorted.core.rowPtr,
|
|
601
|
+
colIdx: sorted.core.colIdx,
|
|
602
|
+
weights: sorted.core.weights,
|
|
603
|
+
arcToEdge: sorted.core.arcToEdge,
|
|
604
|
+
edgeToArc: sorted.core.edgeToArc,
|
|
605
|
+
flags: sorted.flags,
|
|
606
|
+
arena: sorted.core.arena,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* The order-independent checks of the "full" level over a candidate that is about to be rebuilt:
|
|
612
|
+
* the permutation rules (I5 orientation, I6), NaN weights (I8), the id bijection (I11) and unique
|
|
613
|
+
* columns. Sortedness (I4), pairing (I7) and the flags (I9) are established by the rebuild itself.
|
|
614
|
+
* @param s - the candidate
|
|
615
|
+
*/
|
|
616
|
+
function orderIndependentFullChecks(s: GraphSnapshot): void {
|
|
617
|
+
checkI5Orientation(s);
|
|
618
|
+
checkI6(s, true);
|
|
619
|
+
checkI8NaN(s);
|
|
620
|
+
checkI11Bijection(s);
|
|
621
|
+
const tables: readonly [string, AttributeTable][] = [
|
|
622
|
+
["nodes", s.nodes],
|
|
623
|
+
["edges", s.edges],
|
|
624
|
+
["graph", s.graph],
|
|
625
|
+
];
|
|
626
|
+
for (const [name, table] of tables) {
|
|
627
|
+
for (const column of table) {
|
|
628
|
+
checkUniqueColumn(name, column);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// ============================================================ the entry point
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Resolve the options with their defaults, checking the validation level.
|
|
637
|
+
* @param options - the caller's options
|
|
638
|
+
* @returns the resolved values
|
|
639
|
+
*/
|
|
640
|
+
function resolveOptions(options: FromCsrOptions): {
|
|
641
|
+
readonly level: ValidationLevel;
|
|
642
|
+
readonly copy: boolean;
|
|
643
|
+
readonly sortRows: boolean;
|
|
644
|
+
} {
|
|
645
|
+
const level = options.validate ?? "full";
|
|
646
|
+
if (!VALIDATION_LEVELS.has(level)) {
|
|
647
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unsupported validation level ${level}`, {
|
|
648
|
+
field: "validate",
|
|
649
|
+
found: level,
|
|
650
|
+
reason: "unsupported option",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
return { level, copy: options.copy === true, sortRows: options.sortRows !== false };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Build a snapshot from prebuilt CSR arrays (design section 8.1). The arrays are ADOPTED without
|
|
658
|
+
* copying by default (`copy: false`): the caller transfers ownership and must not mutate them
|
|
659
|
+
* afterwards; when they already share one plain ArrayBuffer at 256-byte-aligned offsets in arena
|
|
660
|
+
* order that buffer becomes the snapshot's arena, otherwise `arena` is null. With `copy: true` (or
|
|
661
|
+
* when an array is over a SharedArrayBuffer) the core is copied into a fresh arena. `validate`
|
|
662
|
+
* defaults to "full" (design section 9.5), which computes the flags and verifies the caller's claims
|
|
663
|
+
* (invariant I9); "structure" and "none" trust every claimed flag and compute only the unclaimed ones
|
|
664
|
+
* (design section 8.1: "structure" skips the sortedness / multigraph / flag checks and trusts
|
|
665
|
+
* `flags`). With `sortRows: true` (the default) invariant I4 is checked in O(m) and unsorted rows
|
|
666
|
+
* are rebuilt through the freeze pipeline into a fresh arena; `sortRows: false` asserts sorted rows,
|
|
667
|
+
* checked under "full". A directed input without `arcToEdge` (or with the identity) keeps identity
|
|
668
|
+
* permutations lazy, so `edgeList().weights` aliases `weights`.
|
|
669
|
+
* @param input - the arrays and counts; an undirected input must supply arcToEdge
|
|
670
|
+
* @param options - validation level, copy and sortRows
|
|
671
|
+
* @returns the snapshot; E_INVALID_SNAPSHOT (details.invariant, location) on every rejected input,
|
|
672
|
+
* E_INVALID_ID / E_DUPLICATE_ID for bad ids, E_COLUMN_LENGTH for a column or id array of the wrong
|
|
673
|
+
* length, E_TOO_LARGE for a node count above MAX_COUNT
|
|
674
|
+
*/
|
|
675
|
+
export function fromCsr(input: CsrInput, options: FromCsrOptions = {}): GraphSnapshot {
|
|
676
|
+
const { level, copy, sortRows } = resolveOptions(options);
|
|
677
|
+
checkShape(input);
|
|
678
|
+
const resolved = resolveCore(input, level);
|
|
679
|
+
const { directed, nodeCount } = input;
|
|
680
|
+
const claims = input.flags;
|
|
681
|
+
|
|
682
|
+
// adopt (detecting an arena) or copy
|
|
683
|
+
let core: CoreArrays;
|
|
684
|
+
if (copy || needsCopy(resolved)) {
|
|
685
|
+
core = copyCoreIntoArena(coreArraysOf(resolved, null));
|
|
686
|
+
} else {
|
|
687
|
+
core = coreArraysOf(resolved, detectArena(resolved));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// flags (design sections 3.8, 8.1 and 9.5): "full" computes them and verifies the claims (I9);
|
|
691
|
+
// "structure" and "none" trust every claimed flag and compute only the unclaimed ones
|
|
692
|
+
const trustClaims = level !== "full";
|
|
693
|
+
let flags: SnapshotFlags;
|
|
694
|
+
if (trustClaims && fullyClaimed(claims)) {
|
|
695
|
+
flags = applyClaims(
|
|
696
|
+
{
|
|
697
|
+
...claims,
|
|
698
|
+
weighted: core.weights !== null,
|
|
699
|
+
arcToEdgeIsIdentity: directed && core.arcToEdge === null,
|
|
700
|
+
},
|
|
701
|
+
claims,
|
|
702
|
+
);
|
|
703
|
+
} else {
|
|
704
|
+
const computed = flagsOf(directed, nodeCount, core, resolved.selfLoopCount);
|
|
705
|
+
flags = trustClaims ? applyClaims(computed, claims) : computed;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const parts: SnapshotParts = {
|
|
709
|
+
label: null,
|
|
710
|
+
serial: null,
|
|
711
|
+
directed,
|
|
712
|
+
nodeCount,
|
|
713
|
+
edgeCount: resolved.edgeCount,
|
|
714
|
+
arcCount: resolved.arcCount,
|
|
715
|
+
selfLoopCount: resolved.selfLoopCount,
|
|
716
|
+
rowPtr: core.rowPtr,
|
|
717
|
+
colIdx: core.colIdx,
|
|
718
|
+
weights: core.weights,
|
|
719
|
+
arcToEdge: core.arcToEdge,
|
|
720
|
+
edgeToArc: core.edgeToArc,
|
|
721
|
+
flags,
|
|
722
|
+
ids: idMapOf(input, level, copy),
|
|
723
|
+
nodes: tableOf("node", nodeCount, input.nodeColumns, copy),
|
|
724
|
+
edges: tableOf("edge", resolved.edgeCount, input.edgeColumns, copy),
|
|
725
|
+
graph: createTable("graph", 1),
|
|
726
|
+
extensions: new Map(),
|
|
727
|
+
meta: resolveGraphMeta(input.meta),
|
|
728
|
+
arena: core.arena,
|
|
729
|
+
checksum: false,
|
|
730
|
+
};
|
|
731
|
+
const candidate = createSnapshot(parts);
|
|
732
|
+
|
|
733
|
+
if (sortRows && !rowsSorted(candidate)) {
|
|
734
|
+
// unsorted rows: check what can be checked without sorted rows, then rebuild
|
|
735
|
+
if (level !== "none") {
|
|
736
|
+
validateStructure(candidate);
|
|
737
|
+
}
|
|
738
|
+
if (level === "full") {
|
|
739
|
+
orderIndependentFullChecks(candidate);
|
|
740
|
+
}
|
|
741
|
+
const rebuiltParts = rebuildSorted(candidate, parts);
|
|
742
|
+
const rebuilt = createSnapshot({
|
|
743
|
+
...rebuiltParts,
|
|
744
|
+
flags: trustClaims ? applyClaims(rebuiltParts.flags, claims) : rebuiltParts.flags,
|
|
745
|
+
});
|
|
746
|
+
if (level === "full") {
|
|
747
|
+
if (!directed) {
|
|
748
|
+
compareRows(candidate, rebuilt);
|
|
749
|
+
}
|
|
750
|
+
verifyClaims(claims, rebuilt.flags);
|
|
751
|
+
}
|
|
752
|
+
return rebuilt;
|
|
753
|
+
}
|
|
754
|
+
if (level === "structure") {
|
|
755
|
+
validateStructure(candidate);
|
|
756
|
+
} else if (level === "full") {
|
|
757
|
+
validateFull(candidate);
|
|
758
|
+
verifyClaims(claims, candidate.flags);
|
|
759
|
+
}
|
|
760
|
+
return candidate;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/** The flags that describe which arrays are PRESENT; they are never taken from a claim (I9 at every level). */
|
|
764
|
+
const PRESENCE_FLAGS: readonly (keyof SnapshotFlags)[] = ["weighted", "arcToEdgeIsIdentity"];
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* The trusted flags of a "structure" or "none" level construction: every claimed predicate flag as
|
|
768
|
+
* given, every other one as computed. The two presence flags (`weighted`, `arcToEdgeIsIdentity`)
|
|
769
|
+
* follow the arrays at every level, and a claim that contradicts them is I9 immediately: a claim
|
|
770
|
+
* must never change which arrays the snapshot holds.
|
|
771
|
+
* @param computed - the flags computed from the arrays
|
|
772
|
+
* @param claims - the caller's claims, or undefined
|
|
773
|
+
* @returns the flags to store
|
|
774
|
+
*/
|
|
775
|
+
function applyClaims(computed: SnapshotFlags, claims: FlagClaims | undefined): SnapshotFlags {
|
|
776
|
+
if (claims === undefined) {
|
|
777
|
+
return computed;
|
|
778
|
+
}
|
|
779
|
+
const out: { -readonly [K in keyof SnapshotFlags]: boolean } = { ...computed };
|
|
780
|
+
for (const name of FLAG_NAMES) {
|
|
781
|
+
const value = claims[name];
|
|
782
|
+
if (value === undefined) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (PRESENCE_FLAGS.includes(name)) {
|
|
786
|
+
if (value !== computed[name]) {
|
|
787
|
+
throw invariantViolation(
|
|
788
|
+
"I9",
|
|
789
|
+
`flag ${name} is claimed ${value} but the arrays say ${computed[name]}`,
|
|
790
|
+
{
|
|
791
|
+
flag: name,
|
|
792
|
+
found: value,
|
|
793
|
+
expected: computed[name],
|
|
794
|
+
},
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
out[name] = value;
|
|
800
|
+
}
|
|
801
|
+
return out;
|
|
802
|
+
}
|