@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,1503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The concrete GraphSnapshot class (design sections 3, 5.8, 7, 9.3 and 11.4) and its factory
|
|
3
|
+
* `createSnapshot(parts)`, plus the boundary helpers `isGraphSnapshot` / `equalsTopology` (design
|
|
4
|
+
* section 7.5) and the arena helpers a GPU consumer binds segments with (section 10.3).
|
|
5
|
+
*
|
|
6
|
+
* The object is frozen with `Object.freeze` so `snapshot.rowPtr = x` throws in strict mode; every
|
|
7
|
+
* lazily populated member (the identity permutations of design section 3.1, the view cache of
|
|
8
|
+
* section 7.2, the checksum records of section 5.8, the content hash, the edge id index) lives in
|
|
9
|
+
* one mutable state record reachable only through a private field, which `Object.freeze` does not
|
|
10
|
+
* reach. `arcToEdge` / `edgeToArc` are prototype getters that materialise an identity permutation on
|
|
11
|
+
* first access outside the arena (never on the wire, never in `byteLength()`); `rowPtr`, `colIdx`
|
|
12
|
+
* and `weights` are plain data properties so hot loops pay nothing. Every method that reads the core
|
|
13
|
+
* throws `E_DETACHED` once the core buffer was transferred away (`detached` is derived:
|
|
14
|
+
* `rowPtr.length === 0`).
|
|
15
|
+
*
|
|
16
|
+
* Views are computed by views.ts, queries by queries.ts, derived graphs by derived.ts (which returns
|
|
17
|
+
* SnapshotParts that this module wraps), invariant checks by validate.ts and hashes by hash.ts. The
|
|
18
|
+
* wire methods (`toWire`, `toBytes`, `toByteChunks`, `transferables`) call the wire module directly;
|
|
19
|
+
* the wire module imports this one too, and the cycle is safe because each side reaches the other
|
|
20
|
+
* only through hoisted function declarations invoked at call time, never at module load.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { dropGpuViewCache, isColumnDetached } from "../columns/column.js";
|
|
24
|
+
import { columnSetVersion } from "../columns/table.js";
|
|
25
|
+
import { FORMAT_VERSION, INVALID_INDEX, SNAPSHOT_BRAND } from "../constants.js";
|
|
26
|
+
import { GraphFormatError } from "../errors.js";
|
|
27
|
+
import { EdgeIdIndex } from "../ids/edge-id-index.js";
|
|
28
|
+
import { idMapBuffers, idMapDetached } from "../ids/node-id-map.js";
|
|
29
|
+
import {
|
|
30
|
+
type ArenaLayout,
|
|
31
|
+
type AttributeTable,
|
|
32
|
+
type ByteLengthOptions,
|
|
33
|
+
type Column,
|
|
34
|
+
type ColumnInput,
|
|
35
|
+
type ContractOptions,
|
|
36
|
+
type CooView,
|
|
37
|
+
type CoreArrayName,
|
|
38
|
+
type DegreeOrderOptions,
|
|
39
|
+
type DegreeOrderView,
|
|
40
|
+
type DerivedGraph,
|
|
41
|
+
type EdgeId,
|
|
42
|
+
type EdgeListView,
|
|
43
|
+
type EdgeMask,
|
|
44
|
+
type F32,
|
|
45
|
+
type F64,
|
|
46
|
+
type GraphMeta,
|
|
47
|
+
type GraphSnapshotContract,
|
|
48
|
+
type NodeIdMap,
|
|
49
|
+
type NodeMask,
|
|
50
|
+
type ReverseView,
|
|
51
|
+
type SimplifyOptions,
|
|
52
|
+
type SnapshotFlags,
|
|
53
|
+
type ToBytesOptions,
|
|
54
|
+
type ToUndirectedOptions,
|
|
55
|
+
type ToWireOptions,
|
|
56
|
+
type TypedArrayData,
|
|
57
|
+
type U8,
|
|
58
|
+
type U32,
|
|
59
|
+
type ValidateOptions,
|
|
60
|
+
type ViewName,
|
|
61
|
+
type WireSnapshot,
|
|
62
|
+
} from "../types/index.js";
|
|
63
|
+
import { type SnapshotParts, type ViewCache } from "../types/internal.js";
|
|
64
|
+
import { assertOneOf } from "../util/options.js";
|
|
65
|
+
import { claimHolder } from "../util/shared-buffers.js";
|
|
66
|
+
import { toByteChunks, toBytes } from "../wire/bytes.js";
|
|
67
|
+
import { toWire, transferables } from "../wire/to-wire.js";
|
|
68
|
+
import {
|
|
69
|
+
deriveContract,
|
|
70
|
+
type DerivedParts,
|
|
71
|
+
deriveFilterEdges,
|
|
72
|
+
deriveInducedSubgraph,
|
|
73
|
+
deriveRelabel,
|
|
74
|
+
deriveSimplified,
|
|
75
|
+
deriveToUndirected,
|
|
76
|
+
deriveTranspose,
|
|
77
|
+
deriveWithoutSelfLoops,
|
|
78
|
+
identityDerived,
|
|
79
|
+
withColumnsParts,
|
|
80
|
+
} from "./derived.js";
|
|
81
|
+
import { contentHashOf, hashColumn, hashTypedArray } from "./hash.js";
|
|
82
|
+
import { arcRangeIn, arcSourceIn, findArcIn, multiplicityIn, selfLoopsAtIn } from "./queries.js";
|
|
83
|
+
import { validateFull, validateStructure } from "./validate.js";
|
|
84
|
+
import {
|
|
85
|
+
computeCoo,
|
|
86
|
+
computeDegreeOrder,
|
|
87
|
+
computeEdgeList,
|
|
88
|
+
computeMate,
|
|
89
|
+
computeReverse,
|
|
90
|
+
computeSelfLoops,
|
|
91
|
+
computeSelfLoopWeight,
|
|
92
|
+
computeSymmetric,
|
|
93
|
+
computeTotalWeight,
|
|
94
|
+
identityPermutation,
|
|
95
|
+
rowLengths,
|
|
96
|
+
rowWeightSums,
|
|
97
|
+
sumDegrees,
|
|
98
|
+
sumF64,
|
|
99
|
+
viewArrays,
|
|
100
|
+
viewByteLength,
|
|
101
|
+
widenToF64,
|
|
102
|
+
} from "./views.js";
|
|
103
|
+
|
|
104
|
+
// ============================================================ module state
|
|
105
|
+
|
|
106
|
+
/** The next process-unique serial (design section 5.8). */
|
|
107
|
+
let nextSerial = 1;
|
|
108
|
+
|
|
109
|
+
/** Every view name in the order `cachedViews()` reports them. */
|
|
110
|
+
const VIEW_NAMES: readonly ViewName[] = [
|
|
111
|
+
"reverse",
|
|
112
|
+
"coo",
|
|
113
|
+
"edgeList",
|
|
114
|
+
"outDegree",
|
|
115
|
+
"inDegree",
|
|
116
|
+
"degree",
|
|
117
|
+
"weightedOutDegree",
|
|
118
|
+
"weightedInDegree",
|
|
119
|
+
"weightedDegree",
|
|
120
|
+
"selfLoopWeight",
|
|
121
|
+
"totalWeight",
|
|
122
|
+
"selfLoopArcs",
|
|
123
|
+
"selfLoopsPerNode",
|
|
124
|
+
"mate",
|
|
125
|
+
"degreeOrder",
|
|
126
|
+
"reverseDegreeOrder",
|
|
127
|
+
"symmetric",
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The own enumerable property every snapshot carries whose value is a function, so that
|
|
132
|
+
* `structuredClone(snapshot)` and `postMessage(snapshot)` throw `DataCloneError` immediately (design
|
|
133
|
+
* section 7.5; the structured clone algorithm refuses functions and visits enumerable own
|
|
134
|
+
* properties only).
|
|
135
|
+
*/
|
|
136
|
+
export const CLONE_GUARD_KEY = "__graphtyNoStructuredClone";
|
|
137
|
+
|
|
138
|
+
export { EMPTY_GRAPH_META } from "./graph-meta.js";
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The value of the clone guard property.
|
|
142
|
+
* @returns nothing
|
|
143
|
+
*/
|
|
144
|
+
function cloneGuard(): void {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The FNV-1a records of design section 5.8, kept when the snapshot was built with `checksum: true`. */
|
|
149
|
+
interface ChecksumRecords {
|
|
150
|
+
/** Core array name -> digest (identity permutations recorded when materialised). */
|
|
151
|
+
readonly core: Map<string, string>;
|
|
152
|
+
/** Immutable column -> digest, at construction. */
|
|
153
|
+
readonly columns: WeakMap<Column, string>;
|
|
154
|
+
/** "<view>.<member>" -> digest, at first materialisation. */
|
|
155
|
+
readonly views: Map<string, string>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The mutable state behind a frozen snapshot object. */
|
|
159
|
+
interface SnapshotState {
|
|
160
|
+
/** The arc -> edge permutation, or null until an identity permutation is materialised. */
|
|
161
|
+
arcToEdge: U32 | null;
|
|
162
|
+
/** The edge -> arc permutation, or null until an identity permutation is materialised. */
|
|
163
|
+
edgeToArc: U32 | null;
|
|
164
|
+
/** The view cache (one slot per ViewName). */
|
|
165
|
+
readonly views: ViewCache;
|
|
166
|
+
/** Checksum records, or null when the snapshot was built without `checksum: true`. */
|
|
167
|
+
readonly checksums: ChecksumRecords | null;
|
|
168
|
+
/** The cached content hash. */
|
|
169
|
+
contentHash: string | null;
|
|
170
|
+
/** The lazily built edge id index and the column it was built over. */
|
|
171
|
+
edgeIds: { readonly version: number; readonly column: Column | null; readonly index: EdgeIdIndex | null } | null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Module-private access to the state of a snapshot for the helpers below. */
|
|
175
|
+
const STATES = new WeakMap<GraphSnapshot, SnapshotState>();
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* A fresh view cache with every slot empty.
|
|
179
|
+
* @returns the cache
|
|
180
|
+
*/
|
|
181
|
+
function emptyViewCache(): ViewCache {
|
|
182
|
+
return {
|
|
183
|
+
reverse: null,
|
|
184
|
+
coo: null,
|
|
185
|
+
edgeList: null,
|
|
186
|
+
outDegree: null,
|
|
187
|
+
inDegree: null,
|
|
188
|
+
degree: null,
|
|
189
|
+
weightedOutDegree: null,
|
|
190
|
+
weightedInDegree: null,
|
|
191
|
+
weightedDegree: null,
|
|
192
|
+
selfLoopWeight: null,
|
|
193
|
+
totalWeight: null,
|
|
194
|
+
selfLoopArcs: null,
|
|
195
|
+
selfLoopsPerNode: null,
|
|
196
|
+
mate: null,
|
|
197
|
+
degreeOrder: null,
|
|
198
|
+
reverseDegreeOrder: null,
|
|
199
|
+
symmetric: null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The E_INVALID_SNAPSHOT error of a SnapshotParts inconsistency caught at construction.
|
|
205
|
+
* @param invariant - the invariant number
|
|
206
|
+
* @param message - the description
|
|
207
|
+
* @param details - the location
|
|
208
|
+
* @returns the error
|
|
209
|
+
*/
|
|
210
|
+
function partsError(invariant: string, message: string, details: Readonly<Record<string, unknown>>): GraphFormatError {
|
|
211
|
+
return new GraphFormatError("E_INVALID_SNAPSHOT", `invariant ${invariant} violated: ${message}`, {
|
|
212
|
+
invariant,
|
|
213
|
+
...details,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The O(1) consistency checks the constructor runs on the parts a producer hands it (lengths and
|
|
219
|
+
* the identity-flag rule); everything deeper is `validate()`.
|
|
220
|
+
* @param parts - the parts
|
|
221
|
+
*/
|
|
222
|
+
function checkParts(parts: SnapshotParts): void {
|
|
223
|
+
const { nodeCount, edgeCount, arcCount, flags } = parts;
|
|
224
|
+
if (parts.rowPtr.length !== nodeCount + 1) {
|
|
225
|
+
throw partsError("I1", `rowPtr has ${parts.rowPtr.length} entries, expected ${nodeCount + 1}`, {
|
|
226
|
+
expected: nodeCount + 1,
|
|
227
|
+
found: parts.rowPtr.length,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (parts.colIdx.length !== arcCount) {
|
|
231
|
+
throw partsError("I1", `colIdx has ${parts.colIdx.length} entries, expected ${arcCount}`, {
|
|
232
|
+
expected: arcCount,
|
|
233
|
+
found: parts.colIdx.length,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (parts.weights !== null && parts.weights.length !== arcCount) {
|
|
237
|
+
throw partsError("I8", `weights has ${parts.weights.length} entries, expected ${arcCount}`, {
|
|
238
|
+
expected: arcCount,
|
|
239
|
+
found: parts.weights.length,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const identity = flags.arcToEdgeIsIdentity;
|
|
243
|
+
if ((parts.arcToEdge === null) !== identity || (parts.edgeToArc === null) !== identity) {
|
|
244
|
+
throw partsError("I9", "arcToEdge / edgeToArc must be null exactly when flags.arcToEdgeIsIdentity", {
|
|
245
|
+
flag: "arcToEdgeIsIdentity",
|
|
246
|
+
found: identity,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (identity && (!parts.directed || arcCount !== edgeCount)) {
|
|
250
|
+
throw partsError("I9", "flags.arcToEdgeIsIdentity requires a directed snapshot with arcCount === edgeCount", {
|
|
251
|
+
flag: "arcToEdgeIsIdentity",
|
|
252
|
+
directed: parts.directed,
|
|
253
|
+
arcCount,
|
|
254
|
+
edgeCount,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (parts.arcToEdge !== null && parts.arcToEdge.length !== arcCount) {
|
|
258
|
+
throw partsError("I5", `arcToEdge has ${parts.arcToEdge.length} entries, expected ${arcCount}`, {
|
|
259
|
+
expected: arcCount,
|
|
260
|
+
found: parts.arcToEdge.length,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (parts.edgeToArc !== null && parts.edgeToArc.length !== edgeCount) {
|
|
264
|
+
throw partsError("I5", `edgeToArc has ${parts.edgeToArc.length} entries, expected ${edgeCount}`, {
|
|
265
|
+
expected: edgeCount,
|
|
266
|
+
found: parts.edgeToArc.length,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (parts.ids.size !== nodeCount) {
|
|
270
|
+
throw partsError("I11", `ids.size is ${parts.ids.size}, expected ${nodeCount}`, {
|
|
271
|
+
expected: nodeCount,
|
|
272
|
+
found: parts.ids.size,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (parts.nodes.rowCount !== nodeCount || parts.edges.rowCount !== edgeCount || parts.graph.rowCount !== 1) {
|
|
276
|
+
throw partsError("I12", "table row counts do not match nodeCount / edgeCount / 1", {
|
|
277
|
+
nodes: parts.nodes.rowCount,
|
|
278
|
+
edges: parts.edges.rowCount,
|
|
279
|
+
graph: parts.graph.rowCount,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Record the digests of every immutable column of a table.
|
|
286
|
+
* @param records - the records
|
|
287
|
+
* @param table - the table
|
|
288
|
+
*/
|
|
289
|
+
function recordColumns(records: ChecksumRecords, table: AttributeTable): void {
|
|
290
|
+
for (const column of table) {
|
|
291
|
+
if (!column.meta.mutable) {
|
|
292
|
+
records.columns.set(column, hashColumn(column));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================================================ the class
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The frozen graph (design section 3): a CSR core over 4-byte typed arrays, the id map, the
|
|
301
|
+
* attribute tables, the flags and a per-instance view cache. Topology, counts, flags, id map and
|
|
302
|
+
* immutable columns never change after construction (invariant I17); the column SET of `nodes` /
|
|
303
|
+
* `edges` / `graph` and the CONTENTS of columns declared mutable are mutable side tables (design
|
|
304
|
+
* section 5.8). Every view is a pure function of the core, memoised once and SHARED: a call returns
|
|
305
|
+
* the cached array itself, so writing into a view is a contract violation; call `.slice()` for
|
|
306
|
+
* scratch. Index-taking queries are total for in-range arguments and unchecked otherwise (design
|
|
307
|
+
* section 11.1). Instances come from `createSnapshot()` (the builder, `fromCsr`, `fromWire` and the
|
|
308
|
+
* derived-graph methods); `isGraphSnapshot()` recognises them structurally by brand and
|
|
309
|
+
* formatVersion.
|
|
310
|
+
*/
|
|
311
|
+
export class GraphSnapshot implements GraphSnapshotContract {
|
|
312
|
+
/** Symbol.for brand read by isGraphSnapshot() (design section 7.5). */
|
|
313
|
+
readonly [SNAPSHOT_BRAND]: true;
|
|
314
|
+
/** Process-unique identity of the CORE; shared by withColumns() snapshots (design section 5.8). */
|
|
315
|
+
readonly serial: number;
|
|
316
|
+
/** Debugging aid supplied at freeze. */
|
|
317
|
+
readonly label: string | null;
|
|
318
|
+
/** The data-model major (design section 13.5). */
|
|
319
|
+
readonly formatVersion: 1;
|
|
320
|
+
/** Whether the graph is directed. */
|
|
321
|
+
readonly directed: boolean;
|
|
322
|
+
/** n (invariant I3). */
|
|
323
|
+
readonly nodeCount: number;
|
|
324
|
+
/** Number of logical edges (invariant I13). */
|
|
325
|
+
readonly edgeCount: number;
|
|
326
|
+
/** colIdx.length (invariants I6, I7). */
|
|
327
|
+
readonly arcCount: number;
|
|
328
|
+
/** Logical edges with source === target. */
|
|
329
|
+
readonly selfLoopCount: number;
|
|
330
|
+
/** nodeCount + 1 row offsets (invariant I1). */
|
|
331
|
+
readonly rowPtr: U32;
|
|
332
|
+
/** Target node index of each arc, sorted within each row (invariants I2, I4). */
|
|
333
|
+
readonly colIdx: U32;
|
|
334
|
+
/** arcCount f32 weights; null when unweighted. */
|
|
335
|
+
readonly weights: F32 | null;
|
|
336
|
+
/** Flags kernels branch on (design section 3.8). */
|
|
337
|
+
readonly flags: SnapshotFlags;
|
|
338
|
+
/** The id map (design section 4). */
|
|
339
|
+
readonly ids: NodeIdMap;
|
|
340
|
+
/** Node attribute table, rowCount === nodeCount. */
|
|
341
|
+
readonly nodes: AttributeTable;
|
|
342
|
+
/** Edge attribute table, rowCount === edgeCount, indexed by logical edge. */
|
|
343
|
+
readonly edges: AttributeTable;
|
|
344
|
+
/** Graph attribute table, rowCount === 1. */
|
|
345
|
+
readonly graph: AttributeTable;
|
|
346
|
+
/** Extension tables keyed by name (design section 5.10). */
|
|
347
|
+
readonly extensions: ReadonlyMap<string, AttributeTable>;
|
|
348
|
+
/** Graph-level metadata (design section 5.9). */
|
|
349
|
+
readonly meta: GraphMeta;
|
|
350
|
+
/** The arena holding the core arrays, or null (design section 10.3). */
|
|
351
|
+
readonly arena: ArenaLayout | null;
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The lazily populated members (design section 5.8): defined as a NON-enumerable own property in
|
|
355
|
+
* the constructor (`declare` keeps tsc from emitting a field), so `Object.keys(snapshot)` lists
|
|
356
|
+
* exactly the public fields plus the documented clone guard, and JSON / spread / structured
|
|
357
|
+
* clone never walk the view cache.
|
|
358
|
+
*/
|
|
359
|
+
declare private readonly state: SnapshotState;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Wrap prepared parts. Producers call `createSnapshot()`; the constructor is not part of the public
|
|
363
|
+
* surface. The producer has established I1-I13; the constructor checks the O(1) length rules,
|
|
364
|
+
* records checksums when asked, freezes the object and installs the clone guard.
|
|
365
|
+
* @param parts - the parts (no array may alias memory a builder can still write, invariant I18)
|
|
366
|
+
* @internal
|
|
367
|
+
*/
|
|
368
|
+
constructor(parts: SnapshotParts) {
|
|
369
|
+
checkParts(parts);
|
|
370
|
+
this[SNAPSHOT_BRAND] = true;
|
|
371
|
+
this.serial = parts.serial ?? nextSerial++;
|
|
372
|
+
this.label = parts.label;
|
|
373
|
+
this.formatVersion = FORMAT_VERSION;
|
|
374
|
+
this.directed = parts.directed;
|
|
375
|
+
this.nodeCount = parts.nodeCount;
|
|
376
|
+
this.edgeCount = parts.edgeCount;
|
|
377
|
+
this.arcCount = parts.arcCount;
|
|
378
|
+
this.selfLoopCount = parts.selfLoopCount;
|
|
379
|
+
this.rowPtr = parts.rowPtr;
|
|
380
|
+
this.colIdx = parts.colIdx;
|
|
381
|
+
this.weights = parts.weights;
|
|
382
|
+
this.flags = Object.freeze({ ...parts.flags });
|
|
383
|
+
this.ids = parts.ids;
|
|
384
|
+
this.nodes = parts.nodes;
|
|
385
|
+
this.edges = parts.edges;
|
|
386
|
+
this.graph = parts.graph;
|
|
387
|
+
this.extensions = parts.extensions;
|
|
388
|
+
this.meta = parts.meta;
|
|
389
|
+
this.arena = parts.arena;
|
|
390
|
+
let checksums: ChecksumRecords | null = null;
|
|
391
|
+
if (parts.checksum) {
|
|
392
|
+
checksums = { core: new Map(), columns: new WeakMap(), views: new Map() };
|
|
393
|
+
checksums.core.set("rowPtr", hashTypedArray(parts.rowPtr));
|
|
394
|
+
checksums.core.set("colIdx", hashTypedArray(parts.colIdx));
|
|
395
|
+
if (parts.weights !== null) {
|
|
396
|
+
checksums.core.set("weights", hashTypedArray(parts.weights));
|
|
397
|
+
}
|
|
398
|
+
if (parts.arcToEdge !== null) {
|
|
399
|
+
checksums.core.set("arcToEdge", hashTypedArray(parts.arcToEdge));
|
|
400
|
+
}
|
|
401
|
+
if (parts.edgeToArc !== null) {
|
|
402
|
+
checksums.core.set("edgeToArc", hashTypedArray(parts.edgeToArc));
|
|
403
|
+
}
|
|
404
|
+
recordColumns(checksums, parts.nodes);
|
|
405
|
+
recordColumns(checksums, parts.edges);
|
|
406
|
+
recordColumns(checksums, parts.graph);
|
|
407
|
+
for (const table of parts.extensions.values()) {
|
|
408
|
+
recordColumns(checksums, table);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const state: SnapshotState = {
|
|
412
|
+
arcToEdge: parts.arcToEdge,
|
|
413
|
+
edgeToArc: parts.edgeToArc,
|
|
414
|
+
views: emptyViewCache(),
|
|
415
|
+
checksums,
|
|
416
|
+
contentHash: null,
|
|
417
|
+
edgeIds: null,
|
|
418
|
+
};
|
|
419
|
+
Object.defineProperty(this, "state", { value: state, enumerable: false, writable: false, configurable: false });
|
|
420
|
+
STATES.set(this, state);
|
|
421
|
+
claimStorage(parts);
|
|
422
|
+
Object.defineProperty(this, CLONE_GUARD_KEY, {
|
|
423
|
+
value: cloneGuard,
|
|
424
|
+
enumerable: true,
|
|
425
|
+
writable: false,
|
|
426
|
+
configurable: false,
|
|
427
|
+
});
|
|
428
|
+
Object.freeze(this);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------- core accessors
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* rowPtr.length === 0: the core buffer was transferred away (design section 9.4). Derived from the
|
|
435
|
+
* array state, never a stored bit, so every holder of the same core agrees.
|
|
436
|
+
* @returns true when detached
|
|
437
|
+
*/
|
|
438
|
+
get detached(): boolean {
|
|
439
|
+
return this.rowPtr.length === 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Logical edge of every arc (invariant I5); an identity permutation is materialised on first
|
|
444
|
+
* access as a separate 4-byte-aligned array outside the arena. GPU code tests
|
|
445
|
+
* `flags.arcToEdgeIsIdentity` before reading it.
|
|
446
|
+
* @returns arcCount entries; E_DETACHED after a consuming transfer
|
|
447
|
+
*/
|
|
448
|
+
get arcToEdge(): U32 {
|
|
449
|
+
this.assertAttached();
|
|
450
|
+
const { state } = this;
|
|
451
|
+
if (state.arcToEdge === null) {
|
|
452
|
+
state.arcToEdge = identityPermutation(this.arcCount);
|
|
453
|
+
state.checksums?.core.set("arcToEdge", hashTypedArray(state.arcToEdge));
|
|
454
|
+
}
|
|
455
|
+
return state.arcToEdge;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* The arc holding the declared orientation of every logical edge (invariant I5); an identity
|
|
460
|
+
* permutation is materialised on first access outside the arena.
|
|
461
|
+
* @returns edgeCount entries; E_DETACHED after a consuming transfer
|
|
462
|
+
*/
|
|
463
|
+
get edgeToArc(): U32 {
|
|
464
|
+
this.assertAttached();
|
|
465
|
+
const { state } = this;
|
|
466
|
+
if (state.edgeToArc === null) {
|
|
467
|
+
state.edgeToArc = identityPermutation(this.edgeCount);
|
|
468
|
+
state.checksums?.core.set("edgeToArc", hashTypedArray(state.edgeToArc));
|
|
469
|
+
}
|
|
470
|
+
return state.edgeToArc;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ---------------------------------------------------------------- queries (3.9)
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The out-arc range of a node: [rowPtr[u], rowPtr[u + 1]]. Allocates a tuple; hot loops read
|
|
477
|
+
* rowPtr directly.
|
|
478
|
+
* @param u - the node index
|
|
479
|
+
* @returns the half-open arc range as [start, end]
|
|
480
|
+
*/
|
|
481
|
+
outArcs(u: number): readonly [start: number, end: number] {
|
|
482
|
+
this.assertAttached();
|
|
483
|
+
return [this.rowPtr[u], this.rowPtr[u + 1]];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* rowPtr[u + 1] - rowPtr[u]: the out-arc count (a self-loop counted once).
|
|
488
|
+
* @param u - the node index
|
|
489
|
+
* @returns the out-degree
|
|
490
|
+
*/
|
|
491
|
+
outDegreeOf(u: number): number {
|
|
492
|
+
this.assertAttached();
|
|
493
|
+
return this.rowPtr[u + 1] - this.rowPtr[u];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Binary search for the first arc u -> v (the lowest logical edge index among parallels).
|
|
498
|
+
* @param u - the source node index
|
|
499
|
+
* @param v - the target node index
|
|
500
|
+
* @returns the arc index, or INVALID_INDEX when absent
|
|
501
|
+
*/
|
|
502
|
+
findArc(u: number, v: number): number {
|
|
503
|
+
this.assertAttached();
|
|
504
|
+
return findArcIn(this.rowPtr, this.colIdx, u, v);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Whether an arc u -> v exists.
|
|
509
|
+
* @param u - the source node index
|
|
510
|
+
* @param v - the target node index
|
|
511
|
+
* @returns findArc(u, v) !== INVALID_INDEX
|
|
512
|
+
*/
|
|
513
|
+
hasArc(u: number, v: number): boolean {
|
|
514
|
+
return this.findArc(u, v) !== INVALID_INDEX;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* The half-open arc range [lo, hi) of every arc u -> v; empty when lo === hi. Allocates a tuple.
|
|
519
|
+
* @param u - the source node index
|
|
520
|
+
* @param v - the target node index
|
|
521
|
+
* @returns the range as [lo, hi]
|
|
522
|
+
*/
|
|
523
|
+
arcsBetween(u: number, v: number): readonly [lo: number, hi: number] {
|
|
524
|
+
this.assertAttached();
|
|
525
|
+
return arcRangeIn(this.rowPtr, this.colIdx, u, v);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Number of parallel arcs u -> v.
|
|
530
|
+
* @param u - the source node index
|
|
531
|
+
* @param v - the target node index
|
|
532
|
+
* @returns hi - lo of arcsBetween(u, v)
|
|
533
|
+
*/
|
|
534
|
+
multiplicity(u: number, v: number): number {
|
|
535
|
+
this.assertAttached();
|
|
536
|
+
return multiplicityIn(this.rowPtr, this.colIdx, u, v);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* The row containing an arc: O(1) through the cached coo() view, else a binary search on rowPtr.
|
|
541
|
+
* @param a - the arc index
|
|
542
|
+
* @returns the source node index
|
|
543
|
+
*/
|
|
544
|
+
arcSource(a: number): number {
|
|
545
|
+
this.assertAttached();
|
|
546
|
+
const { coo } = this.state.views;
|
|
547
|
+
if (coo !== null) {
|
|
548
|
+
return coo.src[a];
|
|
549
|
+
}
|
|
550
|
+
return arcSourceIn(this.rowPtr, a);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Declared source of a logical edge: arcSource(edgeToArc[e]).
|
|
555
|
+
* @param e - the logical edge index
|
|
556
|
+
* @returns the source node index
|
|
557
|
+
*/
|
|
558
|
+
edgeSource(e: number): number {
|
|
559
|
+
this.assertAttached();
|
|
560
|
+
const list = this.state.views.edgeList;
|
|
561
|
+
if (list !== null) {
|
|
562
|
+
return list.src[e];
|
|
563
|
+
}
|
|
564
|
+
return this.arcSource(this.edgeToArc[e]);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Declared target of a logical edge: colIdx[edgeToArc[e]].
|
|
569
|
+
* @param e - the logical edge index
|
|
570
|
+
* @returns the target node index
|
|
571
|
+
*/
|
|
572
|
+
edgeTarget(e: number): number {
|
|
573
|
+
return this.colIdx[this.edgeToArc[e]];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Lookup through the role "id" edge column, backed by a Map built on first call and rebuilt when the
|
|
578
|
+
* column is replaced or a mutable id column's version changes (design section 4.6).
|
|
579
|
+
* @param id - the edge id
|
|
580
|
+
* @returns the logical edge index, or INVALID_INDEX on a miss or when no id column exists
|
|
581
|
+
*/
|
|
582
|
+
edgeIndexOf(id: EdgeId): number {
|
|
583
|
+
const { state } = this;
|
|
584
|
+
const version = columnSetVersion(this.edges);
|
|
585
|
+
if (state.edgeIds === null || state.edgeIds.version !== version) {
|
|
586
|
+
// the role lookup is repeated only when the column set changed
|
|
587
|
+
const column = this.edges.byRole("id");
|
|
588
|
+
state.edgeIds = { version, column, index: column === null ? null : new EdgeIdIndex(column) };
|
|
589
|
+
}
|
|
590
|
+
return state.edgeIds.index === null ? INVALID_INDEX : state.edgeIds.index.indexOf(id);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ---------------------------------------------------------------- views (7.2)
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* The in-adjacency, rows sorted by source; the forward arrays themselves when undirected. Cached
|
|
597
|
+
* and shared.
|
|
598
|
+
* @returns the reverse view
|
|
599
|
+
*/
|
|
600
|
+
reverse(): ReverseView {
|
|
601
|
+
return this.cached("reverse", () => computeReverse(this));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Per-arc COO form; src is the only new array. Cached and shared.
|
|
606
|
+
* @returns the COO view
|
|
607
|
+
*/
|
|
608
|
+
coo(): CooView {
|
|
609
|
+
return this.cached("coo", () => computeCoo(this));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Every logical edge once in declared orientation. Cached and shared.
|
|
614
|
+
* @returns the edge list view
|
|
615
|
+
*/
|
|
616
|
+
edgeList(): EdgeListView {
|
|
617
|
+
return this.cached("edgeList", () => computeEdgeList(this));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* rowPtr differences materialised; a self-loop counted once. Cached and shared: call `.slice()`
|
|
622
|
+
* before writing.
|
|
623
|
+
* @returns Uint32Array(n)
|
|
624
|
+
*/
|
|
625
|
+
outDegree(): U32 {
|
|
626
|
+
return this.cached("outDegree", () => rowLengths(this.rowPtr, this.nodeCount));
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Arcs targeting each node, via reverse().rowPtr; the same object as outDegree() when undirected.
|
|
631
|
+
* Cached and shared: call `.slice()` before writing.
|
|
632
|
+
* @returns Uint32Array(n)
|
|
633
|
+
*/
|
|
634
|
+
inDegree(): U32 {
|
|
635
|
+
return this.cached("inDegree", () =>
|
|
636
|
+
this.directed ? rowLengths(this.reverse().rowPtr, this.nodeCount) : this.outDegree(),
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Graph-theoretic degree (NetworkX convention, design section 3.4): in + out when directed; out
|
|
642
|
+
* plus self-loops when undirected. Cached and shared: call `.slice()` before writing.
|
|
643
|
+
* @returns Uint32Array(n)
|
|
644
|
+
*/
|
|
645
|
+
degree(): U32 {
|
|
646
|
+
return this.cached("degree", () =>
|
|
647
|
+
sumDegrees(this.outDegree(), this.directed ? this.inDegree() : this.selfLoopsPerNode()),
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Row sums of weights (a self-loop arc counted once); outDegree widened when unweighted. F64 for
|
|
653
|
+
* CPU precision; may be 0 for a node with out-arcs. Cached and shared: call `.slice()` before
|
|
654
|
+
* writing.
|
|
655
|
+
* @returns Float64Array(n)
|
|
656
|
+
*/
|
|
657
|
+
weightedOutDegree(): F64 {
|
|
658
|
+
return this.cached("weightedOutDegree", () =>
|
|
659
|
+
this.weights === null
|
|
660
|
+
? widenToF64(this.outDegree())
|
|
661
|
+
: rowWeightSums(this.rowPtr, this.weights, this.nodeCount),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Weight sums over incoming arcs; the same object as weightedOutDegree() when undirected. Cached
|
|
667
|
+
* and shared: call `.slice()` before writing.
|
|
668
|
+
* @returns Float64Array(n)
|
|
669
|
+
*/
|
|
670
|
+
weightedInDegree(): F64 {
|
|
671
|
+
return this.cached("weightedInDegree", () => {
|
|
672
|
+
if (!this.directed) {
|
|
673
|
+
return this.weightedOutDegree();
|
|
674
|
+
}
|
|
675
|
+
const reverse = this.reverse();
|
|
676
|
+
return reverse.weights === null
|
|
677
|
+
? widenToF64(this.inDegree())
|
|
678
|
+
: rowWeightSums(reverse.rowPtr, reverse.weights, this.nodeCount);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* NetworkX weighted degree: weightedOutDegree + (directed ? weightedInDegree : selfLoopWeight), so
|
|
684
|
+
* the sum is 2 * totalWeight() when undirected. Cached and shared: call `.slice()` before writing.
|
|
685
|
+
* @returns Float64Array(n)
|
|
686
|
+
*/
|
|
687
|
+
weightedDegree(): F64 {
|
|
688
|
+
return this.cached("weightedDegree", () =>
|
|
689
|
+
sumF64(this.weightedOutDegree(), this.directed ? this.weightedInDegree() : this.selfLoopWeight()),
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Sum of weights over each node's self-loop arcs (selfLoopsPerNode widened when unweighted).
|
|
695
|
+
* Cached and shared: call `.slice()` before writing.
|
|
696
|
+
* @returns Float64Array(n)
|
|
697
|
+
*/
|
|
698
|
+
selfLoopWeight(): F64 {
|
|
699
|
+
return this.cached("selfLoopWeight", () =>
|
|
700
|
+
computeSelfLoopWeight(this, {
|
|
701
|
+
selfLoopArcs: this.selfLoopArcs(),
|
|
702
|
+
selfLoopsPerNode: this.selfLoopsPerNode(),
|
|
703
|
+
}),
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Sum of weights over logical edges (each undirected edge once). Cached.
|
|
709
|
+
* @returns the total weight
|
|
710
|
+
*/
|
|
711
|
+
totalWeight(): number {
|
|
712
|
+
return this.cached("totalWeight", () => computeTotalWeight(this));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* The arcs a with colIdx[a] === row(a), found by binary search per row. Cached and shared: call
|
|
717
|
+
* `.slice()` before writing.
|
|
718
|
+
* @returns Uint32Array(selfLoopCount)
|
|
719
|
+
*/
|
|
720
|
+
selfLoopArcs(): U32 {
|
|
721
|
+
return this.cached("selfLoopArcs", () => this.materialiseSelfLoops().selfLoopArcs);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Self-loop arcs per node. Cached and shared: call `.slice()` before writing.
|
|
726
|
+
* @returns Uint32Array(n)
|
|
727
|
+
*/
|
|
728
|
+
selfLoopsPerNode(): U32 {
|
|
729
|
+
return this.cached("selfLoopsPerNode", () => this.materialiseSelfLoops().selfLoopsPerNode);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Point query: self-loop arcs at one node, O(log d).
|
|
734
|
+
* @param u - the node index
|
|
735
|
+
* @returns the count
|
|
736
|
+
*/
|
|
737
|
+
selfLoopsAt(u: number): number {
|
|
738
|
+
this.assertAttached();
|
|
739
|
+
return selfLoopsAtIn(this.rowPtr, this.colIdx, u);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* For every arc, the arc storing the opposite orientation of the same edge (a self-loop maps to
|
|
744
|
+
* itself); the lockstep walk of design section 6.4. Undirected only. Cached and shared: call
|
|
745
|
+
* `.slice()` before writing.
|
|
746
|
+
* @returns Uint32Array(arcCount); E_DIRECTED on a directed snapshot
|
|
747
|
+
*/
|
|
748
|
+
mate(): U32 {
|
|
749
|
+
return this.cached("mate", () => computeMate(this));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Nodes ordered by descending out-degree of the forward or reverse adjacency with the cuGraph tier
|
|
754
|
+
* boundaries. Both variants cached; on an undirected snapshot they are the same object.
|
|
755
|
+
* @param options - which adjacency's degree to order by
|
|
756
|
+
* @returns the permutation and its tier offsets
|
|
757
|
+
*/
|
|
758
|
+
degreeOrder(options?: DegreeOrderOptions): DegreeOrderView {
|
|
759
|
+
const of = assertOneOf("of", options?.of, ["forward", "reverse"] as const) ?? "forward";
|
|
760
|
+
if (of === "reverse" && this.directed) {
|
|
761
|
+
return this.cached("reverseDegreeOrder", () => computeDegreeOrder(this.reverse().rowPtr, this.nodeCount));
|
|
762
|
+
}
|
|
763
|
+
const forward = this.cached("degreeOrder", () => computeDegreeOrder(this.rowPtr, this.nodeCount));
|
|
764
|
+
if (!this.directed) {
|
|
765
|
+
this.state.views.reverseDegreeOrder = forward;
|
|
766
|
+
}
|
|
767
|
+
return forward;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Whether the arc set is closed under reversal with equal weights (forward row v equals reverse row
|
|
772
|
+
* v for every v); true without work when undirected. Cached.
|
|
773
|
+
* @returns true when symmetric
|
|
774
|
+
*/
|
|
775
|
+
isSymmetric(): boolean {
|
|
776
|
+
return this.cached("symmetric", () => (this.directed ? computeSymmetric(this, this.reverse()) : true));
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Compute a set of views eagerly (inside freeze() through FreezeOptions.prepare, or off the critical
|
|
781
|
+
* path).
|
|
782
|
+
* @param views - the views to materialise
|
|
783
|
+
* @returns this snapshot
|
|
784
|
+
*/
|
|
785
|
+
prepare(views: readonly ViewName[]): this {
|
|
786
|
+
for (const name of views) {
|
|
787
|
+
this.materialiseView(name);
|
|
788
|
+
}
|
|
789
|
+
return this;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Release every cached view (and every identity permutation materialised on demand) and every
|
|
794
|
+
* cached `gpuView()` f32 copy of an f64 column (design section 7.2); they are recomputed on
|
|
795
|
+
* demand. The checksum records of the dropped views are dropped with them.
|
|
796
|
+
*/
|
|
797
|
+
dropCaches(): void {
|
|
798
|
+
const { state } = this;
|
|
799
|
+
for (const name of VIEW_NAMES) {
|
|
800
|
+
state.views[name] = null;
|
|
801
|
+
}
|
|
802
|
+
state.checksums?.views.clear();
|
|
803
|
+
if (this.flags.arcToEdgeIsIdentity) {
|
|
804
|
+
state.arcToEdge = null;
|
|
805
|
+
state.edgeToArc = null;
|
|
806
|
+
state.checksums?.core.delete("arcToEdge");
|
|
807
|
+
state.checksums?.core.delete("edgeToArc");
|
|
808
|
+
}
|
|
809
|
+
for (const table of [this.nodes, this.edges, this.graph, ...this.extensions.values()]) {
|
|
810
|
+
for (const column of table) {
|
|
811
|
+
dropGpuViewCache(column);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Which views are resident.
|
|
818
|
+
* @returns the names of the cached views, in the fixed ViewName order
|
|
819
|
+
*/
|
|
820
|
+
cachedViews(): readonly ViewName[] {
|
|
821
|
+
if (this.detached) {
|
|
822
|
+
// design section 9.1: the view caches of a detached snapshot are dropped
|
|
823
|
+
this.dropCaches();
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
const { views } = this.state;
|
|
827
|
+
return VIEW_NAMES.filter((name) => views[name] !== null);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ---------------------------------------------------------------- derived graphs (7.3)
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Every directed edge becomes undirected; reciprocal pairs collapse to one edge keeping the lower
|
|
834
|
+
* index's row (keep-first). Returns `{ snapshot: this, null maps }` on an undirected snapshot.
|
|
835
|
+
* @param options - reciprocal filtering and the weight reducer
|
|
836
|
+
* @returns the derived graph
|
|
837
|
+
*/
|
|
838
|
+
toUndirected(options?: ToUndirectedOptions): DerivedGraph {
|
|
839
|
+
this.assertAttached();
|
|
840
|
+
if (!this.directed) {
|
|
841
|
+
return this.wrapDerived(identityDerived());
|
|
842
|
+
}
|
|
843
|
+
return this.wrapDerived(deriveToUndirected(this, this.edgeEndpoints(), options));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Orientation of every edge swapped: the reverse view's arrays become the core. Returns this on an
|
|
848
|
+
* undirected snapshot.
|
|
849
|
+
* @returns the derived graph (same node and edge spaces)
|
|
850
|
+
*/
|
|
851
|
+
transpose(): DerivedGraph {
|
|
852
|
+
this.assertAttached();
|
|
853
|
+
if (!this.directed) {
|
|
854
|
+
return this.wrapDerived(identityDerived());
|
|
855
|
+
}
|
|
856
|
+
return this.wrapDerived(deriveTranspose(this, this.reverse()));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* One edge per (u, v) group (parallels are adjacent by invariant I4), survivor = lowest index;
|
|
861
|
+
* flags.multigraph is false afterwards.
|
|
862
|
+
* @param options - reducers and self-loop policy
|
|
863
|
+
* @returns the derived graph
|
|
864
|
+
*/
|
|
865
|
+
simplified(options?: SimplifyOptions): DerivedGraph {
|
|
866
|
+
this.assertAttached();
|
|
867
|
+
return this.wrapDerived(deriveSimplified(this, this.edgeEndpoints(), options));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Every edge with source !== target.
|
|
872
|
+
* @returns the derived graph
|
|
873
|
+
*/
|
|
874
|
+
withoutSelfLoops(): DerivedGraph {
|
|
875
|
+
this.assertAttached();
|
|
876
|
+
return this.wrapDerived(deriveWithoutSelfLoops(this, this.edgeEndpoints()));
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Keep the logical edges whose mask bit is set.
|
|
881
|
+
* @param keep - packed bitmap over logical edges; E_MASK_LENGTH when shorter than ceil(edgeCount / 32) words
|
|
882
|
+
* @returns the derived graph
|
|
883
|
+
*/
|
|
884
|
+
filterEdges(keep: EdgeMask): DerivedGraph {
|
|
885
|
+
this.assertAttached();
|
|
886
|
+
return this.wrapDerived(deriveFilterEdges(this, this.edgeEndpoints(), keep));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* The subgraph induced by a node selection: an index list (order = new index order; E_INDEX_RANGE
|
|
891
|
+
* for an out-of-range or repeated index) or a packed mask (ascending order; E_MASK_LENGTH when
|
|
892
|
+
* short). Edges with both endpoints kept.
|
|
893
|
+
* @param selection - the node indices or a mask
|
|
894
|
+
* @returns the derived graph
|
|
895
|
+
*/
|
|
896
|
+
inducedSubgraph(selection: U32 | { readonly mask: NodeMask }): DerivedGraph {
|
|
897
|
+
this.assertAttached();
|
|
898
|
+
return this.wrapDerived(deriveInducedSubgraph(this, this.edgeEndpoints(), selection));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Contract the nodes of each partition block into one node (design section 7.3).
|
|
903
|
+
* @param partition - one label per node; E_PARTITION for a wrong length or an INVALID_INDEX label
|
|
904
|
+
* @param options - weight reducer, self-loop and parallel policies, column reducers
|
|
905
|
+
* @returns the derived graph with blockSizes and an identity id map
|
|
906
|
+
*/
|
|
907
|
+
contract(partition: U32, options?: ContractOptions): DerivedGraph {
|
|
908
|
+
this.assertAttached();
|
|
909
|
+
return this.wrapDerived(deriveContract(this, this.edgeEndpoints(), partition, options));
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Permute the node space: perm[newIndex] = oldIndex; the id map follows; edge order is preserved.
|
|
914
|
+
* @param perm - a permutation of 0..n-1; E_INVALID_PERMUTATION otherwise
|
|
915
|
+
* @returns the derived graph
|
|
916
|
+
*/
|
|
917
|
+
relabel(perm: U32): DerivedGraph {
|
|
918
|
+
this.assertAttached();
|
|
919
|
+
return this.wrapDerived(deriveRelabel(this, this.edgeEndpoints(), perm));
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* A new snapshot object sharing the core, the id map and the serial with a CLONED column set plus
|
|
924
|
+
* the given columns (the only operation that clones the column set).
|
|
925
|
+
* @param nodes - node columns to add, keyed by name
|
|
926
|
+
* @param edges - edge columns to add, keyed by name
|
|
927
|
+
* @returns the new snapshot
|
|
928
|
+
*/
|
|
929
|
+
withColumns(
|
|
930
|
+
nodes?: Readonly<Record<string, TypedArrayData | ColumnInput>>,
|
|
931
|
+
edges?: Readonly<Record<string, TypedArrayData | ColumnInput>>,
|
|
932
|
+
): GraphSnapshot {
|
|
933
|
+
this.assertAttached();
|
|
934
|
+
const identity = this.flags.arcToEdgeIsIdentity;
|
|
935
|
+
const parts = withColumnsParts(
|
|
936
|
+
this,
|
|
937
|
+
nodes,
|
|
938
|
+
edges,
|
|
939
|
+
identity ? null : this.arcToEdge,
|
|
940
|
+
identity ? null : this.edgeToArc,
|
|
941
|
+
);
|
|
942
|
+
return new GraphSnapshot({ ...parts, checksum: this.state.checksums !== null });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ---------------------------------------------------------------- memory, transfer, checks (9, 11)
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Resident bytes of the core (an identity permutation counts zero even when materialised), plus
|
|
949
|
+
* the side structures selected by the options.
|
|
950
|
+
* @param options - which side structures to include
|
|
951
|
+
* @returns the byte count
|
|
952
|
+
*/
|
|
953
|
+
byteLength(options: ByteLengthOptions = {}): number {
|
|
954
|
+
let bytes = this.rowPtr.byteLength + this.colIdx.byteLength;
|
|
955
|
+
if (this.weights !== null) {
|
|
956
|
+
bytes += this.weights.byteLength;
|
|
957
|
+
}
|
|
958
|
+
if (!this.flags.arcToEdgeIsIdentity) {
|
|
959
|
+
bytes += this.arcToEdge.byteLength + this.edgeToArc.byteLength;
|
|
960
|
+
}
|
|
961
|
+
if (options.views === true) {
|
|
962
|
+
const { views } = this.state;
|
|
963
|
+
for (const name of VIEW_NAMES) {
|
|
964
|
+
const value = views[name];
|
|
965
|
+
if (value !== null && !(name === "reverseDegreeOrder" && value === views.degreeOrder)) {
|
|
966
|
+
bytes += viewByteLength(this, name, value);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (options.columns === true) {
|
|
971
|
+
for (const table of [this.nodes, this.edges, this.graph, ...this.extensions.values()]) {
|
|
972
|
+
for (const column of table) {
|
|
973
|
+
bytes += column.byteLength;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (options.ids === true) {
|
|
978
|
+
bytes += this.ids.byteLength();
|
|
979
|
+
}
|
|
980
|
+
return bytes;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* A 64-bit content hash of the core arrays (two 32-bit FNV-1a lanes) as 16 hex characters,
|
|
985
|
+
* computed lazily and cached (design section 9.3).
|
|
986
|
+
* @returns the hash
|
|
987
|
+
*/
|
|
988
|
+
contentHash(): string {
|
|
989
|
+
this.assertAttached();
|
|
990
|
+
const { state } = this;
|
|
991
|
+
state.contentHash ??= contentHashOf(this);
|
|
992
|
+
return state.contentHash;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* The distinct, exclusively owned backing buffers: the postMessage transfer list (design section
|
|
997
|
+
* 9.1). Buffers shared with another holder (a withColumns() sibling, a derived graph sharing the
|
|
998
|
+
* node table) are excluded.
|
|
999
|
+
* @returns the buffers
|
|
1000
|
+
*/
|
|
1001
|
+
transferables(): ArrayBuffer[] {
|
|
1002
|
+
this.assertAttached();
|
|
1003
|
+
return transferables(this);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* The plain-object wire form (design section 9.1): a JSON-serialisable manifest plus the backing
|
|
1008
|
+
* buffers. With `transfer: true` only exclusively owned buffers are listed as transferables and
|
|
1009
|
+
* shared ones are copied.
|
|
1010
|
+
* @param options - transfer mode, views and columns to include
|
|
1011
|
+
* @returns the wire snapshot
|
|
1012
|
+
*/
|
|
1013
|
+
toWire(options?: ToWireOptions): WireSnapshot {
|
|
1014
|
+
this.assertAttached();
|
|
1015
|
+
return toWire(this, options);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* The GSNP byte container as one contiguous buffer (design section 9.2).
|
|
1020
|
+
* @param options - views to include
|
|
1021
|
+
* @returns the container bytes
|
|
1022
|
+
*/
|
|
1023
|
+
toBytes(options?: ToBytesOptions): U8 {
|
|
1024
|
+
this.assertAttached();
|
|
1025
|
+
return toBytes(this, options);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* The GSNP container as a sequence of chunks (design section 9.2): the header plus manifest,
|
|
1030
|
+
* then one chunk per non-empty segment.
|
|
1031
|
+
* @param options - views to include
|
|
1032
|
+
* @returns the chunks
|
|
1033
|
+
*/
|
|
1034
|
+
toByteChunks(options?: ToBytesOptions): Iterable<U8> {
|
|
1035
|
+
this.assertAttached();
|
|
1036
|
+
return toByteChunks(this, options);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Check the invariants (design section 11.4): the recorded checksums first when `checksum` is set
|
|
1041
|
+
* (a mutated frozen array is the most useful diagnosis, details.reason "checksum"; "no-checksum"
|
|
1042
|
+
* when none were recorded), then "structure" or "full" (default) per the level table of design
|
|
1043
|
+
* section 9.5. Throws E_INVALID_SNAPSHOT with details.invariant and the location on the first
|
|
1044
|
+
* violation.
|
|
1045
|
+
* @param options - level and checksum comparison
|
|
1046
|
+
*/
|
|
1047
|
+
validate(options: ValidateOptions = {}): void {
|
|
1048
|
+
this.assertAttached();
|
|
1049
|
+
const level = assertOneOf("level", options.level, ["structure", "full"] as const) ?? "full";
|
|
1050
|
+
if (typeof options.checksum !== "boolean" && options.checksum !== undefined) {
|
|
1051
|
+
throw new GraphFormatError("E_UNSUPPORTED", "validate option checksum must be a boolean", {
|
|
1052
|
+
field: "checksum",
|
|
1053
|
+
found: options.checksum,
|
|
1054
|
+
reason: "unsupported option",
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
// a column or id store transferred away underneath the snapshot (a holder outside the owner
|
|
1058
|
+
// count, design section 9.1) is E_DETACHED, not a length-rule violation
|
|
1059
|
+
if (idMapDetached(this.ids)) {
|
|
1060
|
+
throw new GraphFormatError("E_DETACHED", "the id map's storage was transferred away", {
|
|
1061
|
+
serial: this.serial,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
for (const table of [this.nodes, this.edges, this.graph, ...this.extensions.values()]) {
|
|
1065
|
+
for (const column of table) {
|
|
1066
|
+
if (isColumnDetached(column)) {
|
|
1067
|
+
throw new GraphFormatError("E_DETACHED", `column "${column.meta.name}" was transferred away`, {
|
|
1068
|
+
column: column.meta.name,
|
|
1069
|
+
serial: this.serial,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (options.checksum === true) {
|
|
1075
|
+
this.verifyChecksums();
|
|
1076
|
+
}
|
|
1077
|
+
if (level === "structure") {
|
|
1078
|
+
validateStructure(this);
|
|
1079
|
+
} else {
|
|
1080
|
+
validateFull(this);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// ---------------------------------------------------------------- private helpers
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Throw E_DETACHED when the core was transferred away.
|
|
1088
|
+
*/
|
|
1089
|
+
private assertAttached(): void {
|
|
1090
|
+
if (this.rowPtr.length === 0) {
|
|
1091
|
+
throw new GraphFormatError("E_DETACHED", "the snapshot's core buffer was transferred away", {
|
|
1092
|
+
serial: this.serial,
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Return the cached value of a view, computing and recording it on first use.
|
|
1099
|
+
* @param name - the view name
|
|
1100
|
+
* @param compute - the computation
|
|
1101
|
+
* @returns the cached value
|
|
1102
|
+
*/
|
|
1103
|
+
private cached<K extends ViewName>(name: K, compute: () => NonNullable<ViewCache[K]>): NonNullable<ViewCache[K]> {
|
|
1104
|
+
this.assertAttached();
|
|
1105
|
+
const { views } = this.state;
|
|
1106
|
+
const existing = views[name];
|
|
1107
|
+
if (existing !== null) {
|
|
1108
|
+
return existing;
|
|
1109
|
+
}
|
|
1110
|
+
const value = compute();
|
|
1111
|
+
views[name] = value;
|
|
1112
|
+
this.recordView(name, value);
|
|
1113
|
+
return value;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Compute both self-loop views in one pass and seed the slot the caller did not ask for.
|
|
1118
|
+
* @returns the two views
|
|
1119
|
+
*/
|
|
1120
|
+
private materialiseSelfLoops(): { selfLoopArcs: U32; selfLoopsPerNode: U32 } {
|
|
1121
|
+
const loops = computeSelfLoops(this);
|
|
1122
|
+
const { views } = this.state;
|
|
1123
|
+
if (views.selfLoopArcs === null) {
|
|
1124
|
+
views.selfLoopArcs = loops.selfLoopArcs;
|
|
1125
|
+
this.recordView("selfLoopArcs", loops.selfLoopArcs);
|
|
1126
|
+
}
|
|
1127
|
+
if (views.selfLoopsPerNode === null) {
|
|
1128
|
+
views.selfLoopsPerNode = loops.selfLoopsPerNode;
|
|
1129
|
+
this.recordView("selfLoopsPerNode", loops.selfLoopsPerNode);
|
|
1130
|
+
}
|
|
1131
|
+
return { selfLoopArcs: views.selfLoopArcs, selfLoopsPerNode: views.selfLoopsPerNode };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/**
|
|
1135
|
+
* Record the digests of a freshly materialised view when checksums are on.
|
|
1136
|
+
* @param name - the view name
|
|
1137
|
+
* @param value - the view
|
|
1138
|
+
*/
|
|
1139
|
+
private recordView(name: ViewName, value: unknown): void {
|
|
1140
|
+
recordViewDigests(this, this.state, name, value);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Compare every recorded checksum with the current bytes (design section 5.8).
|
|
1145
|
+
*/
|
|
1146
|
+
private verifyChecksums(): void {
|
|
1147
|
+
const records = this.state.checksums;
|
|
1148
|
+
if (records === null) {
|
|
1149
|
+
throw new GraphFormatError("E_INVALID_SNAPSHOT", "no checksums were recorded for this snapshot", {
|
|
1150
|
+
reason: "no-checksum",
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
const mismatch = (what: string, details: Readonly<Record<string, unknown>>): GraphFormatError =>
|
|
1154
|
+
new GraphFormatError("E_INVALID_SNAPSHOT", `checksum mismatch: ${what} was modified after freeze`, {
|
|
1155
|
+
reason: "checksum",
|
|
1156
|
+
...details,
|
|
1157
|
+
});
|
|
1158
|
+
const { state } = this;
|
|
1159
|
+
const core: readonly [CoreArrayName, ArrayBufferView | null][] = [
|
|
1160
|
+
["rowPtr", this.rowPtr],
|
|
1161
|
+
["colIdx", this.colIdx],
|
|
1162
|
+
["weights", this.weights],
|
|
1163
|
+
["arcToEdge", state.arcToEdge],
|
|
1164
|
+
["edgeToArc", state.edgeToArc],
|
|
1165
|
+
];
|
|
1166
|
+
for (const [name, array] of core) {
|
|
1167
|
+
const recorded = records.core.get(name);
|
|
1168
|
+
if (recorded !== undefined && array !== null && hashTypedArray(array) !== recorded) {
|
|
1169
|
+
throw mismatch(`core array ${name}`, { array: name });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const tables: readonly [string, AttributeTable][] = [
|
|
1173
|
+
["nodes", this.nodes],
|
|
1174
|
+
["edges", this.edges],
|
|
1175
|
+
["graph", this.graph],
|
|
1176
|
+
...[...this.extensions].map(([name, table]): [string, AttributeTable] => [`extensions[${name}]`, table]),
|
|
1177
|
+
];
|
|
1178
|
+
for (const [tableName, table] of tables) {
|
|
1179
|
+
for (const column of table) {
|
|
1180
|
+
const recorded = records.columns.get(column);
|
|
1181
|
+
if (recorded !== undefined && hashColumn(column) !== recorded) {
|
|
1182
|
+
throw mismatch(`column "${column.meta.name}" of ${tableName}`, {
|
|
1183
|
+
table: tableName,
|
|
1184
|
+
column: column.meta.name,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
for (const name of VIEW_NAMES) {
|
|
1190
|
+
const value = state.views[name];
|
|
1191
|
+
if (value === null) {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
for (const [member, array] of Object.entries(viewArrays(this, name, value))) {
|
|
1195
|
+
const recorded = records.views.get(`${name}.${member}`);
|
|
1196
|
+
if (recorded !== undefined && hashTypedArray(array) !== recorded) {
|
|
1197
|
+
throw mismatch(`view ${name}.${member}`, { view: name, member });
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* The edge endpoints in declared orientation for the derived-graph functions: the cached edge list
|
|
1205
|
+
* when resident, else a fresh one that is NOT cached (derived graphs must not grow the source's
|
|
1206
|
+
* resident size, design section 7.3).
|
|
1207
|
+
* @returns an edge list view
|
|
1208
|
+
*/
|
|
1209
|
+
private edgeEndpoints(): EdgeListView {
|
|
1210
|
+
return this.state.views.edgeList ?? computeEdgeList(this);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Wrap derived parts into a DerivedGraph, creating the new snapshot (with this snapshot's checksum
|
|
1215
|
+
* setting) or returning this for the identity cases.
|
|
1216
|
+
* @param derived - the derived parts
|
|
1217
|
+
* @returns the derived graph
|
|
1218
|
+
*/
|
|
1219
|
+
private wrapDerived(derived: DerivedParts): DerivedGraph {
|
|
1220
|
+
const snapshot =
|
|
1221
|
+
derived.parts === null
|
|
1222
|
+
? this
|
|
1223
|
+
: new GraphSnapshot({ ...derived.parts, checksum: this.state.checksums !== null });
|
|
1224
|
+
return {
|
|
1225
|
+
snapshot,
|
|
1226
|
+
nodeOrigin: derived.nodeOrigin,
|
|
1227
|
+
edgeOrigin: derived.edgeOrigin,
|
|
1228
|
+
nodeRemap: derived.nodeRemap,
|
|
1229
|
+
edgeRemap: derived.edgeRemap,
|
|
1230
|
+
blockSizes: derived.blockSizes,
|
|
1231
|
+
report: derived.report,
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Materialise one named view (the prepare() dispatch).
|
|
1237
|
+
* @param name - the view name
|
|
1238
|
+
*/
|
|
1239
|
+
private materialiseView(name: ViewName): void {
|
|
1240
|
+
switch (name) {
|
|
1241
|
+
case "reverse":
|
|
1242
|
+
this.reverse();
|
|
1243
|
+
break;
|
|
1244
|
+
case "coo":
|
|
1245
|
+
this.coo();
|
|
1246
|
+
break;
|
|
1247
|
+
case "edgeList":
|
|
1248
|
+
this.edgeList();
|
|
1249
|
+
break;
|
|
1250
|
+
case "outDegree":
|
|
1251
|
+
this.outDegree();
|
|
1252
|
+
break;
|
|
1253
|
+
case "inDegree":
|
|
1254
|
+
this.inDegree();
|
|
1255
|
+
break;
|
|
1256
|
+
case "degree":
|
|
1257
|
+
this.degree();
|
|
1258
|
+
break;
|
|
1259
|
+
case "weightedOutDegree":
|
|
1260
|
+
this.weightedOutDegree();
|
|
1261
|
+
break;
|
|
1262
|
+
case "weightedInDegree":
|
|
1263
|
+
this.weightedInDegree();
|
|
1264
|
+
break;
|
|
1265
|
+
case "weightedDegree":
|
|
1266
|
+
this.weightedDegree();
|
|
1267
|
+
break;
|
|
1268
|
+
case "selfLoopWeight":
|
|
1269
|
+
this.selfLoopWeight();
|
|
1270
|
+
break;
|
|
1271
|
+
case "totalWeight":
|
|
1272
|
+
this.totalWeight();
|
|
1273
|
+
break;
|
|
1274
|
+
case "selfLoopArcs":
|
|
1275
|
+
this.selfLoopArcs();
|
|
1276
|
+
break;
|
|
1277
|
+
case "selfLoopsPerNode":
|
|
1278
|
+
this.selfLoopsPerNode();
|
|
1279
|
+
break;
|
|
1280
|
+
case "mate":
|
|
1281
|
+
this.mate();
|
|
1282
|
+
break;
|
|
1283
|
+
case "degreeOrder":
|
|
1284
|
+
this.degreeOrder();
|
|
1285
|
+
break;
|
|
1286
|
+
case "reverseDegreeOrder":
|
|
1287
|
+
this.degreeOrder({ of: "reverse" });
|
|
1288
|
+
break;
|
|
1289
|
+
case "symmetric":
|
|
1290
|
+
this.isSymmetric();
|
|
1291
|
+
break;
|
|
1292
|
+
default: {
|
|
1293
|
+
const unknown: never = name;
|
|
1294
|
+
throw new GraphFormatError("E_UNSUPPORTED", `unknown view ${String(unknown)}`, { view: unknown });
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Record this snapshot as a holder of every buffer, table and id map it keeps (design section 9.1):
|
|
1302
|
+
* a second snapshot claiming the same storage (a withColumns() sibling, a derived graph sharing the
|
|
1303
|
+
* node table) marks it shared, so a transfer copies it instead of emptying the sibling. Each distinct
|
|
1304
|
+
* buffer is claimed once per snapshot (the arena backs several core arrays).
|
|
1305
|
+
* @param parts - the parts the snapshot was built from
|
|
1306
|
+
*/
|
|
1307
|
+
function claimStorage(parts: SnapshotParts): void {
|
|
1308
|
+
const buffers = new Set<ArrayBuffer>([parts.rowPtr.buffer, parts.colIdx.buffer]);
|
|
1309
|
+
if (parts.weights !== null) {
|
|
1310
|
+
buffers.add(parts.weights.buffer);
|
|
1311
|
+
}
|
|
1312
|
+
if (parts.arcToEdge !== null) {
|
|
1313
|
+
buffers.add(parts.arcToEdge.buffer);
|
|
1314
|
+
}
|
|
1315
|
+
if (parts.edgeToArc !== null) {
|
|
1316
|
+
buffers.add(parts.edgeToArc.buffer);
|
|
1317
|
+
}
|
|
1318
|
+
for (const buffer of idMapBuffers(parts.ids)) {
|
|
1319
|
+
buffers.add(buffer);
|
|
1320
|
+
}
|
|
1321
|
+
for (const buffer of buffers) {
|
|
1322
|
+
claimHolder(buffer);
|
|
1323
|
+
}
|
|
1324
|
+
claimHolder(parts.ids);
|
|
1325
|
+
claimHolder(parts.nodes);
|
|
1326
|
+
claimHolder(parts.edges);
|
|
1327
|
+
claimHolder(parts.graph);
|
|
1328
|
+
for (const table of parts.extensions.values()) {
|
|
1329
|
+
claimHolder(table);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ============================================================ factory and helpers
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Create a snapshot from prepared parts (design section 6.3 step 12): the constructor freezes the
|
|
1337
|
+
* object, records checksums when `parts.checksum` is set and allocates the per-instance view cache.
|
|
1338
|
+
* @param parts - the parts; I1-I13 established by the producer, O(1) length rules checked here
|
|
1339
|
+
* @returns the frozen snapshot
|
|
1340
|
+
*/
|
|
1341
|
+
export function createSnapshot(parts: SnapshotParts): GraphSnapshot {
|
|
1342
|
+
return new GraphSnapshot(parts);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Structural check for a snapshot (design section 7.5): the SNAPSHOT_BRAND property plus the
|
|
1347
|
+
* formatVersion, never instanceof, so a duplicated package copy still interoperates.
|
|
1348
|
+
* @param x - any value
|
|
1349
|
+
* @returns true when x is a GraphSnapshot of this format version
|
|
1350
|
+
*/
|
|
1351
|
+
export function isGraphSnapshot(x: unknown): x is GraphSnapshot {
|
|
1352
|
+
if (typeof x !== "object" || x === null) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
const candidate = x as { [SNAPSHOT_BRAND]?: unknown; formatVersion?: unknown };
|
|
1356
|
+
return candidate[SNAPSHOT_BRAND] === true && candidate.formatVersion === FORMAT_VERSION;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Whether two typed arrays hold the same elements.
|
|
1361
|
+
* @param a - the first array
|
|
1362
|
+
* @param b - the second array
|
|
1363
|
+
* @returns true when equal element by element
|
|
1364
|
+
*/
|
|
1365
|
+
function sameElements(a: ArrayLike<number>, b: ArrayLike<number>): boolean {
|
|
1366
|
+
if (a.length !== b.length) {
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
for (let i = 0; i < a.length; i++) {
|
|
1370
|
+
if (a[i] !== b[i]) {
|
|
1371
|
+
return false;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return true;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Compare the core arrays and id maps of two snapshots element by element (design section 7.5):
|
|
1379
|
+
* directedness, counts, rowPtr, colIdx, weights (both absent or equal), arcToEdge / edgeToArc
|
|
1380
|
+
* (identity compared without materialising) and every id (SameValueZero).
|
|
1381
|
+
* @param a - the first snapshot
|
|
1382
|
+
* @param b - the second snapshot
|
|
1383
|
+
* @returns true when the topologies and id maps are equal
|
|
1384
|
+
*/
|
|
1385
|
+
export function equalsTopology(a: GraphSnapshot, b: GraphSnapshot): boolean {
|
|
1386
|
+
if (
|
|
1387
|
+
a.directed !== b.directed ||
|
|
1388
|
+
a.nodeCount !== b.nodeCount ||
|
|
1389
|
+
a.edgeCount !== b.edgeCount ||
|
|
1390
|
+
a.arcCount !== b.arcCount ||
|
|
1391
|
+
a.selfLoopCount !== b.selfLoopCount
|
|
1392
|
+
) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
if (!sameElements(a.rowPtr, b.rowPtr) || !sameElements(a.colIdx, b.colIdx)) {
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
if ((a.weights === null) !== (b.weights === null)) {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
if (a.weights !== null && b.weights !== null && !sameElements(a.weights, b.weights)) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
const identityA = a.flags.arcToEdgeIsIdentity;
|
|
1405
|
+
const identityB = b.flags.arcToEdgeIsIdentity;
|
|
1406
|
+
if (!identityA || !identityB) {
|
|
1407
|
+
if (!sameElements(a.arcToEdge, b.arcToEdge) || !sameElements(a.edgeToArc, b.edgeToArc)) {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (a.ids.size !== b.ids.size) {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
for (let i = 0; i < a.nodeCount; i++) {
|
|
1415
|
+
const idA = a.ids.idOf(i);
|
|
1416
|
+
const idB = b.ids.idOf(i);
|
|
1417
|
+
if (
|
|
1418
|
+
idA !== idB &&
|
|
1419
|
+
!(typeof idA === "number" && typeof idB === "number" && Number.isNaN(idA) && Number.isNaN(idB))
|
|
1420
|
+
) {
|
|
1421
|
+
return false;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Record the digests of a materialised view in the checksum records, when the snapshot keeps any.
|
|
1429
|
+
* @param snapshot - the snapshot
|
|
1430
|
+
* @param state - its state record
|
|
1431
|
+
* @param name - the view name
|
|
1432
|
+
* @param value - the view
|
|
1433
|
+
*/
|
|
1434
|
+
function recordViewDigests(snapshot: GraphSnapshot, state: SnapshotState, name: ViewName, value: unknown): void {
|
|
1435
|
+
const records = state.checksums;
|
|
1436
|
+
if (records === null) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
for (const [member, array] of Object.entries(viewArrays(snapshot, name, value))) {
|
|
1440
|
+
records.views.set(`${name}.${member}`, hashTypedArray(array));
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Install a view value the wire carried (`includeViews`, design section 9.1) into the snapshot's
|
|
1446
|
+
* cache, so the receiver does not recompute it. The caller has already checked that the value has
|
|
1447
|
+
* the shape of the view; the slot is only filled when it is still empty.
|
|
1448
|
+
* @param snapshot - the snapshot
|
|
1449
|
+
* @param name - the view name
|
|
1450
|
+
* @param value - the view value
|
|
1451
|
+
* @returns true when the view was installed, false when the slot was already resident
|
|
1452
|
+
*/
|
|
1453
|
+
export function seedView<K extends ViewName>(
|
|
1454
|
+
snapshot: GraphSnapshot,
|
|
1455
|
+
name: K,
|
|
1456
|
+
value: NonNullable<ViewCache[K]>,
|
|
1457
|
+
): boolean {
|
|
1458
|
+
const state = STATES.get(snapshot);
|
|
1459
|
+
if (state === undefined || state.views[name] !== null) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
state.views[name] = value;
|
|
1463
|
+
recordViewDigests(snapshot, state, name, value);
|
|
1464
|
+
return true;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* The cached value of a view without computing it (for tests and for the wire module's
|
|
1469
|
+
* `includeViews`).
|
|
1470
|
+
* @param snapshot - the snapshot
|
|
1471
|
+
* @param name - the view name
|
|
1472
|
+
* @returns the cached value, or null when not resident
|
|
1473
|
+
*/
|
|
1474
|
+
export function peekView<K extends ViewName>(snapshot: GraphSnapshot, name: K): ViewCache[K] {
|
|
1475
|
+
const state = STATES.get(snapshot);
|
|
1476
|
+
if (state === undefined) {
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
return state.views[name];
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Whether the snapshot's permutation arrays are held (materialised) rather than lazy, for tests of
|
|
1484
|
+
* the identity rule and for the wire module (which never serialises an identity permutation).
|
|
1485
|
+
* @param snapshot - the snapshot
|
|
1486
|
+
* @returns true when arcToEdge has been materialised or was supplied
|
|
1487
|
+
*/
|
|
1488
|
+
export function permutationMaterialised(snapshot: GraphSnapshot): boolean {
|
|
1489
|
+
const state = STATES.get(snapshot);
|
|
1490
|
+
return state !== undefined && state.arcToEdge !== null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Whether checksums were recorded at construction (design section 5.8).
|
|
1495
|
+
* @param snapshot - the snapshot
|
|
1496
|
+
* @returns true when validate({ checksum: true }) can compare
|
|
1497
|
+
*/
|
|
1498
|
+
export function hasChecksums(snapshot: GraphSnapshot): boolean {
|
|
1499
|
+
const state = STATES.get(snapshot);
|
|
1500
|
+
return state !== undefined && state.checksums !== null;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// ============================================================ arena helpers (10.3)
|