@graphty/webgpu-graph-algorithms 0.5.0 → 0.6.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/README.md +104 -52
- package/dist/browser.js +1 -1
- package/dist/chunks/{context-CRbw2Wyo.js → context-BXqgCifx.js} +225 -33
- package/dist/chunks/context-BXqgCifx.js.map +1 -0
- package/dist/node.js +1 -1
- package/dist/src/accelerator.d.ts +12 -10
- package/dist/src/accelerator.d.ts.map +1 -1
- package/dist/src/accelerator.js +32 -10
- package/dist/src/accelerator.js.map +1 -1
- package/dist/src/algorithms/components.d.ts.map +1 -1
- package/dist/src/algorithms/components.js +12 -13
- package/dist/src/algorithms/components.js.map +1 -1
- package/dist/src/algorithms/degree.d.ts +6 -8
- package/dist/src/algorithms/degree.d.ts.map +1 -1
- package/dist/src/algorithms/degree.js +58 -35
- package/dist/src/algorithms/degree.js.map +1 -1
- package/dist/src/algorithms/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/pagerank.js +16 -14
- package/dist/src/algorithms/pagerank.js.map +1 -1
- package/dist/src/algorithms/power-iteration.d.ts +2 -2
- package/dist/src/algorithms/power-iteration.d.ts.map +1 -1
- package/dist/src/algorithms/power-iteration.js +17 -14
- package/dist/src/algorithms/power-iteration.js.map +1 -1
- package/dist/src/constants.d.ts +85 -8
- package/dist/src/constants.d.ts.map +1 -1
- package/dist/src/constants.js +85 -8
- package/dist/src/constants.js.map +1 -1
- package/dist/src/errors.d.ts +3 -2
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +2 -1
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +10 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +14 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/kernel/dispatch.d.ts +8 -3
- package/dist/src/kernel/dispatch.d.ts.map +1 -1
- package/dist/src/kernel/dispatch.js +18 -7
- package/dist/src/kernel/dispatch.js.map +1 -1
- package/dist/src/kernel/kernel.d.ts +30 -1
- package/dist/src/kernel/kernel.d.ts.map +1 -1
- package/dist/src/kernel/kernel.js +49 -5
- package/dist/src/kernel/kernel.js.map +1 -1
- package/dist/src/kernel/prelude.d.ts.map +1 -1
- package/dist/src/kernel/prelude.js +9 -1
- package/dist/src/kernel/prelude.js.map +1 -1
- package/dist/src/kernel/profiler.d.ts +15 -3
- package/dist/src/kernel/profiler.d.ts.map +1 -1
- package/dist/src/kernel/profiler.js +27 -4
- package/dist/src/kernel/profiler.js.map +1 -1
- package/dist/src/kernels.d.ts +18 -8
- package/dist/src/kernels.d.ts.map +1 -1
- package/dist/src/kernels.js +345 -22
- package/dist/src/kernels.js.map +1 -1
- package/dist/src/layouts/calibrate.d.ts +51 -0
- package/dist/src/layouts/calibrate.d.ts.map +1 -0
- package/dist/src/layouts/calibrate.js +172 -0
- package/dist/src/layouts/calibrate.js.map +1 -0
- package/dist/src/layouts/force-simulation.d.ts +42 -5
- package/dist/src/layouts/force-simulation.d.ts.map +1 -1
- package/dist/src/layouts/force-simulation.js +84 -22
- package/dist/src/layouts/force-simulation.js.map +1 -1
- package/dist/src/layouts/forceatlas2.d.ts +107 -38
- package/dist/src/layouts/forceatlas2.d.ts.map +1 -1
- package/dist/src/layouts/forceatlas2.js +297 -290
- package/dist/src/layouts/forceatlas2.js.map +1 -1
- package/dist/src/layouts/fruchterman-reingold.d.ts +241 -0
- package/dist/src/layouts/fruchterman-reingold.d.ts.map +1 -0
- package/dist/src/layouts/fruchterman-reingold.js +739 -0
- package/dist/src/layouts/fruchterman-reingold.js.map +1 -0
- package/dist/src/layouts/model-common.d.ts +140 -0
- package/dist/src/layouts/model-common.d.ts.map +1 -0
- package/dist/src/layouts/model-common.js +269 -0
- package/dist/src/layouts/model-common.js.map +1 -0
- package/dist/src/layouts/repulsion-grid.d.ts +152 -0
- package/dist/src/layouts/repulsion-grid.d.ts.map +1 -0
- package/dist/src/layouts/repulsion-grid.js +318 -0
- package/dist/src/layouts/repulsion-grid.js.map +1 -0
- package/dist/src/layouts/spring-electrical.d.ts +224 -0
- package/dist/src/layouts/spring-electrical.d.ts.map +1 -0
- package/dist/src/layouts/spring-electrical.js +665 -0
- package/dist/src/layouts/spring-electrical.js.map +1 -0
- package/dist/src/memory/residency.d.ts +6 -2
- package/dist/src/memory/residency.d.ts.map +1 -1
- package/dist/src/memory/residency.js +84 -14
- package/dist/src/memory/residency.js.map +1 -1
- package/dist/src/primitives/core-shape.d.ts +38 -2
- package/dist/src/primitives/core-shape.d.ts.map +1 -1
- package/dist/src/primitives/core-shape.js +71 -3
- package/dist/src/primitives/core-shape.js.map +1 -1
- package/dist/src/primitives/grid-pyramid.d.ts +71 -0
- package/dist/src/primitives/grid-pyramid.d.ts.map +1 -0
- package/dist/src/primitives/grid-pyramid.js +143 -0
- package/dist/src/primitives/grid-pyramid.js.map +1 -0
- package/dist/src/primitives/grid.d.ts +118 -0
- package/dist/src/primitives/grid.d.ts.map +1 -0
- package/dist/src/primitives/grid.js +225 -0
- package/dist/src/primitives/grid.js.map +1 -0
- package/dist/src/primitives/histogram.d.ts +67 -0
- package/dist/src/primitives/histogram.d.ts.map +1 -0
- package/dist/src/primitives/histogram.js +190 -0
- package/dist/src/primitives/histogram.js.map +1 -0
- package/dist/src/primitives/radix-sort.d.ts +75 -0
- package/dist/src/primitives/radix-sort.d.ts.map +1 -0
- package/dist/src/primitives/radix-sort.js +168 -0
- package/dist/src/primitives/radix-sort.js.map +1 -0
- package/dist/src/primitives/scan.d.ts +44 -0
- package/dist/src/primitives/scan.d.ts.map +1 -0
- package/dist/src/primitives/scan.js +151 -0
- package/dist/src/primitives/scan.js.map +1 -0
- package/dist/src/primitives/segmented-reduce.d.ts +25 -17
- package/dist/src/primitives/segmented-reduce.d.ts.map +1 -1
- package/dist/src/primitives/segmented-reduce.js +166 -47
- package/dist/src/primitives/segmented-reduce.js.map +1 -1
- package/dist/src/primitives/spmv.d.ts +18 -14
- package/dist/src/primitives/spmv.d.ts.map +1 -1
- package/dist/src/primitives/spmv.js +94 -58
- package/dist/src/primitives/spmv.js.map +1 -1
- package/dist/src/primitives/verify.d.ts +49 -0
- package/dist/src/primitives/verify.d.ts.map +1 -0
- package/dist/src/primitives/verify.js +229 -0
- package/dist/src/primitives/verify.js.map +1 -0
- package/dist/src/types/accelerator.d.ts +7 -3
- package/dist/src/types/accelerator.d.ts.map +1 -1
- package/dist/src/types/context.d.ts +53 -0
- package/dist/src/types/context.d.ts.map +1 -1
- package/dist/src/types/layout.d.ts +52 -0
- package/dist/src/types/layout.d.ts.map +1 -1
- package/dist/src/types/options.d.ts +43 -1
- package/dist/src/types/options.d.ts.map +1 -1
- package/dist/src/wgsl/counting-scatter.wgsl.d.ts +8 -0
- package/dist/src/wgsl/counting-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/counting-scatter.wgsl.js +17 -0
- package/dist/src/wgsl/counting-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts +23 -8
- package/dist/src/wgsl/fa2-attraction.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-attraction.wgsl.js +100 -17
- package/dist/src/wgsl/fa2-attraction.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts +7 -2
- package/dist/src/wgsl/fa2-integrate.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-integrate.wgsl.js +28 -2
- package/dist/src/wgsl/fa2-integrate.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts +4 -2
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js +14 -5
- package/dist/src/wgsl/fa2-repulsion-exact.wgsl.js.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts +12 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js +54 -0
- package/dist/src/wgsl/fa2-stats-finalize.wgsl.js.map +1 -1
- package/dist/src/wgsl/grid-cell-key.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.js +30 -0
- package/dist/src/wgsl/grid-cell-key.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js +29 -0
- package/dist/src/wgsl/grid-centroid-hub.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-centroid.wgsl.d.ts +8 -0
- package/dist/src/wgsl/grid-centroid.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-centroid.wgsl.js +29 -0
- package/dist/src/wgsl/grid-centroid.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-downsample.wgsl.d.ts +7 -0
- package/dist/src/wgsl/grid-downsample.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-downsample.wgsl.js +28 -0
- package/dist/src/wgsl/grid-downsample.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-far-field.wgsl.d.ts +13 -0
- package/dist/src/wgsl/grid-far-field.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-far-field.wgsl.js +98 -0
- package/dist/src/wgsl/grid-far-field.wgsl.js.map +1 -0
- package/dist/src/wgsl/grid-near-field.wgsl.d.ts +19 -0
- package/dist/src/wgsl/grid-near-field.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/grid-near-field.wgsl.js +129 -0
- package/dist/src/wgsl/grid-near-field.wgsl.js.map +1 -0
- package/dist/src/wgsl/histogram.wgsl.d.ts +7 -0
- package/dist/src/wgsl/histogram.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/histogram.wgsl.js +15 -0
- package/dist/src/wgsl/histogram.wgsl.js.map +1 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.d.ts +8 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.js +26 -0
- package/dist/src/wgsl/indirect-finalize.wgsl.js.map +1 -0
- package/dist/src/wgsl/radix-hist.wgsl.d.ts +9 -0
- package/dist/src/wgsl/radix-hist.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/radix-hist.wgsl.js +31 -0
- package/dist/src/wgsl/radix-hist.wgsl.js.map +1 -0
- package/dist/src/wgsl/radix-scatter.wgsl.d.ts +9 -0
- package/dist/src/wgsl/radix-scatter.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/radix-scatter.wgsl.js +40 -0
- package/dist/src/wgsl/radix-scatter.wgsl.js.map +1 -0
- package/dist/src/wgsl/scan-add.wgsl.d.ts +6 -0
- package/dist/src/wgsl/scan-add.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/scan-add.wgsl.js +14 -0
- package/dist/src/wgsl/scan-add.wgsl.js.map +1 -0
- package/dist/src/wgsl/scan-block.wgsl.d.ts +8 -0
- package/dist/src/wgsl/scan-block.wgsl.d.ts.map +1 -0
- package/dist/src/wgsl/scan-block.wgsl.js +30 -0
- package/dist/src/wgsl/scan-block.wgsl.js.map +1 -0
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts +22 -8
- package/dist/src/wgsl/segmented-reduce.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/segmented-reduce.wgsl.js +84 -15
- package/dist/src/wgsl/segmented-reduce.wgsl.js.map +1 -1
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts +22 -11
- package/dist/src/wgsl/spmv-pull.wgsl.d.ts.map +1 -1
- package/dist/src/wgsl/spmv-pull.wgsl.js +110 -36
- package/dist/src/wgsl/spmv-pull.wgsl.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/webgpu-graph-algorithms.js +5016 -1130
- package/dist/webgpu-graph-algorithms.js.map +1 -1
- package/package.json +10 -7
- package/src/accelerator.ts +46 -12
- package/src/algorithms/components.ts +12 -16
- package/src/algorithms/degree.ts +58 -43
- package/src/algorithms/pagerank.ts +20 -18
- package/src/algorithms/power-iteration.ts +19 -18
- package/src/constants.ts +108 -8
- package/src/errors.ts +3 -1
- package/src/index.ts +25 -5
- package/src/kernel/dispatch.ts +18 -7
- package/src/kernel/kernel.ts +59 -5
- package/src/kernel/prelude.ts +15 -0
- package/src/kernel/profiler.ts +28 -4
- package/src/kernels.ts +378 -24
- package/src/layouts/calibrate.ts +187 -0
- package/src/layouts/force-simulation.ts +111 -26
- package/src/layouts/forceatlas2.ts +346 -324
- package/src/layouts/fruchterman-reingold.ts +918 -0
- package/src/layouts/model-common.ts +323 -0
- package/src/layouts/repulsion-grid.ts +451 -0
- package/src/layouts/spring-electrical.ts +845 -0
- package/src/memory/residency.ts +126 -20
- package/src/primitives/core-shape.ts +91 -4
- package/src/primitives/grid-pyramid.ts +221 -0
- package/src/primitives/grid.ts +349 -0
- package/src/primitives/histogram.ts +273 -0
- package/src/primitives/radix-sort.ts +246 -0
- package/src/primitives/scan.ts +197 -0
- package/src/primitives/segmented-reduce.ts +214 -56
- package/src/primitives/spmv.ts +125 -65
- package/src/primitives/verify.ts +249 -0
- package/src/types/accelerator.ts +15 -3
- package/src/types/context.ts +56 -0
- package/src/types/layout.ts +58 -0
- package/src/types/options.ts +45 -1
- package/src/wgsl/counting-scatter.wgsl.ts +16 -0
- package/src/wgsl/fa2-attraction.wgsl.ts +100 -17
- package/src/wgsl/fa2-integrate.wgsl.ts +28 -2
- package/src/wgsl/fa2-repulsion-exact.wgsl.ts +14 -5
- package/src/wgsl/fa2-stats-finalize.wgsl.ts +54 -0
- package/src/wgsl/grid-cell-key.wgsl.ts +29 -0
- package/src/wgsl/grid-centroid-hub.wgsl.ts +28 -0
- package/src/wgsl/grid-centroid.wgsl.ts +28 -0
- package/src/wgsl/grid-downsample.wgsl.ts +27 -0
- package/src/wgsl/grid-far-field.wgsl.ts +97 -0
- package/src/wgsl/grid-near-field.wgsl.ts +128 -0
- package/src/wgsl/histogram.wgsl.ts +14 -0
- package/src/wgsl/indirect-finalize.wgsl.ts +25 -0
- package/src/wgsl/radix-hist.wgsl.ts +30 -0
- package/src/wgsl/radix-scatter.wgsl.ts +39 -0
- package/src/wgsl/scan-add.wgsl.ts +13 -0
- package/src/wgsl/scan-block.wgsl.ts +29 -0
- package/src/wgsl/segmented-reduce.wgsl.ts +84 -15
- package/src/wgsl/spmv-pull.wgsl.ts +110 -36
- package/dist/chunks/context-CRbw2Wyo.js.map +0 -1
|
@@ -4,37 +4,46 @@
|
|
|
4
4
|
* record (spec 7.2 with the 4.6 NetworkX corrections), the per-iteration Fa2Params values, the controller resets of
|
|
5
5
|
* spec 7.17 and the stats decoder -- plus the two option resolvers and `createForceAtlas2`. Positions are vec4f
|
|
6
6
|
* (xyz + mass) in layout units on the device (D23, 7.18); the speed controller runs on the device (D15); no
|
|
7
|
-
* displacement clamp (D25); K2 runs the
|
|
7
|
+
* displacement clamp (D25); K2 runs the degree tiers over `degreeOrder()` (P4 PD-7).
|
|
8
8
|
*
|
|
9
9
|
* Model decisions this file fixes (the plan of P3-T2 lists the reasons): recordIteration with no `upTo` records every
|
|
10
|
-
* stage including toScene; the K1-K5 dispatches of every iteration of one batch share ONE compute
|
|
11
|
-
* batch's first recordIteration and remembered by batch id) and toScene runs in a second pass
|
|
12
|
-
* 4.4); the fill kernel takes its FillParams from a model-owned 256-byte uniform buffer
|
|
13
|
-
* iteration after every load() zeroes oldForce with a fill (paper mode)
|
|
14
|
-
*
|
|
10
|
+
* stage including toScene; on the exact tier the K1-K5 dispatches of every iteration of one batch share ONE compute
|
|
11
|
+
* pass (opened by the batch's first recordIteration and remembered by batch id) and toScene runs in a second pass
|
|
12
|
+
* that ends it (contract 4.4); the fill kernel takes its FillParams from a model-owned 256-byte uniform buffer
|
|
13
|
+
* ("fillParams"); the first iteration after every load() zeroes oldForce with a fill (paper mode). The grid tier
|
|
14
|
+
* (P4-T10) is reached through `RepulsionGrid` when `tierFor(tuning, n)` says so (PD-18): `buffers()` adds the grid
|
|
15
|
+
* buffers, K1 derives the frame under `gridMax > 0` (PD-14), the iteration is recorded as the three passes `fa2-k1`
|
|
16
|
+
* / `fa2-attraction` / `fa2-grid` before `fa2-to-scene` (PD-16), and `stages` is the union list of both tiers
|
|
17
|
+
* (PD-17: `upTo` stops after the last stage recorded at or before its position).
|
|
15
18
|
*/
|
|
16
|
-
import { FA2_DEFAULTS, LAYOUT_TUNING_DEFAULTS, MAX_ITERATIONS_PER_STEP, TRACE_RECORD_BYTES, UNIFORM_SLOT_BYTES, } from "../constants.js";
|
|
19
|
+
import { EXACT_MAX_NODES, FA2_DEFAULTS, GRID_BBOX_MARGIN, GRID_EXTENT_FLOOR, LAYOUT_TUNING_DEFAULTS, MAX_ITERATIONS_PER_STEP, TRACE_RECORD_BYTES, UNIFORM_SLOT_BYTES, } from "../constants.js";
|
|
17
20
|
import { BufferUsage } from "../device/webgpu-constants.js";
|
|
18
21
|
import { WebGpuGraphError } from "../errors.js";
|
|
19
22
|
import { plan1d } from "../kernel/dispatch.js";
|
|
20
|
-
import { FA2_PARAMS, FA2_STATE, FA2_TRACE, FILL_PARAMS,
|
|
21
|
-
import {
|
|
23
|
+
import { FA2_PARAMS, FA2_STATE, FA2_TRACE, FILL_PARAMS, kernelSpec } from "../kernels.js";
|
|
24
|
+
import { arcCountOf } from "../primitives/core-shape.js";
|
|
25
|
+
import { gridSpecFor } from "../primitives/grid.js";
|
|
26
|
+
import { ForceSimulation, tierFor, } from "./force-simulation.js";
|
|
22
27
|
import { resolveNodeMass, resolveWeights } from "./inputs.js";
|
|
28
|
+
import { bindAttraction, describeValue, FILL_PARAMS_BUFFER, FORCE_BYTES_PER_NODE, invalid, isPositiveInteger, pickBoolean, pickCenter, pickDim, pickNumber, pickSeed, recordAttraction, scalar, seedWord, subset, vector, } from "./model-common.js";
|
|
23
29
|
import { RepulsionExact } from "./repulsion-exact.js";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
const
|
|
28
|
-
/** The
|
|
29
|
-
const
|
|
30
|
+
import { RepulsionGrid } from "./repulsion-grid.js";
|
|
31
|
+
// ============================================================ constants and small helpers
|
|
32
|
+
/** The stage names of both tiers in dispatch order plus the per-batch toScene (spec 7.4; contract 3.13; P4 PD-17): the exact tier records K1 K2 K3 K4 K5, the grid tier K1 K2 G1..G7 K4 K5. */
|
|
33
|
+
const FA2_STAGES = ["K1", "K2", "K3", "G1", "G2", "G3", "G4", "G5", "G6", "G7", "K4", "K5", "toScene"];
|
|
34
|
+
/** The FA2_STAGES index of the first grid stage, of K4, of K5 and of toScene. */
|
|
35
|
+
const STAGE_G1 = 3;
|
|
36
|
+
const STAGE_K4 = 10;
|
|
37
|
+
const STAGE_K5 = 11;
|
|
38
|
+
const STAGE_TO_SCENE = 12;
|
|
39
|
+
/** The name of the model-owned hub-counter buffer K1 binds on every tier (P4 PD-14). */
|
|
40
|
+
const HUB_COUNTERS_BUFFER = "hubCounters";
|
|
30
41
|
/** The one-workgroup dispatch of K1 (spec 7.4). */
|
|
31
42
|
const ONE_WORKGROUP = { x: 1, y: 1, z: 1, items: 1, stride: null };
|
|
32
43
|
/** Every override K2 accepts, with its default (contract 3.10.1 plus the two standard graph overrides). */
|
|
33
44
|
const K2_DEFAULTS = { LINLOG: false, DISTRIBUTED: false, TIER: 0, USE_PERM: false, HAS_WEIGHTS: false };
|
|
34
45
|
/** Every override K5 accepts, with its default. */
|
|
35
46
|
const K5_DEFAULTS = { SWING_MODE: 0 };
|
|
36
|
-
/** 2^32, the modulus of the u32 seed word (computed with `%`, never a bitwise operator). */
|
|
37
|
-
const U32_MODULUS = 4294967296;
|
|
38
47
|
/** The resolved record with no option given: FA2_DEFAULTS plus the null / origin defaults of spec 7.14. */
|
|
39
48
|
const DEFAULT_RESOLVED = Object.freeze({
|
|
40
49
|
...FA2_DEFAULTS,
|
|
@@ -44,191 +53,6 @@ const DEFAULT_RESOLVED = Object.freeze({
|
|
|
44
53
|
center: [0, 0, 0],
|
|
45
54
|
seed: null,
|
|
46
55
|
});
|
|
47
|
-
/**
|
|
48
|
-
* A short, safe rendering of an argument value for error messages (never String() on an object).
|
|
49
|
-
* @param value - the value
|
|
50
|
-
* @returns the rendering
|
|
51
|
-
*/
|
|
52
|
-
function describeValue(value) {
|
|
53
|
-
if (value === null) {
|
|
54
|
-
return "null";
|
|
55
|
-
}
|
|
56
|
-
if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
|
|
57
|
-
return String(value);
|
|
58
|
-
}
|
|
59
|
-
if (typeof value === "undefined") {
|
|
60
|
-
return "undefined";
|
|
61
|
-
}
|
|
62
|
-
if (typeof value === "object" && "length" in value && typeof value.length === "number") {
|
|
63
|
-
return `[${value.length} values]`;
|
|
64
|
-
}
|
|
65
|
-
return typeof value;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* The E_INVALID_ARGUMENT error of an option check (contract 3.1: { argument, value, expected }).
|
|
69
|
-
* @param argument - the option name
|
|
70
|
-
* @param value - the value given
|
|
71
|
-
* @param expected - what was expected
|
|
72
|
-
* @returns the error (not thrown here)
|
|
73
|
-
*/
|
|
74
|
-
function invalid(argument, value, expected) {
|
|
75
|
-
return new WebGpuGraphError("E_INVALID_ARGUMENT", `${argument} must be ${expected}; got ${describeValue(value)}`, {
|
|
76
|
-
argument,
|
|
77
|
-
value,
|
|
78
|
-
expected,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* A numeric option: the given value when defined, else the fallback; validated by `check` (the value is checked as
|
|
83
|
-
* `unknown` so a JS caller's string or object is E_INVALID_ARGUMENT too).
|
|
84
|
-
* @param name - the option name
|
|
85
|
-
* @param given - the value given (undefined = absent)
|
|
86
|
-
* @param fallback - the previous record's value or the default
|
|
87
|
-
* @param check - the range predicate over a finite number
|
|
88
|
-
* @param expected - the range in words (the error message)
|
|
89
|
-
* @returns the value
|
|
90
|
-
*/
|
|
91
|
-
function pickNumber(name, given, fallback, check, expected) {
|
|
92
|
-
const value = given === undefined ? fallback : given;
|
|
93
|
-
if (typeof value !== "number" || !Number.isFinite(value) || !check(value)) {
|
|
94
|
-
throw invalid(name, value, expected);
|
|
95
|
-
}
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* A boolean option: the given value when defined, else the fallback; a non-boolean is E_INVALID_ARGUMENT.
|
|
100
|
-
* @param name - the option name
|
|
101
|
-
* @param given - the value given (undefined = absent)
|
|
102
|
-
* @param fallback - the previous record's value or the default
|
|
103
|
-
* @returns the value
|
|
104
|
-
*/
|
|
105
|
-
function pickBoolean(name, given, fallback) {
|
|
106
|
-
const value = given === undefined ? fallback : given;
|
|
107
|
-
if (typeof value !== "boolean") {
|
|
108
|
-
throw invalid(name, value, "a boolean");
|
|
109
|
-
}
|
|
110
|
-
return value;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* The layout dimension: 2 or 3.
|
|
114
|
-
* @param given - the value given (undefined = absent)
|
|
115
|
-
* @param fallback - the previous record's value or the default
|
|
116
|
-
* @returns 2 or 3
|
|
117
|
-
*/
|
|
118
|
-
function pickDim(given, fallback) {
|
|
119
|
-
const value = given === undefined ? fallback : given;
|
|
120
|
-
if (value !== 2 && value !== 3) {
|
|
121
|
-
throw invalid("dim", value, "2 or 3");
|
|
122
|
-
}
|
|
123
|
-
return value;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* The scene-unit center: an array-like of 2 (z = 0) or 3 finite numbers.
|
|
127
|
-
* @param given - the value given (undefined = absent)
|
|
128
|
-
* @param fallback - the previous record's value or the default
|
|
129
|
-
* @returns the three components
|
|
130
|
-
*/
|
|
131
|
-
function pickCenter(given, fallback) {
|
|
132
|
-
if (given === undefined) {
|
|
133
|
-
return fallback;
|
|
134
|
-
}
|
|
135
|
-
const expected = "an array of 2 or 3 finite numbers";
|
|
136
|
-
const value = given;
|
|
137
|
-
if (typeof value !== "object" || value === null || !("length" in value)) {
|
|
138
|
-
throw invalid("center", given, expected);
|
|
139
|
-
}
|
|
140
|
-
const { length } = value;
|
|
141
|
-
if (length !== 2 && length !== 3) {
|
|
142
|
-
throw invalid("center", given, expected);
|
|
143
|
-
}
|
|
144
|
-
const x = given[0];
|
|
145
|
-
const y = given[1];
|
|
146
|
-
const z = length === 3 ? given[2] : 0;
|
|
147
|
-
if (typeof x !== "number" ||
|
|
148
|
-
typeof y !== "number" ||
|
|
149
|
-
typeof z !== "number" ||
|
|
150
|
-
!Number.isFinite(x) ||
|
|
151
|
-
!Number.isFinite(y) ||
|
|
152
|
-
!Number.isFinite(z)) {
|
|
153
|
-
throw invalid("center", given, expected);
|
|
154
|
-
}
|
|
155
|
-
return [x, y, z];
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* The seed: a finite number, or null (unseeded; 0 keeps the port's "0 = unseeded" quirk through the Lcg).
|
|
159
|
-
* @param given - the value given (undefined = absent)
|
|
160
|
-
* @param fallback - the previous record's value or the default
|
|
161
|
-
* @returns the seed or null
|
|
162
|
-
*/
|
|
163
|
-
function pickSeed(given, fallback) {
|
|
164
|
-
if (given === undefined) {
|
|
165
|
-
return fallback;
|
|
166
|
-
}
|
|
167
|
-
const value = given;
|
|
168
|
-
if (value !== null && (typeof value !== "number" || !Number.isFinite(value))) {
|
|
169
|
-
throw invalid("seed", given, "a finite number or null");
|
|
170
|
-
}
|
|
171
|
-
return value;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Integer >= 1.
|
|
175
|
-
* @param value - a finite number
|
|
176
|
-
* @returns whether it is a positive integer
|
|
177
|
-
*/
|
|
178
|
-
function isPositiveInteger(value) {
|
|
179
|
-
return Number.isInteger(value) && value >= 1;
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* The u32 word written into Fa2Params.seed: 0 for null, else floor(|seed|) mod 2^32.
|
|
183
|
-
* @param seed - the resolved seed
|
|
184
|
-
* @returns the u32 value
|
|
185
|
-
*/
|
|
186
|
-
function seedWord(seed) {
|
|
187
|
-
if (seed === null) {
|
|
188
|
-
return 0;
|
|
189
|
-
}
|
|
190
|
-
return Math.floor(Math.abs(seed)) % U32_MODULUS;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* A scalar field of a block's read() result.
|
|
194
|
-
* @param values - the values read
|
|
195
|
-
* @param name - the field name
|
|
196
|
-
* @returns the number
|
|
197
|
-
*/
|
|
198
|
-
function scalar(values, name) {
|
|
199
|
-
const value = values[name];
|
|
200
|
-
if (typeof value !== "number") {
|
|
201
|
-
throw invalid(name, value, "a scalar field");
|
|
202
|
-
}
|
|
203
|
-
return value;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* A vector field of a block's read() result.
|
|
207
|
-
* @param values - the values read
|
|
208
|
-
* @param name - the field name
|
|
209
|
-
* @returns the components
|
|
210
|
-
*/
|
|
211
|
-
function vector(values, name) {
|
|
212
|
-
const value = values[name];
|
|
213
|
-
if (typeof value === "number") {
|
|
214
|
-
throw invalid(name, value, "a vector field");
|
|
215
|
-
}
|
|
216
|
-
return value;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* The override record a kernel gets: its defaults overlaid with the values present in the merged set (contract 3.9:
|
|
220
|
-
* a name a spec does not declare is rejected at compose time, so nothing else is passed through).
|
|
221
|
-
* @param merged - the merged override set of the model (plus USE_PERM / HAS_WEIGHTS from the simulation)
|
|
222
|
-
* @param defaults - the kernel's accepted names with their defaults
|
|
223
|
-
* @returns the kernel's override record, every accepted name explicit
|
|
224
|
-
*/
|
|
225
|
-
function subset(merged, defaults) {
|
|
226
|
-
const out = {};
|
|
227
|
-
for (const name of Object.keys(defaults)) {
|
|
228
|
-
out[name] = name in merged ? merged[name] : defaults[name];
|
|
229
|
-
}
|
|
230
|
-
return out;
|
|
231
|
-
}
|
|
232
56
|
/**
|
|
233
57
|
* The K3 / K4 override values of a merged set (typed for RepulsionExact).
|
|
234
58
|
* @param merged - the merged override set
|
|
@@ -241,6 +65,14 @@ function repulsionOverrides(merged) {
|
|
|
241
65
|
GRAVITY_CENTER: merged.GRAVITY_CENTER === 1 ? 1 : 0,
|
|
242
66
|
};
|
|
243
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The G6 / G7 / K4 override values of a merged set (typed for RepulsionGrid): K3's three and the FA2 law (P4-T13).
|
|
70
|
+
* @param merged - the merged override set
|
|
71
|
+
* @returns the grid stage's overrides
|
|
72
|
+
*/
|
|
73
|
+
function gridOverrides(merged) {
|
|
74
|
+
return { ...repulsionOverrides(merged), LAW: 0 };
|
|
75
|
+
}
|
|
244
76
|
// ============================================================ the resolvers
|
|
245
77
|
/**
|
|
246
78
|
* Applies FA2_DEFAULTS to the option record; validates ranges (spec 7.14; contract 3.13). With `previous` the record
|
|
@@ -285,8 +117,8 @@ export function resolveForceAtlas2Options(options, previous) {
|
|
|
285
117
|
return Object.freeze(resolved);
|
|
286
118
|
}
|
|
287
119
|
/**
|
|
288
|
-
* Applies LAYOUT_TUNING_DEFAULTS (spec 7.14
|
|
289
|
-
*
|
|
120
|
+
* Applies LAYOUT_TUNING_DEFAULTS (spec 7.14; contract 3.3). `nearMax` is an integer >= 2 (P4 DEP-P4-M: the
|
|
121
|
+
* near-field estimator needs at least one sampled entry besides the node itself).
|
|
290
122
|
* @param tuning - the GPU-only knobs given (any object carrying them, e.g. the createForceAtlas2 options)
|
|
291
123
|
* @returns the frozen resolved tuning
|
|
292
124
|
*/
|
|
@@ -303,7 +135,7 @@ export function resolveLayoutTuning(tuning) {
|
|
|
303
135
|
const resolved = {
|
|
304
136
|
repulsion,
|
|
305
137
|
exactMaxNodes: pickNumber("exactMaxNodes", t.exactMaxNodes, LAYOUT_TUNING_DEFAULTS.exactMaxNodes, isPositiveInteger, "an integer >= 1"),
|
|
306
|
-
nearMax: pickNumber("nearMax", t.nearMax, LAYOUT_TUNING_DEFAULTS.nearMax, isPositiveInteger, "an integer >=
|
|
138
|
+
nearMax: pickNumber("nearMax", t.nearMax, LAYOUT_TUNING_DEFAULTS.nearMax, (v) => isPositiveInteger(v) && v >= 2, "an integer >= 2"),
|
|
307
139
|
deterministic: pickBoolean("deterministic", t.deterministic, LAYOUT_TUNING_DEFAULTS.deterministic),
|
|
308
140
|
gridMax2D: pickNumber("gridMax2D", t.gridMax2D, LAYOUT_TUNING_DEFAULTS.gridMax2D, isPositiveInteger, "an integer >= 1"),
|
|
309
141
|
gridMax3D: pickNumber("gridMax3D", t.gridMax3D, LAYOUT_TUNING_DEFAULTS.gridMax3D, isPositiveInteger, "an integer >= 1"),
|
|
@@ -312,7 +144,7 @@ export function resolveLayoutTuning(tuning) {
|
|
|
312
144
|
};
|
|
313
145
|
return Object.freeze(resolved);
|
|
314
146
|
}
|
|
315
|
-
/** The ForceAtlas2 model (spec 7.4: K1 K2 K3 K4 K5 per iteration; toScene once per batch). Stages:
|
|
147
|
+
/** The ForceAtlas2 model (spec 7.4: K1 K2 K3 K4 K5 per iteration on the exact tier, K1 K2 G1..G7 K4 K5 on the grid tier; toScene once per batch). Stages: the union list of PD-17. */
|
|
316
148
|
export class ForceAtlas2Model {
|
|
317
149
|
/**
|
|
318
150
|
* Creates the model for one simulation.
|
|
@@ -337,6 +169,8 @@ export class ForceAtlas2Model {
|
|
|
337
169
|
this.bound = null;
|
|
338
170
|
/** Armed by onLoad(): the next recordIteration zeroes oldForce first (paper mode). */
|
|
339
171
|
this.resetOldForce = false;
|
|
172
|
+
/** The grid of the load inputs() last resolved (null on the exact tier): onLoad() writes its frame, specs() lists its kernels. */
|
|
173
|
+
this.nextGrid = null;
|
|
340
174
|
/**
|
|
341
175
|
* The K1-K5 compute pass of the batch being recorded, keyed by CommandBatch.id (unique per batch): every
|
|
342
176
|
* recordIteration of one batch dispatches into it (ONE pass per batch, contract 4.4); null between batches and
|
|
@@ -355,15 +189,18 @@ export class ForceAtlas2Model {
|
|
|
355
189
|
}
|
|
356
190
|
/**
|
|
357
191
|
* force 12n and oldForce 12n (zeroed) in BOTH swing modes (3.10.1: a writable slot is never aliased; mode 1 leaves
|
|
358
|
-
* oldForce unread and unwritten),
|
|
359
|
-
*
|
|
192
|
+
* oldForce unread and unwritten), the 256-byte FillParams uniform buffer the fill dispatches read, the 16-byte
|
|
193
|
+
* `hubCounters` K1 binds on every tier (P4 PD-14), and the grid buffers of `RepulsionGrid.buffers` exactly when
|
|
194
|
+
* `tierFor(tuning, n)` is the grid tier (PD-18). n = 0 reports one node's worth of bytes so no zero-length buffer
|
|
195
|
+
* is ever created (spec 3.6).
|
|
360
196
|
* @param n - the node count
|
|
361
|
-
* @param
|
|
362
|
-
* @returns the
|
|
197
|
+
* @param dim - the layout dimension (the force arrays are stride 3 in both; the grid's geometry differs)
|
|
198
|
+
* @returns the model-owned buffer specs
|
|
363
199
|
*/
|
|
364
|
-
buffers(n,
|
|
200
|
+
buffers(n, dim) {
|
|
365
201
|
const bytes = Math.max(1, n) * FORCE_BYTES_PER_NODE;
|
|
366
202
|
const usage = BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST;
|
|
203
|
+
const grid = tierFor(this.tuning, n) === "grid" ? RepulsionGrid.buffers(n, gridSpecFor(n, dim, this.tuning)) : [];
|
|
367
204
|
return [
|
|
368
205
|
{ name: "force", byteLength: bytes, usage, zero: true },
|
|
369
206
|
{ name: "oldForce", byteLength: bytes, usage, zero: true },
|
|
@@ -373,29 +210,25 @@ export class ForceAtlas2Model {
|
|
|
373
210
|
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
374
211
|
zero: false,
|
|
375
212
|
},
|
|
213
|
+
{ name: HUB_COUNTERS_BUFFER, byteLength: 16, usage, zero: true },
|
|
214
|
+
...grid,
|
|
376
215
|
];
|
|
377
216
|
}
|
|
378
217
|
/**
|
|
379
|
-
* { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) } (3.13 inputs.ts)
|
|
380
|
-
*
|
|
381
|
-
*
|
|
218
|
+
* { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) } (3.13 inputs.ts).
|
|
219
|
+
* Also remembers the grid of this load (`tierFor(tuning, n)`, spec 7.8) for onLoad() and specs(): the simulation
|
|
220
|
+
* calls inputs() first, then onLoad() before bind().
|
|
382
221
|
* @param s - the snapshot being loaded
|
|
383
222
|
* @param options - the simulation's current option record
|
|
384
223
|
* @returns the per-load inputs
|
|
385
224
|
*/
|
|
386
225
|
inputs(s, options) {
|
|
387
226
|
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
388
|
-
const { repulsion, exactMaxNodes } = this.tuning;
|
|
389
227
|
const n = s.nodeCount;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
feature: "repulsion.grid",
|
|
395
|
-
hint: 'pass repulsion: "exact" (or raise exactMaxNodes) to run the exact tier at this size',
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
return { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) };
|
|
228
|
+
// resolve first: a throwing mass / weight resolution leaves the remembered grid of the previous load intact
|
|
229
|
+
const inputs = { mass: resolveNodeMass(s, resolved.nodeMass), weights: resolveWeights(s, resolved.weight) };
|
|
230
|
+
this.nextGrid = tierFor(this.tuning, n) === "grid" ? gridSpecFor(n, resolved.dim, this.tuning) : null;
|
|
231
|
+
return inputs;
|
|
399
232
|
}
|
|
400
233
|
/**
|
|
401
234
|
* { LINLOG, DISTRIBUTED, TIER: 0, SWING_MODE: compat === "networkx" ? 1 : 0, STRONG_GRAVITY, GRAVITY_CENTER:
|
|
@@ -419,7 +252,9 @@ export class ForceAtlas2Model {
|
|
|
419
252
|
}
|
|
420
253
|
/**
|
|
421
254
|
* The seven module specs of an override set in dispatch order -- K1, K2, K3, K4, K5, toScene, fill -- each with
|
|
422
|
-
* only the override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix
|
|
255
|
+
* only the override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix,
|
|
256
|
+
* followed by the grid tier's specs (`RepulsionGrid.specs`) when the load inputs() last resolved is a grid load
|
|
257
|
+
* (the pipeline key carries no geometry, so the spec's size is immaterial).
|
|
423
258
|
* @param overrides - the merged override set (the model's plus USE_PERM / HAS_WEIGHTS)
|
|
424
259
|
* @param _subgroups - accepted for the ForceModel interface and unused: every reducing FA2 body carries
|
|
425
260
|
* needs: ["subgroups"] in its registry entry and the composer picks the twin from caps.features (contract 4.3)
|
|
@@ -427,6 +262,9 @@ export class ForceAtlas2Model {
|
|
|
427
262
|
*/
|
|
428
263
|
specs(overrides, _subgroups) {
|
|
429
264
|
const [repulsionSpec, speedSpec] = RepulsionExact.specs(repulsionOverrides(overrides));
|
|
265
|
+
const grid = this.nextGrid === null
|
|
266
|
+
? []
|
|
267
|
+
: RepulsionGrid.specs(gridOverrides(overrides), gridSpecFor(EXACT_MAX_NODES + 1, 2, this.tuning));
|
|
430
268
|
return [
|
|
431
269
|
kernelSpec("fa2-stats-finalize"),
|
|
432
270
|
kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS)),
|
|
@@ -435,44 +273,56 @@ export class ForceAtlas2Model {
|
|
|
435
273
|
kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)),
|
|
436
274
|
kernelSpec("fa2-to-scene"),
|
|
437
275
|
kernelSpec("fill"),
|
|
276
|
+
...grid,
|
|
438
277
|
];
|
|
439
278
|
}
|
|
440
279
|
/**
|
|
441
|
-
* Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2
|
|
442
|
-
* force when arcCount === 0), K3 + K4 through RepulsionExact, K5,
|
|
443
|
-
* FillParams { count: 3n, value: 0, mode: 0 } into the model's
|
|
280
|
+
* Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2 over the degree
|
|
281
|
+
* tiers through bindAttraction (or the fill of force when arcCount === 0), K3 + K4 through RepulsionExact, K5,
|
|
282
|
+
* toScene, and the fill of oldForce; writes the FillParams { count: 3n, value: 0, mode: 0 } into the model's
|
|
283
|
+
* uniform buffer. With n === 0 nothing is bound. The K2 TIER 1 / 2 pipelines compile on the first load whose
|
|
284
|
+
* degrees need them (P4 PD-7), a one-time cost at that load.
|
|
444
285
|
* @param resources - the graph, the shared and model buffers, the ring and the cache
|
|
445
286
|
* @param overrides - the merged override set
|
|
446
287
|
*/
|
|
447
288
|
async bind(resources, overrides) {
|
|
448
289
|
this.dropBound();
|
|
449
290
|
this.resources = resources;
|
|
450
|
-
const { n, pipelines, caps, core,
|
|
291
|
+
const { n, pipelines, caps, core, ring, device } = resources;
|
|
451
292
|
if (n === 0) {
|
|
452
293
|
return;
|
|
453
294
|
}
|
|
454
|
-
const
|
|
295
|
+
const pos = resources.buffer("positions");
|
|
296
|
+
const force = resources.buffer("force");
|
|
297
|
+
const params = ring.binding(FA2_PARAMS);
|
|
298
|
+
const hasArcs = core.colIdx !== null;
|
|
299
|
+
const [k1, k5, toScene, fill] = await Promise.all([
|
|
455
300
|
pipelines.kernel(kernelSpec("fa2-stats-finalize")),
|
|
456
|
-
pipelines.kernel(kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS))),
|
|
457
301
|
pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS))),
|
|
458
302
|
pipelines.kernel(kernelSpec("fa2-to-scene")),
|
|
459
303
|
pipelines.kernel(kernelSpec("fill")),
|
|
460
304
|
]);
|
|
461
|
-
const repulsion =
|
|
305
|
+
const repulsion = resources.tier === "grid"
|
|
306
|
+
? null
|
|
307
|
+
: await RepulsionExact.create(pipelines, caps, repulsionOverrides(overrides));
|
|
308
|
+
const attraction = hasArcs
|
|
309
|
+
? await bindAttraction(resources, subset(overrides, K2_DEFAULTS), { pos, force, params })
|
|
310
|
+
: null;
|
|
311
|
+
const grid = resources.tier === "grid"
|
|
312
|
+
? await RepulsionGrid.create(resources, k1.workgroupSize, gridOverrides(overrides), gridSpecFor(n, resources.dim, this.tuning))
|
|
313
|
+
: null;
|
|
462
314
|
if (this.resources !== resources) {
|
|
463
315
|
// a newer bind() superseded this one while the pipelines compiled; its own bind groups stand
|
|
316
|
+
grid?.dispose();
|
|
464
317
|
return;
|
|
465
318
|
}
|
|
466
|
-
const pos = resources.buffer("positions");
|
|
467
319
|
const scene = resources.buffer("scenePositions");
|
|
468
320
|
const fixed = resources.buffer("fixed");
|
|
469
321
|
const partials = resources.buffer("partials");
|
|
470
322
|
const state = resources.buffer("state");
|
|
471
323
|
const trace = resources.buffer("trace");
|
|
472
|
-
const force = resources.buffer("force");
|
|
473
324
|
const oldForce = resources.buffer("oldForce");
|
|
474
325
|
const fillParamsBuffer = resources.buffer(FILL_PARAMS_BUFFER);
|
|
475
|
-
const params = ring.binding(FA2_PARAMS);
|
|
476
326
|
const fillParams = {
|
|
477
327
|
buffer: fillParamsBuffer.buffer,
|
|
478
328
|
offset: fillParamsBuffer.offset,
|
|
@@ -482,20 +332,41 @@ export class ForceAtlas2Model {
|
|
|
482
332
|
const fillBytes = new ArrayBuffer(FILL_PARAMS.byteLength);
|
|
483
333
|
FILL_PARAMS.write(new DataView(fillBytes), { count: 3 * n, value: 0, mode: 0 });
|
|
484
334
|
device.queue.writeBuffer(fillParamsBuffer.buffer, fillParamsBuffer.offset, fillBytes);
|
|
485
|
-
const
|
|
486
|
-
|
|
335
|
+
const hubCounters = resources.buffer(HUB_COUNTERS_BUFFER);
|
|
336
|
+
const exact = { pos, state, trace, force, oldForce, fixedMask: fixed, partials, params };
|
|
337
|
+
repulsion?.bind(exact);
|
|
338
|
+
grid?.bind({
|
|
339
|
+
...exact,
|
|
340
|
+
cellKey: resources.buffer("cellKey"),
|
|
341
|
+
cellVal: resources.buffer("cellVal"),
|
|
342
|
+
sortedKey: resources.buffer("sortedKey"),
|
|
343
|
+
sortedIdx: resources.buffer("sortedIdx"),
|
|
344
|
+
cellHist: resources.buffer("cellHist"),
|
|
345
|
+
cellStart: resources.buffer("cellStart"),
|
|
346
|
+
hubList: resources.buffer("hubList"),
|
|
347
|
+
hubCounters,
|
|
348
|
+
hubArgs: resources.buffer("hubArgs"),
|
|
349
|
+
pyramid: resources.buffer("pyramid"),
|
|
350
|
+
});
|
|
487
351
|
const wg = k1.workgroupSize;
|
|
488
352
|
this.bound = {
|
|
489
353
|
n,
|
|
490
354
|
plan: plan1d(n, wg, caps),
|
|
491
355
|
fillPlan: plan1d(3 * n, wg, caps),
|
|
492
356
|
k1,
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
:
|
|
357
|
+
// PD-14: on the exact tier K1's grid slots take dummies (cellHist := partials, both read-only; hubCounters
|
|
358
|
+
// is the model's 16-byte buffer on every tier) and the block is dead under gridMax 0
|
|
359
|
+
k1Bound: k1.bind({
|
|
360
|
+
partials,
|
|
361
|
+
S: state,
|
|
362
|
+
T: trace,
|
|
363
|
+
cellHist: grid === null ? partials : resources.buffer("cellHist"),
|
|
364
|
+
hubCounters,
|
|
365
|
+
P: params,
|
|
366
|
+
}),
|
|
367
|
+
attraction,
|
|
498
368
|
repulsion,
|
|
369
|
+
grid,
|
|
499
370
|
k5,
|
|
500
371
|
k5Bound: k5.bind({ force, oldForce, fixedMask: fixed, S: state, pos, partials, P: params }),
|
|
501
372
|
toScene,
|
|
@@ -513,14 +384,19 @@ export class ForceAtlas2Model {
|
|
|
513
384
|
* @returns the uniform values
|
|
514
385
|
*/
|
|
515
386
|
paramsFor(iteration, options) {
|
|
516
|
-
const { n } = this.requireResources();
|
|
387
|
+
const { n, core, tiers, tier, dim } = this.requireResources();
|
|
517
388
|
const resolved = resolveForceAtlas2Options(options, this.current);
|
|
518
389
|
const { nearMax, extentFactor } = this.tuning;
|
|
390
|
+
const grid = tier === "grid" ? gridSpecFor(n, dim, this.tuning) : null;
|
|
391
|
+
// P4 PD-7: TIER 2 reads [0, hiEnd), TIER 1 [hiEnd, midEnd), TIER 0 [tierStart, tierEnd) = [midEnd, n)
|
|
392
|
+
const so = tiers?.segmentOffsets;
|
|
393
|
+
const hiEnd = so?.[1] ?? 0;
|
|
394
|
+
const midEnd = so?.[2] ?? 0;
|
|
519
395
|
return {
|
|
520
396
|
n,
|
|
521
397
|
dim: resolved.dim,
|
|
522
398
|
flags: 0,
|
|
523
|
-
tierStart:
|
|
399
|
+
tierStart: midEnd,
|
|
524
400
|
tierEnd: n,
|
|
525
401
|
iterationIndex: iteration,
|
|
526
402
|
seed: seedWord(resolved.seed),
|
|
@@ -532,33 +408,37 @@ export class ForceAtlas2Model {
|
|
|
532
408
|
center: [resolved.center[0], resolved.center[1], resolved.center[2], 0],
|
|
533
409
|
settleThreshold: resolved.settleThreshold,
|
|
534
410
|
extentFactor,
|
|
535
|
-
gridMax: 0,
|
|
536
|
-
levels: 0,
|
|
537
|
-
|
|
411
|
+
gridMax: grid?.g ?? 0,
|
|
412
|
+
levels: grid?.levels ?? 0,
|
|
413
|
+
arcBase: 0,
|
|
414
|
+
arcEnd: arcCountOf(core),
|
|
415
|
+
accumulate: 0,
|
|
416
|
+
hiEnd,
|
|
417
|
+
midEnd,
|
|
538
418
|
};
|
|
539
419
|
}
|
|
540
420
|
/**
|
|
541
|
-
* Records one iteration into the batch
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
421
|
+
* Records one iteration into the batch, stopping after stage `upTo` when given (spec 7.4; debugRunStages /
|
|
422
|
+
* inspect, spec 11.9 item 2; PD-17: `upTo` names a position in the union list and the recording stops after the
|
|
423
|
+
* last stage recorded at or before it, so "K3" on the grid tier stops after K2 and "G5" on the exact tier after
|
|
424
|
+
* K3). The exact tier: K1, K2 (or the fill of force when arcCount === 0), K3, K4, K5 in the batch's ONE compute
|
|
425
|
+
* pass (opened by the first call of a batch and reused by every later call with the same batch.id, PLAN
|
|
426
|
+
* DECISION 2), then toScene in a second pass that ends it. The grid tier (PD-16): the passes `fa2-k1` (K1),
|
|
427
|
+
* `fa2-attraction` (K2's tiers) and `fa2-grid` (G1-G7, K4, K5) per iteration, then `fa2-to-scene`. The profiler
|
|
428
|
+
* budgets PROFILER_QUERY_SLOTS / 2 = 128 passes per batch, so a grid batch above 42 iterations is timed only in
|
|
429
|
+
* part (the exact tier's two passes per batch always fit): the simulation then reports msPerIteration from the
|
|
430
|
+
* wall time, never from the sum of the timed prefix (`ForceSimulation.batchMilliseconds`). The simulation
|
|
431
|
+
* passes "K5" for iterations 0..k-2 and undefined for the last, so toScene runs once per batch. The first call
|
|
432
|
+
* after load() zeroes oldForce before K1 (paper mode). With n === 0 nothing is recorded (PLAN DECISION 9); a
|
|
433
|
+
* call before bind() completed is E_NOT_LOADED (never a silent no-op).
|
|
548
434
|
* @param batch - the batch being recorded
|
|
549
435
|
* @param slot - the UniformRing slot holding this iteration's Fa2Params
|
|
550
|
-
* @param tier -
|
|
436
|
+
* @param tier - the tier the simulation resolved at load() (the same rule bind() applied, PD-18)
|
|
551
437
|
* @param upTo - a stage name to stop after; undefined records every stage including toScene
|
|
552
438
|
*/
|
|
553
439
|
recordIteration(batch, slot, tier, upTo) {
|
|
554
|
-
if (tier === "grid") {
|
|
555
|
-
throw new WebGpuGraphError("E_UNSUPPORTED", "the grid repulsion tier lands in P4", {
|
|
556
|
-
feature: "repulsion.grid",
|
|
557
|
-
hint: 'pass repulsion: "exact"',
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
440
|
const resources = this.requireResources();
|
|
561
|
-
const stop = upTo === undefined ?
|
|
441
|
+
const stop = upTo === undefined ? STAGE_TO_SCENE : this.stageIndex(upTo);
|
|
562
442
|
const { bound } = this;
|
|
563
443
|
if (bound === null) {
|
|
564
444
|
if (resources.n === 0) {
|
|
@@ -569,47 +449,124 @@ export class ForceAtlas2Model {
|
|
|
569
449
|
});
|
|
570
450
|
}
|
|
571
451
|
const offset = resources.ring.offsetOf(slot);
|
|
452
|
+
if (tier === "grid") {
|
|
453
|
+
this.recordGridIteration(batch, bound, offset, stop);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const { repulsion } = bound;
|
|
457
|
+
if (repulsion === null) {
|
|
458
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the ForceAtlas2 model was bound on the grid tier", {
|
|
459
|
+
state: "loaded",
|
|
460
|
+
});
|
|
461
|
+
}
|
|
572
462
|
const pass = this.openPass !== null && this.openPass.id === batch.id ? this.openPass.pass : batch.pass("fa2");
|
|
573
463
|
this.openPass = { id: batch.id, pass };
|
|
574
|
-
|
|
575
|
-
this.resetOldForce = false;
|
|
576
|
-
if (bound.fillOldBound !== null) {
|
|
577
|
-
bound.fill.dispatch(pass, bound.fillOldBound, bound.fillPlan, [0]);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
464
|
+
this.recordK1(pass, bound, offset);
|
|
581
465
|
if (stop < 1) {
|
|
582
466
|
return;
|
|
583
467
|
}
|
|
584
|
-
|
|
585
|
-
|
|
468
|
+
this.recordK2(pass, bound, offset);
|
|
469
|
+
if (stop < 2) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
repulsion.recordRepulsion(pass, bound.n, offset);
|
|
473
|
+
if (stop < STAGE_K4) {
|
|
474
|
+
return;
|
|
586
475
|
}
|
|
587
|
-
|
|
588
|
-
|
|
476
|
+
repulsion.recordSpeedFinalize(pass, offset);
|
|
477
|
+
if (stop < STAGE_K5) {
|
|
478
|
+
return;
|
|
589
479
|
}
|
|
590
|
-
|
|
480
|
+
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
481
|
+
if (stop < STAGE_TO_SCENE) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.recordToScene(batch, bound, offset);
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* The grid tier's iteration (PD-16): three compute passes before toScene.
|
|
488
|
+
* @param batch - the batch being recorded
|
|
489
|
+
* @param bound - the bound model
|
|
490
|
+
* @param offset - the Fa2Params dynamic offset of the iteration
|
|
491
|
+
* @param stop - the FA2_STAGES index to stop after
|
|
492
|
+
*/
|
|
493
|
+
recordGridIteration(batch, bound, offset, stop) {
|
|
494
|
+
const { grid } = bound;
|
|
495
|
+
if (grid === null) {
|
|
496
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the ForceAtlas2 model was bound on the exact tier", {
|
|
497
|
+
state: "loaded",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
this.openPass = null;
|
|
501
|
+
this.recordK1(batch.pass("fa2-k1"), bound, offset);
|
|
502
|
+
if (stop < 1) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
this.recordK2(batch.pass("fa2-attraction"), bound, offset);
|
|
506
|
+
if (stop < STAGE_G1) {
|
|
591
507
|
return;
|
|
592
508
|
}
|
|
593
|
-
|
|
594
|
-
|
|
509
|
+
const pass = batch.pass("fa2-grid");
|
|
510
|
+
const gridStop = stop < STAGE_K4 ? FA2_STAGES[stop] : undefined;
|
|
511
|
+
grid.recordRepulsion(pass, bound.n, offset, gridStop);
|
|
512
|
+
if (stop < STAGE_K4) {
|
|
595
513
|
return;
|
|
596
514
|
}
|
|
597
|
-
|
|
598
|
-
if (stop <
|
|
515
|
+
grid.recordSpeedFinalize(pass, offset);
|
|
516
|
+
if (stop < STAGE_K5) {
|
|
599
517
|
return;
|
|
600
518
|
}
|
|
601
519
|
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
602
|
-
if (stop <
|
|
520
|
+
if (stop < STAGE_TO_SCENE) {
|
|
603
521
|
return;
|
|
604
522
|
}
|
|
605
|
-
|
|
523
|
+
this.recordToScene(batch, bound, offset);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* The oldForce reset of the first iteration after load() (paper mode), then K1 (one workgroup).
|
|
527
|
+
* @param pass - the open compute pass
|
|
528
|
+
* @param bound - the bound model
|
|
529
|
+
* @param offset - the Fa2Params dynamic offset
|
|
530
|
+
*/
|
|
531
|
+
recordK1(pass, bound, offset) {
|
|
532
|
+
if (this.resetOldForce) {
|
|
533
|
+
this.resetOldForce = false;
|
|
534
|
+
if (bound.fillOldBound !== null) {
|
|
535
|
+
bound.fill.dispatch(pass, bound.fillOldBound, bound.fillPlan, [0]);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* K2's tier dispatches, or the fill of force when the graph has no arcs (spec 7.5).
|
|
542
|
+
* @param pass - the open compute pass
|
|
543
|
+
* @param bound - the bound model
|
|
544
|
+
* @param offset - the Fa2Params dynamic offset
|
|
545
|
+
*/
|
|
546
|
+
recordK2(pass, bound, offset) {
|
|
547
|
+
if (bound.attraction !== null) {
|
|
548
|
+
recordAttraction(pass, bound.attraction, offset);
|
|
549
|
+
}
|
|
550
|
+
else if (bound.fillForceBound !== null) {
|
|
551
|
+
bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* The toScene pass that ends the iteration's pass; the batch is complete after it, so nothing reuses the pass.
|
|
556
|
+
* @param batch - the batch
|
|
557
|
+
* @param bound - the bound model
|
|
558
|
+
* @param offset - the Fa2Params dynamic offset
|
|
559
|
+
*/
|
|
560
|
+
recordToScene(batch, bound, offset) {
|
|
606
561
|
this.openPass = null;
|
|
607
562
|
const scenePass = batch.pass("fa2-to-scene");
|
|
608
563
|
bound.toScene.dispatch(scenePass, bound.toSceneBound, bound.plan, [offset]);
|
|
609
564
|
}
|
|
610
565
|
/**
|
|
611
566
|
* speed = 1, speedEfficiency = 1, swing = 1, traction = 1 (mode 1 accumulates from 1; mode 0 overwrites them each
|
|
612
|
-
* iteration, the initial value is irrelevant); arms the oldForce reset of the next recordIteration.
|
|
567
|
+
* iteration, the initial value is irrelevant); arms the oldForce reset of the next recordIteration. On a grid
|
|
568
|
+
* load the frame of the first build (K1 folds nothing on the first iteration): the same six values K1 derives,
|
|
569
|
+
* in f32 with the kernel's order of operations, from the host-written min / max / centroid / rmsRadius.
|
|
613
570
|
* @param state - the state writer of the simulation
|
|
614
571
|
*/
|
|
615
572
|
onLoad(state) {
|
|
@@ -618,6 +575,9 @@ export class ForceAtlas2Model {
|
|
|
618
575
|
state.set("swing", 1);
|
|
619
576
|
state.set("traction", 1);
|
|
620
577
|
this.resetOldForce = true;
|
|
578
|
+
if (this.nextGrid !== null) {
|
|
579
|
+
writeGridFrame(state, this.nextGrid, this.tuning.extentFactor);
|
|
580
|
+
}
|
|
621
581
|
}
|
|
622
582
|
/**
|
|
623
583
|
* Mode 0: nothing (D8). Mode 1 (networkx): swing = traction = 1 (spec 7.2 "load() and reheat() reset them to 1").
|
|
@@ -651,7 +611,9 @@ export class ForceAtlas2Model {
|
|
|
651
611
|
}
|
|
652
612
|
/**
|
|
653
613
|
* Decodes the state header and the k trace records of a completed batch (k = trace.byteLength / 32) into
|
|
654
|
-
* ForceAtlas2Stats: the
|
|
614
|
+
* ForceAtlas2Stats: `repulsionTier` is the bound tier, the grid fields are the header's on the grid tier
|
|
615
|
+
* (`maxCellOccupancy` / `outsideGrid`: the counts of the iteration before the last K1) and null on the exact
|
|
616
|
+
* tier; msPerIteration null (the simulation owns the clock).
|
|
655
617
|
* @param state - a DataView over the 256-byte state header
|
|
656
618
|
* @param trace - a DataView over the k Fa2Trace records of the batch
|
|
657
619
|
* @returns the stats
|
|
@@ -672,15 +634,16 @@ export class ForceAtlas2Model {
|
|
|
672
634
|
settledCount: scalar(record, "settledCount"),
|
|
673
635
|
});
|
|
674
636
|
}
|
|
637
|
+
const grid = this.resources?.tier === "grid";
|
|
675
638
|
return {
|
|
676
639
|
iteration: scalar(header, "iteration"),
|
|
677
640
|
meanDisplacement: scalar(header, "meanDisplacement"),
|
|
678
641
|
rmsRadius: scalar(header, "rmsRadius"),
|
|
679
642
|
layoutRadius: scalar(header, "radius"),
|
|
680
643
|
centroid: [centroid[0], centroid[1], centroid[2]],
|
|
681
|
-
repulsionTier: "exact",
|
|
682
|
-
maxCellOccupancy: null,
|
|
683
|
-
outsideGrid: null,
|
|
644
|
+
repulsionTier: grid ? "grid" : "exact",
|
|
645
|
+
maxCellOccupancy: grid ? scalar(header, "maxCellOccupancy") : null,
|
|
646
|
+
outsideGrid: grid ? scalar(header, "outsideGrid") : null,
|
|
684
647
|
msPerIteration: null,
|
|
685
648
|
swing: scalar(header, "swing"),
|
|
686
649
|
traction: scalar(header, "traction"),
|
|
@@ -714,10 +677,17 @@ export class ForceAtlas2Model {
|
|
|
714
677
|
}
|
|
715
678
|
throw invalid("upTo", upTo, FA2_STAGES.join(" | "));
|
|
716
679
|
}
|
|
680
|
+
/**
|
|
681
|
+
* Releases the grid stage's lease and the bind groups (the simulation calls it from dispose() once every
|
|
682
|
+
* in-flight batch has settled).
|
|
683
|
+
*/
|
|
684
|
+
dispose() {
|
|
685
|
+
this.dropBound();
|
|
686
|
+
}
|
|
717
687
|
/**
|
|
718
688
|
* Drops the bind groups of the previous bind() (the buffers changed) so the cached kernels do not accumulate stale
|
|
719
|
-
* groups across reloads; K3 / K4 live inside RepulsionExact and keep the P1-T6 behaviour
|
|
720
|
-
* of a batch recorded before the rebind.
|
|
689
|
+
* groups across reloads; K3 / K4 live inside RepulsionExact and keep the P1-T6 behaviour; the grid stage
|
|
690
|
+
* releases its lease. Also forgets the pass of a batch recorded before the rebind.
|
|
721
691
|
*/
|
|
722
692
|
dropBound() {
|
|
723
693
|
this.openPass = null;
|
|
@@ -725,12 +695,48 @@ export class ForceAtlas2Model {
|
|
|
725
695
|
if (bound === null) {
|
|
726
696
|
return;
|
|
727
697
|
}
|
|
728
|
-
for (const kernel of [bound.k1, bound.
|
|
698
|
+
for (const kernel of [bound.k1, bound.k5, bound.toScene, bound.fill]) {
|
|
699
|
+
kernel.invalidate();
|
|
700
|
+
}
|
|
701
|
+
for (const [kernel] of bound.attraction?.kernels ?? []) {
|
|
729
702
|
kernel.invalidate();
|
|
730
703
|
}
|
|
704
|
+
bound.grid?.dispose();
|
|
731
705
|
this.bound = null;
|
|
732
706
|
}
|
|
733
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* The grid frame of the first build after load() (spec 7.7 geometry table; PD-10): K1's text in f32 with the same
|
|
710
|
+
* order of operations -- `box = (max - min) * GRID_BBOX_MARGIN`, `extent = max(min(max(box), extentFactor *
|
|
711
|
+
* rmsRadius), GRID_EXTENT_FLOOR)`, `cellSize = extent / G`, `gridMin = centroid - extent / 2` (cellSize in `.w`),
|
|
712
|
+
* `invCellSize = 1 / cellSize`, `eps = 0.25 cellSize` -- plus zero counts. Shared with the FR and spring-electrical
|
|
713
|
+
* models' onLoad (P4-T13).
|
|
714
|
+
* @param state - the state writer (min / max / centroid / rmsRadius already written by the simulation)
|
|
715
|
+
* @param spec - the grid of the load
|
|
716
|
+
* @param extentFactor - the tuning's extent factor
|
|
717
|
+
*/
|
|
718
|
+
export function writeGridFrame(state, spec, extentFactor) {
|
|
719
|
+
const f = Math.fround;
|
|
720
|
+
const axis = (name) => {
|
|
721
|
+
const value = state.get(name);
|
|
722
|
+
return typeof value === "number" ? [value, value, value] : value;
|
|
723
|
+
};
|
|
724
|
+
const min = axis("min");
|
|
725
|
+
const max = axis("max");
|
|
726
|
+
const centroid = axis("centroid");
|
|
727
|
+
const rms = state.get("rmsRadius");
|
|
728
|
+
const box = [0, 1, 2].map((a) => f(f(f(max[a]) - f(min[a])) * f(GRID_BBOX_MARGIN)));
|
|
729
|
+
const bboxExtent = spec.dim === 3 ? Math.max(box[0], box[1], box[2]) : Math.max(box[0], box[1]);
|
|
730
|
+
const rmsTerm = f(f(extentFactor) * f(typeof rms === "number" ? rms : 0));
|
|
731
|
+
const extent = Math.max(Math.min(bboxExtent, rmsTerm), f(GRID_EXTENT_FLOOR));
|
|
732
|
+
const cellSize = f(extent / spec.g);
|
|
733
|
+
const half = f(0.5 * extent);
|
|
734
|
+
state.set("gridMin", [f(f(centroid[0]) - half), f(f(centroid[1]) - half), f(f(centroid[2]) - half), cellSize]);
|
|
735
|
+
state.set("invCellSize", f(1 / cellSize));
|
|
736
|
+
state.set("eps", f(0.25 * cellSize));
|
|
737
|
+
state.set("outsideGrid", 0);
|
|
738
|
+
state.set("maxCellOccupancy", 0);
|
|
739
|
+
}
|
|
734
740
|
// ============================================================ the factory
|
|
735
741
|
/**
|
|
736
742
|
* The resolve callback of the simulation's setParams: the patch over the current record, re-validated (the current
|
|
@@ -743,8 +749,9 @@ function resolvePatch(patch, current) {
|
|
|
743
749
|
return resolveForceAtlas2Options(patch, resolveForceAtlas2Options(current));
|
|
744
750
|
}
|
|
745
751
|
/**
|
|
746
|
-
* Spec 3.3 createForceAtlas2, verbatim: a GpuLayoutSimulation running ForceAtlas2 on the exact
|
|
747
|
-
* the option defaults of spec 7.14 and the GPU-only tuning of GpuLayoutTuning (contract 3.13
|
|
752
|
+
* Spec 3.3 createForceAtlas2, verbatim: a GpuLayoutSimulation running ForceAtlas2 on the exact or the grid repulsion
|
|
753
|
+
* tier (spec 7.8) with the option defaults of spec 7.14 and the GPU-only tuning of GpuLayoutTuning (contract 3.13
|
|
754
|
+
* "Contracts").
|
|
748
755
|
* @param ctx - the context (E_DISPOSED / E_DEVICE_LOST through assertReady)
|
|
749
756
|
* @param options - the ForceAtlas2 options and the GPU-only tuning knobs in one record
|
|
750
757
|
* @returns the simulation in state "created"; load() next
|