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