@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
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fruchterman-Reingold on the exact or the grid repulsion tier (spec 7.20, 7.8; contract 3.13): the ForceModel that
|
|
3
|
+
* ForceSimulation drives -- the K1 K2 K3 K5 sequence per iteration on the exact tier (K1 K2 G1..G7 K5 on the grid
|
|
4
|
+
* tier, P4-T13) and toScene per batch (no K4: no speed controller), the constant override set (`LAW` 1 on K2 / K3
|
|
5
|
+
* and on G6 / G7, `APPLY` 1 on K5, `STATS_MODE` 1 on K1, PD-1), the per-iteration Fa2Params values
|
|
6
|
+
* with the cooling schedule's temperature (PD-5), the controller resets and the stats decoder -- plus the option
|
|
7
|
+
* resolver and `createFruchtermanReingold`. The model shares FA2's four kernels, buffers and blocks: `oldForce` is
|
|
8
|
+
* allocated, bound and never read (K5 compiles `SWING_MODE = 1`, PD-2 / PD-20); the mass lane is 1 for every node
|
|
9
|
+
* and weights are ignored (PD-11); the `fixed` option resolves at load through ModelInputs.fixed (PD-6); a
|
|
10
|
+
* `setParams({ fixed })` patch is E_INVALID_ARGUMENT (PD-16).
|
|
11
|
+
*
|
|
12
|
+
* The temperature index (PD-5): the simulation hands paramsFor the GLOBAL iteration index, which load() restarts at 0
|
|
13
|
+
* and reheat() does not touch. The model keeps `tempOrigin` (the global index at which the temperature index is 0):
|
|
14
|
+
* onLoad sets it to 0; onReheat arms `pendingReheat`; the first paramsFor(global) after it sets
|
|
15
|
+
* `tempOrigin = global - floor(0.7 * iterations)`, so the temperature restarts at floor(0.7 * iterations) exactly as
|
|
16
|
+
* the CPU's reheat() does. The temperature is `max(0, 0.1 - dt * (global - tempOrigin))`, dt = 0.1 / (iterations + 1);
|
|
17
|
+
* at 0 nothing moves and the settle window closes the run (DEP-P5-C: the budget restarts at 0, the temperature does
|
|
18
|
+
* not).
|
|
19
|
+
*
|
|
20
|
+
* The grid tier (P4-T13, PD-22) is FA2's: `RepulsionGrid` with `LAW` 1 when `tierFor(tuning, n)` says so (PD-18),
|
|
21
|
+
* the grid buffers from `buffers()`, K1's grid block under `gridMax > 0` (PD-14) over the model's own `hubCounters`,
|
|
22
|
+
* the three passes `fr-k1` / `fr-attraction` / `fr-grid` before `fa2-to-scene` (PD-16), and the union stage list
|
|
23
|
+
* (PD-17: `upTo` stops after the last stage recorded at or before its position; K4 is never recorded).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type GraphSnapshot, type NodeMask } from "@graphty/graph-format";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
FA2_FLAG_ADAPTIVE,
|
|
30
|
+
FR_ADAPTIVE_MAX_ITERATIONS,
|
|
31
|
+
FR_DEFAULTS,
|
|
32
|
+
FR_REHEAT_FRACTION,
|
|
33
|
+
FR_START_TEMPERATURE,
|
|
34
|
+
MAX_ITERATIONS_PER_STEP,
|
|
35
|
+
TRACE_RECORD_BYTES,
|
|
36
|
+
UNIFORM_SLOT_BYTES,
|
|
37
|
+
} from "../constants.js";
|
|
38
|
+
import { type GpuContext } from "../context.js";
|
|
39
|
+
import { BufferUsage } from "../device/webgpu-constants.js";
|
|
40
|
+
import { WebGpuGraphError } from "../errors.js";
|
|
41
|
+
import { type CommandBatch } from "../kernel/batch.js";
|
|
42
|
+
import { type DispatchPlan, plan1d } from "../kernel/dispatch.js";
|
|
43
|
+
import { type BoundKernel, type Kernel } from "../kernel/kernel.js";
|
|
44
|
+
import { type UniformBlock, type UniformValues } from "../kernel/struct-block.js";
|
|
45
|
+
import { type WgslModuleSpec } from "../kernel/wgsl.js";
|
|
46
|
+
import { FA2_PARAMS, FA2_STATE, FA2_TRACE, FILL_PARAMS, kernelSpec } from "../kernels.js";
|
|
47
|
+
import { arcCountOf } from "../primitives/core-shape.js";
|
|
48
|
+
import { type GridSpec, gridSpecFor } from "../primitives/grid.js";
|
|
49
|
+
import {
|
|
50
|
+
type FruchtermanReingoldStats,
|
|
51
|
+
type FruchtermanReingoldTraceRecord,
|
|
52
|
+
type GpuLayoutSimulation,
|
|
53
|
+
type GpuLayoutTuning,
|
|
54
|
+
type ResolvedLayoutTuning,
|
|
55
|
+
} from "../types/layout.js";
|
|
56
|
+
import { type Binding } from "../types/memory.js";
|
|
57
|
+
import { type FruchtermanReingoldOptions, type ResolvedFruchtermanReingoldOptions } from "../types/options.js";
|
|
58
|
+
import {
|
|
59
|
+
type BufferSpec,
|
|
60
|
+
type ForceModel,
|
|
61
|
+
ForceSimulation,
|
|
62
|
+
type ModelInputs,
|
|
63
|
+
type ModelResources,
|
|
64
|
+
type StateWriter,
|
|
65
|
+
tierFor,
|
|
66
|
+
} from "./force-simulation.js";
|
|
67
|
+
import { resolveLayoutTuning, writeGridFrame } from "./forceatlas2.js";
|
|
68
|
+
import {
|
|
69
|
+
type AttractionBound,
|
|
70
|
+
bindAttraction,
|
|
71
|
+
describeValue,
|
|
72
|
+
FILL_PARAMS_BUFFER,
|
|
73
|
+
FORCE_BYTES_PER_NODE,
|
|
74
|
+
invalid,
|
|
75
|
+
isPositiveInteger,
|
|
76
|
+
type Overrides,
|
|
77
|
+
pickCenter,
|
|
78
|
+
pickDim,
|
|
79
|
+
pickNumber,
|
|
80
|
+
pickSeed,
|
|
81
|
+
recordAttraction,
|
|
82
|
+
scalar,
|
|
83
|
+
seedWord,
|
|
84
|
+
subset,
|
|
85
|
+
vector,
|
|
86
|
+
} from "./model-common.js";
|
|
87
|
+
import { type GridStage, RepulsionGrid, type RepulsionGridOverrides } from "./repulsion-grid.js";
|
|
88
|
+
|
|
89
|
+
// ============================================================ constants
|
|
90
|
+
|
|
91
|
+
/** The stage names of both tiers in dispatch order plus the per-batch toScene (spec 7.20; P4 PD-17: the union list; the exact tier records K1 K2 K3 K5, the grid tier K1 K2 G1..G7 K5, K4 never). */
|
|
92
|
+
const FR_STAGES = ["K1", "K2", "K3", "G1", "G2", "G3", "G4", "G5", "G6", "G7", "K4", "K5", "toScene"] as const;
|
|
93
|
+
|
|
94
|
+
/** The FR_STAGES index of the first grid stage, of K4, of K5 and of toScene. */
|
|
95
|
+
const STAGE_G1 = 3;
|
|
96
|
+
const STAGE_K4 = 10;
|
|
97
|
+
const STAGE_K5 = 11;
|
|
98
|
+
const STAGE_TO_SCENE = 12;
|
|
99
|
+
|
|
100
|
+
/** The name of the model-owned hub-counter buffer K1 binds on every tier (P4 PD-14). */
|
|
101
|
+
const HUB_COUNTERS_BUFFER = "hubCounters";
|
|
102
|
+
|
|
103
|
+
/** The one-workgroup dispatch of K1 (spec 7.4). */
|
|
104
|
+
const ONE_WORKGROUP: DispatchPlan = { x: 1, y: 1, z: 1, items: 1, stride: null };
|
|
105
|
+
|
|
106
|
+
/** The model's override set, constant for every option record (PD-1, PD-20). */
|
|
107
|
+
const FR_OVERRIDES: Overrides = Object.freeze({
|
|
108
|
+
LINLOG: false,
|
|
109
|
+
DISTRIBUTED: false,
|
|
110
|
+
TIER: 0,
|
|
111
|
+
SWING_MODE: 1,
|
|
112
|
+
STRONG_GRAVITY: false,
|
|
113
|
+
GRAVITY_CENTER: 0,
|
|
114
|
+
LAW: 1,
|
|
115
|
+
APPLY: 1,
|
|
116
|
+
STATS_MODE: 1,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/** The grid stage's override set (G6 / G7 / K4; P4-T13, PD-22): K3's constant three and the FR law. */
|
|
120
|
+
const FR_GRID_OVERRIDES: RepulsionGridOverrides = Object.freeze({
|
|
121
|
+
SWING_MODE: 1,
|
|
122
|
+
STRONG_GRAVITY: false,
|
|
123
|
+
GRAVITY_CENTER: 0,
|
|
124
|
+
LAW: 1,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/** Every override each kernel accepts, with its default (the names its registry entry declares). */
|
|
128
|
+
const K1_DEFAULTS: Overrides = { STATS_MODE: 0 };
|
|
129
|
+
const K2_DEFAULTS: Overrides = {
|
|
130
|
+
LINLOG: false,
|
|
131
|
+
DISTRIBUTED: false,
|
|
132
|
+
TIER: 0,
|
|
133
|
+
USE_PERM: false,
|
|
134
|
+
HAS_WEIGHTS: false,
|
|
135
|
+
LAW: 0,
|
|
136
|
+
};
|
|
137
|
+
const K3_DEFAULTS: Overrides = { SWING_MODE: 0, STRONG_GRAVITY: false, GRAVITY_CENTER: 0, LAW: 0 };
|
|
138
|
+
const K5_DEFAULTS: Overrides = { SWING_MODE: 0, APPLY: 0 };
|
|
139
|
+
|
|
140
|
+
/** The resolved record with no option given: FR_DEFAULTS plus the null / origin defaults. */
|
|
141
|
+
const DEFAULT_RESOLVED: ResolvedFruchtermanReingoldOptions = Object.freeze<ResolvedFruchtermanReingoldOptions>({
|
|
142
|
+
...FR_DEFAULTS,
|
|
143
|
+
center: [0, 0, 0],
|
|
144
|
+
seed: null,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ============================================================ the resolver
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The `k` option: undefined keeps the fallback; null, 0 and NaN mean the auto default (null); else a finite
|
|
151
|
+
* number > 0.
|
|
152
|
+
* @param given - the value given
|
|
153
|
+
* @param fallback - the previous record's value or the default
|
|
154
|
+
* @returns k or null
|
|
155
|
+
*/
|
|
156
|
+
function resolveK(given: number | null | undefined, fallback: number | null): number | null {
|
|
157
|
+
if (given === undefined) {
|
|
158
|
+
return fallback;
|
|
159
|
+
}
|
|
160
|
+
if (given === null || given === 0 || Number.isNaN(given)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return pickNumber("k", given, 1, (v) => v > 0, "> 0 or null");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The `cooling` option: undefined keeps the fallback; else one of the two schedule names.
|
|
168
|
+
* @param given - the value given
|
|
169
|
+
* @param fallback - the previous record's value or the default
|
|
170
|
+
* @returns the schedule
|
|
171
|
+
*/
|
|
172
|
+
function pickCooling(given: unknown, fallback: "linear" | "adaptive"): "linear" | "adaptive" {
|
|
173
|
+
if (given === undefined) {
|
|
174
|
+
return fallback;
|
|
175
|
+
}
|
|
176
|
+
if (given === "linear" || given === "adaptive") {
|
|
177
|
+
return given;
|
|
178
|
+
}
|
|
179
|
+
throw invalid("cooling", given, '"linear" | "adaptive"');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Applies FR_DEFAULTS to the option record and validates every range (spec 7.20, 9.3). `k`: null, 0 and NaN mean the
|
|
184
|
+
* auto default `1 / sqrt(n)` (the CPU's `if (!k)`, `layout/src/simulation/fruchterman-reingold.ts:140-152`); a
|
|
185
|
+
* negative or infinite k is E_INVALID_ARGUMENT. `iterations` is an integer >= 0 (0: settled at load); under
|
|
186
|
+
* `cooling: "adaptive"` it is only the run's cap and a fresh record without one gets FR_ADAPTIVE_MAX_ITERATIONS. `fixed` is a
|
|
187
|
+
* NodeMask, a bool node column name or null. With `previous` the record is a PATCH over it and `maxInFlight` may not
|
|
188
|
+
* change (the uniform ring is sized by it at construction).
|
|
189
|
+
* @param options - the caller's options (or a setParams patch)
|
|
190
|
+
* @param previous - the current resolved record when resolving a patch
|
|
191
|
+
* @returns the frozen resolved record
|
|
192
|
+
*/
|
|
193
|
+
export function resolveFruchtermanReingoldOptions(
|
|
194
|
+
options: FruchtermanReingoldOptions | undefined,
|
|
195
|
+
previous?: ResolvedFruchtermanReingoldOptions,
|
|
196
|
+
): ResolvedFruchtermanReingoldOptions {
|
|
197
|
+
const o: FruchtermanReingoldOptions = options ?? {};
|
|
198
|
+
const base = previous ?? DEFAULT_RESOLVED;
|
|
199
|
+
if (previous !== undefined && o.maxInFlight !== undefined && o.maxInFlight !== previous.maxInFlight) {
|
|
200
|
+
throw new WebGpuGraphError(
|
|
201
|
+
"E_INVALID_ARGUMENT",
|
|
202
|
+
`maxInFlight cannot change after creation (the uniform ring is sized by it): got ${describeValue(o.maxInFlight)}, current ${previous.maxInFlight}`,
|
|
203
|
+
{ argument: "maxInFlight", value: o.maxInFlight, expected: previous.maxInFlight },
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const fixed: unknown = o.fixed === undefined ? base.fixed : o.fixed;
|
|
207
|
+
if (fixed !== null && typeof fixed !== "string" && !(fixed instanceof Uint32Array)) {
|
|
208
|
+
throw invalid("fixed", fixed, "a NodeMask (Uint32Array), the name of a bool node column, or null");
|
|
209
|
+
}
|
|
210
|
+
const cooling = pickCooling(o.cooling, base.cooling);
|
|
211
|
+
// under adaptive cooling `iterations` is only a cap: a fresh record without one gets the adaptive budget, not the schedule's 50
|
|
212
|
+
const iterationsFallback =
|
|
213
|
+
previous === undefined && cooling === "adaptive" && o.iterations === undefined
|
|
214
|
+
? FR_ADAPTIVE_MAX_ITERATIONS
|
|
215
|
+
: base.iterations;
|
|
216
|
+
const resolved: ResolvedFruchtermanReingoldOptions = {
|
|
217
|
+
k: resolveK(o.k, base.k),
|
|
218
|
+
iterations: pickNumber(
|
|
219
|
+
"iterations",
|
|
220
|
+
o.iterations,
|
|
221
|
+
iterationsFallback,
|
|
222
|
+
(v) => Number.isInteger(v) && v >= 0,
|
|
223
|
+
"an integer >= 0",
|
|
224
|
+
),
|
|
225
|
+
cooling,
|
|
226
|
+
fixed: fixed as NodeMask | string | null,
|
|
227
|
+
dim: pickDim(o.dim, base.dim),
|
|
228
|
+
scale: pickNumber("scale", o.scale, base.scale, (v) => v > 0, "> 0"),
|
|
229
|
+
center: pickCenter(o.center, base.center),
|
|
230
|
+
seed: pickSeed(o.seed, base.seed),
|
|
231
|
+
settleThreshold: pickNumber("settleThreshold", o.settleThreshold, base.settleThreshold, (v) => v >= 0, ">= 0"),
|
|
232
|
+
settleWindow: pickNumber(
|
|
233
|
+
"settleWindow",
|
|
234
|
+
o.settleWindow,
|
|
235
|
+
base.settleWindow,
|
|
236
|
+
isPositiveInteger,
|
|
237
|
+
"an integer >= 1",
|
|
238
|
+
),
|
|
239
|
+
iterationsPerStep: pickNumber(
|
|
240
|
+
"iterationsPerStep",
|
|
241
|
+
o.iterationsPerStep,
|
|
242
|
+
base.iterationsPerStep,
|
|
243
|
+
(v) => isPositiveInteger(v) && v <= MAX_ITERATIONS_PER_STEP,
|
|
244
|
+
`an integer in [1, ${MAX_ITERATIONS_PER_STEP}]`,
|
|
245
|
+
),
|
|
246
|
+
maxInFlight: pickNumber("maxInFlight", o.maxInFlight, base.maxInFlight, isPositiveInteger, "an integer >= 1"),
|
|
247
|
+
};
|
|
248
|
+
return Object.freeze(resolved);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The fixed mask of a load (spec 7.12; the rule of `layout/src/simulation/fruchterman-reingold.ts:576-606` minus its
|
|
253
|
+
* "previous pins" clause, which ForceSimulation.load() implements by keeping the words on a same-size reload): a
|
|
254
|
+
* string names a bool node column (E_INVALID_ARGUMENT when absent or not bool); a mask is copied; null takes the
|
|
255
|
+
* role-"fixed" bool column when present, else null (leave the words as they are).
|
|
256
|
+
* @param s - the snapshot being loaded
|
|
257
|
+
* @param spec - the resolved `fixed` option
|
|
258
|
+
* @returns the mask words to apply at load, or null
|
|
259
|
+
*/
|
|
260
|
+
function resolveFixed(s: GraphSnapshot, spec: NodeMask | string | null): NodeMask | null {
|
|
261
|
+
const words = Math.ceil(s.nodeCount / 32);
|
|
262
|
+
if (typeof spec === "string") {
|
|
263
|
+
const column = s.nodes.get(spec);
|
|
264
|
+
if (column === null) {
|
|
265
|
+
throw invalid(
|
|
266
|
+
"fixed",
|
|
267
|
+
spec,
|
|
268
|
+
`the name of a bool node column (the snapshot holds no node column "${spec}")`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (column.dtype !== "bool") {
|
|
272
|
+
throw invalid("fixed", spec, `the name of a bool node column ("${spec}" is ${column.dtype})`);
|
|
273
|
+
}
|
|
274
|
+
return new Uint32Array(column.data.subarray(0, words));
|
|
275
|
+
}
|
|
276
|
+
if (spec !== null) {
|
|
277
|
+
if (spec.length < words) {
|
|
278
|
+
throw invalid("fixed", spec.length, `a mask of at least ${words} words for ${s.nodeCount} nodes`);
|
|
279
|
+
}
|
|
280
|
+
return new Uint32Array(spec.subarray(0, words));
|
|
281
|
+
}
|
|
282
|
+
const byRole = s.nodes.byRole("fixed");
|
|
283
|
+
if (byRole !== null && byRole.dtype === "bool") {
|
|
284
|
+
return new Uint32Array(byRole.data.subarray(0, words));
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ============================================================ the model
|
|
290
|
+
|
|
291
|
+
/** Everything bind() produced for one load(): the kernels, their bind groups and the dispatch plans of this n. */
|
|
292
|
+
interface BoundModel {
|
|
293
|
+
readonly n: number;
|
|
294
|
+
/** plan1d(n): K2, K3, K5, toScene. */
|
|
295
|
+
readonly plan: DispatchPlan;
|
|
296
|
+
/** plan1d(3n): the fill of force (3 words per node). */
|
|
297
|
+
readonly fillPlan: DispatchPlan;
|
|
298
|
+
readonly k1: Kernel;
|
|
299
|
+
readonly k1Bound: BoundKernel;
|
|
300
|
+
/** The K2 tier dispatches (P4 PD-7); null when arcCount === 0 (K2 is not recorded; the fill below zeroes force instead, spec 7.5). */
|
|
301
|
+
readonly attraction: AttractionBound | null;
|
|
302
|
+
/** K3 and its bind group, or null on the grid tier (P4 PD-18): one tier's kernels compile per load. */
|
|
303
|
+
readonly k3: Kernel | null;
|
|
304
|
+
readonly k3Bound: BoundKernel | null;
|
|
305
|
+
/** The grid-tier stage (G1-G7), or null on the exact tier (P4 PD-18). */
|
|
306
|
+
readonly grid: RepulsionGrid | null;
|
|
307
|
+
readonly k5: Kernel;
|
|
308
|
+
readonly k5Bound: BoundKernel;
|
|
309
|
+
readonly toScene: Kernel;
|
|
310
|
+
readonly toSceneBound: BoundKernel;
|
|
311
|
+
readonly fill: Kernel;
|
|
312
|
+
/** The fill of `force` (arcCount === 0 only). */
|
|
313
|
+
readonly fillForceBound: BoundKernel | null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The Fruchterman-Reingold model (spec 7.20: K1 K2 K3 K5 per iteration on the exact tier, K1 K2 G1..G7 K5 on the grid tier; toScene once per batch). Stages: the union list of PD-17. */
|
|
317
|
+
export class FruchtermanReingoldModel implements ForceModel<FruchtermanReingoldOptions, FruchtermanReingoldStats> {
|
|
318
|
+
/** The model kind of spec 7.19. */
|
|
319
|
+
readonly kind = "fruchtermanReingold";
|
|
320
|
+
/** The stage names in dispatch order (the `upTo` vocabulary of recordIteration and debugRunStages). */
|
|
321
|
+
readonly stages: typeof FR_STAGES = FR_STAGES;
|
|
322
|
+
/** Fa2Params: the per-iteration uniform block (the simulation writes the shared fields into it). */
|
|
323
|
+
readonly params: UniformBlock = FA2_PARAMS;
|
|
324
|
+
/** Fa2State: the state header block. */
|
|
325
|
+
readonly state: UniformBlock = FA2_STATE;
|
|
326
|
+
/** Fa2Trace: one record per iteration of a batch. */
|
|
327
|
+
readonly trace: UniformBlock = FA2_TRACE;
|
|
328
|
+
/** The resolved GPU-only tuning this model was created with. */
|
|
329
|
+
readonly tuning: ResolvedLayoutTuning;
|
|
330
|
+
|
|
331
|
+
/** The option record the model holds: the constructor's record, replaced by onSetParams() ONLY (as FA2's). */
|
|
332
|
+
private current: ResolvedFruchtermanReingoldOptions;
|
|
333
|
+
/** The global iteration index at which the temperature index is 0 (PD-5). */
|
|
334
|
+
private tempOrigin = 0;
|
|
335
|
+
/** Armed by onReheat(); consumed by the next paramsFor(), which re-anchors tempOrigin (PD-5). */
|
|
336
|
+
private pendingReheat = false;
|
|
337
|
+
private resources: ModelResources | null = null;
|
|
338
|
+
private bound: BoundModel | null = null;
|
|
339
|
+
/** The grid of the load inputs() last resolved (null on the exact tier): onLoad() writes its frame, specs() lists its kernels. */
|
|
340
|
+
private nextGrid: GridSpec | null = null;
|
|
341
|
+
/** The K1-K5 compute pass of the batch being recorded, keyed by CommandBatch.id (one pass per batch, contract 4.4). */
|
|
342
|
+
private openPass: { readonly id: number; readonly pass: GPUComputePassEncoder } | null = null;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Creates the model for one simulation.
|
|
346
|
+
* @param tuning - the resolved GPU-only tuning (the tier rule; compat has no effect on this model)
|
|
347
|
+
* @param resolved - the resolved option record at creation
|
|
348
|
+
*/
|
|
349
|
+
constructor(tuning: ResolvedLayoutTuning, resolved: ResolvedFruchtermanReingoldOptions) {
|
|
350
|
+
this.tuning = tuning;
|
|
351
|
+
this.current = resolved;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* force 12n and oldForce 12n (zeroed; bound and never read, PD-2), the 256-byte FillParams uniform buffer the
|
|
356
|
+
* fill dispatch reads, the 16-byte `hubCounters` K1 binds on every tier (P4 PD-14), and the grid buffers of
|
|
357
|
+
* `RepulsionGrid.buffers` exactly when `tierFor(tuning, n)` is the grid tier (PD-18). n = 0 reports one node's
|
|
358
|
+
* worth of bytes so no zero-length buffer is ever created.
|
|
359
|
+
* @param n - the node count
|
|
360
|
+
* @param dim - the layout dimension (the force arrays are stride 3 in both; the grid's geometry differs)
|
|
361
|
+
* @returns the model-owned buffer specs
|
|
362
|
+
*/
|
|
363
|
+
buffers(n: number, dim: 2 | 3): readonly BufferSpec[] {
|
|
364
|
+
const bytes = Math.max(1, n) * FORCE_BYTES_PER_NODE;
|
|
365
|
+
const usage = BufferUsage.STORAGE | BufferUsage.COPY_SRC | BufferUsage.COPY_DST;
|
|
366
|
+
const grid =
|
|
367
|
+
tierFor(this.tuning, n) === "grid" ? RepulsionGrid.buffers(n, gridSpecFor(n, dim, this.tuning)) : [];
|
|
368
|
+
return [
|
|
369
|
+
{ name: "force", byteLength: bytes, usage, zero: true },
|
|
370
|
+
{ name: "oldForce", byteLength: bytes, usage, zero: true },
|
|
371
|
+
{
|
|
372
|
+
name: FILL_PARAMS_BUFFER,
|
|
373
|
+
byteLength: UNIFORM_SLOT_BYTES,
|
|
374
|
+
usage: BufferUsage.UNIFORM | BufferUsage.COPY_DST,
|
|
375
|
+
zero: false,
|
|
376
|
+
},
|
|
377
|
+
{ name: HUB_COUNTERS_BUFFER, byteLength: 16, usage, zero: true },
|
|
378
|
+
...grid,
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Mass 1 for every node and no weights (PD-11: FR has no mass and ignores weights), the fixed mask of the
|
|
384
|
+
* `fixed` option applied at load (PD-6). Also remembers the grid of this load (`tierFor(tuning, n)`, spec 7.8)
|
|
385
|
+
* for onLoad() and specs(): the simulation calls inputs() first, then onLoad() before bind().
|
|
386
|
+
* @param s - the snapshot being loaded
|
|
387
|
+
* @param options - the simulation's current option record
|
|
388
|
+
* @returns the per-load inputs
|
|
389
|
+
*/
|
|
390
|
+
inputs(s: GraphSnapshot, options: FruchtermanReingoldOptions): ModelInputs {
|
|
391
|
+
const resolved = resolveFruchtermanReingoldOptions(options, this.current);
|
|
392
|
+
// resolve first: a throwing mask resolution leaves the remembered grid of the previous load intact
|
|
393
|
+
const inputs: ModelInputs = {
|
|
394
|
+
mass: new Float32Array(s.nodeCount).fill(1),
|
|
395
|
+
weights: { data: null, source: "none", column: null },
|
|
396
|
+
fixed: resolveFixed(s, resolved.fixed),
|
|
397
|
+
};
|
|
398
|
+
const n = s.nodeCount;
|
|
399
|
+
this.nextGrid = tierFor(this.tuning, n) === "grid" ? gridSpecFor(n, resolved.dim, this.tuning) : null;
|
|
400
|
+
return inputs;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* The constant FR override set (PD-1): no option changes a law, so setParams never recompiles.
|
|
405
|
+
* @param _options - an option record (unused: the set is constant)
|
|
406
|
+
* @returns the model's own override set
|
|
407
|
+
*/
|
|
408
|
+
overrides(_options: FruchtermanReingoldOptions): Overrides {
|
|
409
|
+
return FR_OVERRIDES;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The six module specs of an override set in dispatch order -- K1, K2, K3, K5, toScene, fill -- each with only the
|
|
414
|
+
* override names its entry declares (K2 also USE_PERM / HAS_WEIGHTS), for warm() and the compile matrix,
|
|
415
|
+
* followed by the grid tier's specs (`RepulsionGrid.specs` under FR_GRID_OVERRIDES) when the load inputs() last
|
|
416
|
+
* resolved is a grid load (the pipeline key carries no geometry).
|
|
417
|
+
* @param overrides - the merged override set (the model's plus USE_PERM / HAS_WEIGHTS)
|
|
418
|
+
* @param _subgroups - accepted for the ForceModel interface and unused (the composer picks the twin from caps)
|
|
419
|
+
* @returns the specs
|
|
420
|
+
*/
|
|
421
|
+
specs(overrides: Overrides, _subgroups: boolean): readonly WgslModuleSpec[] {
|
|
422
|
+
const grid = this.nextGrid === null ? [] : RepulsionGrid.specs(FR_GRID_OVERRIDES, this.nextGrid);
|
|
423
|
+
return [
|
|
424
|
+
kernelSpec("fa2-stats-finalize", subset(overrides, K1_DEFAULTS)),
|
|
425
|
+
kernelSpec("fa2-attraction", subset(overrides, K2_DEFAULTS)),
|
|
426
|
+
kernelSpec("fa2-repulsion-exact", subset(overrides, K3_DEFAULTS)),
|
|
427
|
+
kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)),
|
|
428
|
+
kernelSpec("fa2-to-scene"),
|
|
429
|
+
kernelSpec("fill"),
|
|
430
|
+
...grid,
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Compiles (through the cache) and binds every kernel against the buffers of this load(): K1, K2 over the degree
|
|
436
|
+
* tiers through bindAttraction (or the fill of force when arcCount === 0), K3 on the exact tier or G1-G7 through
|
|
437
|
+
* RepulsionGrid on the grid tier (PD-18), K5, toScene; writes the FillParams { count: 3n, value: 0, mode: 0 }
|
|
438
|
+
* into the model's uniform buffer. With n === 0 nothing is bound. The K2 TIER 1 / 2 pipelines compile on the
|
|
439
|
+
* first load whose degrees need them (P4 PD-7).
|
|
440
|
+
* @param resources - the graph, the shared and model buffers, the ring and the cache
|
|
441
|
+
* @param overrides - the merged override set
|
|
442
|
+
*/
|
|
443
|
+
async bind(resources: ModelResources, overrides: Overrides): Promise<void> {
|
|
444
|
+
this.dropBound();
|
|
445
|
+
this.resources = resources;
|
|
446
|
+
const { n, pipelines, caps, core, ring, device } = resources;
|
|
447
|
+
if (n === 0) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const pos = resources.buffer("positions");
|
|
451
|
+
const force = resources.buffer("force");
|
|
452
|
+
const params = ring.binding(FA2_PARAMS);
|
|
453
|
+
const hasArcs = core.colIdx !== null;
|
|
454
|
+
// sequential on purpose: PipelineCache.get compiles inside a validation scope, one stack per device
|
|
455
|
+
const k1 = await pipelines.kernel(kernelSpec("fa2-stats-finalize", subset(overrides, K1_DEFAULTS)));
|
|
456
|
+
const attraction = hasArcs
|
|
457
|
+
? await bindAttraction(resources, subset(overrides, K2_DEFAULTS), { pos, force, params })
|
|
458
|
+
: null;
|
|
459
|
+
const k3 =
|
|
460
|
+
resources.tier === "grid"
|
|
461
|
+
? null
|
|
462
|
+
: await pipelines.kernel(kernelSpec("fa2-repulsion-exact", subset(overrides, K3_DEFAULTS)));
|
|
463
|
+
const k5 = await pipelines.kernel(kernelSpec("fa2-integrate", subset(overrides, K5_DEFAULTS)));
|
|
464
|
+
const toScene = await pipelines.kernel(kernelSpec("fa2-to-scene"));
|
|
465
|
+
const fill = await pipelines.kernel(kernelSpec("fill"));
|
|
466
|
+
const grid =
|
|
467
|
+
resources.tier === "grid"
|
|
468
|
+
? await RepulsionGrid.create(
|
|
469
|
+
resources,
|
|
470
|
+
k1.workgroupSize,
|
|
471
|
+
FR_GRID_OVERRIDES,
|
|
472
|
+
gridSpecFor(n, resources.dim, this.tuning),
|
|
473
|
+
)
|
|
474
|
+
: null;
|
|
475
|
+
if (this.resources !== resources) {
|
|
476
|
+
// a newer bind() superseded this one while the pipelines compiled; its own bind groups stand
|
|
477
|
+
grid?.dispose();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const scene = resources.buffer("scenePositions");
|
|
481
|
+
const fixed = resources.buffer("fixed");
|
|
482
|
+
const partials = resources.buffer("partials");
|
|
483
|
+
const state = resources.buffer("state");
|
|
484
|
+
const trace = resources.buffer("trace");
|
|
485
|
+
const oldForce = resources.buffer("oldForce");
|
|
486
|
+
const fillParamsBuffer = resources.buffer(FILL_PARAMS_BUFFER);
|
|
487
|
+
const fillParams: Binding = {
|
|
488
|
+
buffer: fillParamsBuffer.buffer,
|
|
489
|
+
offset: fillParamsBuffer.offset,
|
|
490
|
+
size: FILL_PARAMS.byteLength,
|
|
491
|
+
window: null,
|
|
492
|
+
};
|
|
493
|
+
const fillBytes = new ArrayBuffer(FILL_PARAMS.byteLength);
|
|
494
|
+
FILL_PARAMS.write(new DataView(fillBytes), { count: 3 * n, value: 0, mode: 0 });
|
|
495
|
+
device.queue.writeBuffer(fillParamsBuffer.buffer, fillParamsBuffer.offset, fillBytes);
|
|
496
|
+
const hubCounters = resources.buffer(HUB_COUNTERS_BUFFER);
|
|
497
|
+
grid?.bind({
|
|
498
|
+
pos,
|
|
499
|
+
state,
|
|
500
|
+
trace,
|
|
501
|
+
force,
|
|
502
|
+
oldForce,
|
|
503
|
+
fixedMask: fixed,
|
|
504
|
+
partials,
|
|
505
|
+
params,
|
|
506
|
+
cellKey: resources.buffer("cellKey"),
|
|
507
|
+
cellVal: resources.buffer("cellVal"),
|
|
508
|
+
sortedKey: resources.buffer("sortedKey"),
|
|
509
|
+
sortedIdx: resources.buffer("sortedIdx"),
|
|
510
|
+
cellHist: resources.buffer("cellHist"),
|
|
511
|
+
cellStart: resources.buffer("cellStart"),
|
|
512
|
+
hubList: resources.buffer("hubList"),
|
|
513
|
+
hubCounters,
|
|
514
|
+
hubArgs: resources.buffer("hubArgs"),
|
|
515
|
+
pyramid: resources.buffer("pyramid"),
|
|
516
|
+
});
|
|
517
|
+
const wg = k1.workgroupSize;
|
|
518
|
+
this.bound = {
|
|
519
|
+
n,
|
|
520
|
+
plan: plan1d(n, wg, caps),
|
|
521
|
+
fillPlan: plan1d(3 * n, wg, caps),
|
|
522
|
+
k1,
|
|
523
|
+
// PD-14: on the exact tier K1's cellHist slot takes a dummy (partials, both read-only) and the block is
|
|
524
|
+
// dead under gridMax 0; hubCounters is the model's 16-byte buffer on every tier
|
|
525
|
+
k1Bound: k1.bind({
|
|
526
|
+
partials,
|
|
527
|
+
S: state,
|
|
528
|
+
T: trace,
|
|
529
|
+
cellHist: grid === null ? partials : resources.buffer("cellHist"),
|
|
530
|
+
hubCounters,
|
|
531
|
+
P: params,
|
|
532
|
+
}),
|
|
533
|
+
attraction,
|
|
534
|
+
k3,
|
|
535
|
+
k3Bound: k3?.bind({ pos, S: state, force, oldForce, fixedMask: fixed, partials, P: params }) ?? null,
|
|
536
|
+
grid,
|
|
537
|
+
k5,
|
|
538
|
+
k5Bound: k5.bind({ force, oldForce, fixedMask: fixed, S: state, pos, partials, P: params }),
|
|
539
|
+
toScene,
|
|
540
|
+
toSceneBound: toScene.bind({ pos, scene, P: params }),
|
|
541
|
+
fill,
|
|
542
|
+
fillForceBound: hasArcs ? null : fill.bind({ dst: force, P: fillParams }),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The Fa2Params values of one iteration: the FA2 fields at their neutral values (scalingRatio, gravity and
|
|
548
|
+
* jitterTolerance 0), `frK` = k or 1 / sqrt(n), and this iteration's temperature (PD-5 anchors here). Under
|
|
549
|
+
* `cooling: "adaptive"` the flags carry FA2_FLAG_ADAPTIVE and the kernels take the temperature from the state
|
|
550
|
+
* block instead (K1 updates it, K5 reads it); the uniform's value is then the start temperature and unused. The
|
|
551
|
+
* simulation overwrites the shared fields with the same values and OR-s in its own flags.
|
|
552
|
+
* @param iteration - the GLOBAL iteration index
|
|
553
|
+
* @param options - the simulation's current option record
|
|
554
|
+
* @returns the uniform values
|
|
555
|
+
*/
|
|
556
|
+
paramsFor(iteration: number, options: FruchtermanReingoldOptions): UniformValues {
|
|
557
|
+
const { n, core, tiers, tier, dim } = this.requireResources();
|
|
558
|
+
const resolved = resolveFruchtermanReingoldOptions(options, this.current);
|
|
559
|
+
const { nearMax, extentFactor } = this.tuning;
|
|
560
|
+
const adaptive = resolved.cooling === "adaptive";
|
|
561
|
+
const grid = tier === "grid" ? gridSpecFor(n, dim, this.tuning) : null;
|
|
562
|
+
// P4 PD-7: TIER 2 reads [0, hiEnd), TIER 1 [hiEnd, midEnd), TIER 0 [tierStart, tierEnd) = [midEnd, n)
|
|
563
|
+
const so = tiers?.segmentOffsets;
|
|
564
|
+
const hiEnd = so?.[1] ?? 0;
|
|
565
|
+
const midEnd = so?.[2] ?? 0;
|
|
566
|
+
return {
|
|
567
|
+
n,
|
|
568
|
+
dim: resolved.dim,
|
|
569
|
+
flags: adaptive ? FA2_FLAG_ADAPTIVE : 0,
|
|
570
|
+
tierStart: midEnd,
|
|
571
|
+
tierEnd: n,
|
|
572
|
+
iterationIndex: iteration,
|
|
573
|
+
seed: seedWord(resolved.seed),
|
|
574
|
+
nearMax,
|
|
575
|
+
scalingRatio: 0,
|
|
576
|
+
gravity: 0,
|
|
577
|
+
jitterTolerance: 0,
|
|
578
|
+
scale: resolved.scale,
|
|
579
|
+
center: [resolved.center[0], resolved.center[1], resolved.center[2], 0],
|
|
580
|
+
settleThreshold: resolved.settleThreshold,
|
|
581
|
+
extentFactor,
|
|
582
|
+
gridMax: grid?.g ?? 0,
|
|
583
|
+
levels: grid?.levels ?? 0,
|
|
584
|
+
arcBase: 0,
|
|
585
|
+
arcEnd: arcCountOf(core),
|
|
586
|
+
accumulate: 0,
|
|
587
|
+
hiEnd,
|
|
588
|
+
midEnd,
|
|
589
|
+
frK: resolved.k ?? 1 / Math.sqrt(n),
|
|
590
|
+
temperature: adaptive ? FR_START_TEMPERATURE : this.temperatureAt(iteration, resolved),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Records one iteration into the batch, stopping after stage `upTo` when given (PD-17: `upTo` names a position
|
|
596
|
+
* in the union list and the recording stops after the last stage recorded at or before it, so "K3" on the grid
|
|
597
|
+
* tier stops after K2 and "G5" on the exact tier after K3). The exact tier: K1, K2 (or the fill of force when
|
|
598
|
+
* arcCount === 0), K3, K5 in the batch's ONE compute pass (opened by the first call of a batch and reused by
|
|
599
|
+
* every later call with the same batch.id), then toScene in a second pass that ends it. The grid tier (PD-16):
|
|
600
|
+
* the passes `fr-k1` (K1), `fr-attraction` (K2's tiers) and `fr-grid` (G1-G7, K5) per iteration, then
|
|
601
|
+
* `fa2-to-scene`. With n === 0 nothing is recorded; a call before bind() completed is E_NOT_LOADED (never a
|
|
602
|
+
* silent no-op).
|
|
603
|
+
* @param batch - the batch being recorded
|
|
604
|
+
* @param slot - the UniformRing slot holding this iteration's Fa2Params
|
|
605
|
+
* @param tier - the tier the simulation resolved at load() (the same rule bind() applied, PD-18)
|
|
606
|
+
* @param upTo - a stage name to stop after; undefined records every stage including toScene
|
|
607
|
+
*/
|
|
608
|
+
recordIteration(batch: CommandBatch, slot: number, tier: "exact" | "grid", upTo?: string): void {
|
|
609
|
+
const resources = this.requireResources();
|
|
610
|
+
const stop = upTo === undefined ? STAGE_TO_SCENE : this.stageIndex(upTo);
|
|
611
|
+
const { bound } = this;
|
|
612
|
+
if (bound === null) {
|
|
613
|
+
if (resources.n === 0) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
throw new WebGpuGraphError(
|
|
617
|
+
"E_NOT_LOADED",
|
|
618
|
+
"the Fruchterman-Reingold model is not bound (bind() has not completed)",
|
|
619
|
+
{ state: "loaded" },
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
const offset = resources.ring.offsetOf(slot);
|
|
623
|
+
if (tier === "grid") {
|
|
624
|
+
this.recordGridIteration(batch, bound, offset, stop);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const { k3, k3Bound } = bound;
|
|
628
|
+
if (k3 === null || k3Bound === null) {
|
|
629
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the Fruchterman-Reingold model was bound on the grid tier", {
|
|
630
|
+
state: "loaded",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const pass = this.openPass !== null && this.openPass.id === batch.id ? this.openPass.pass : batch.pass("fr");
|
|
634
|
+
this.openPass = { id: batch.id, pass };
|
|
635
|
+
bound.k1.dispatch(pass, bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
636
|
+
if (stop < 1) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
this.recordK2(pass, bound, offset);
|
|
640
|
+
if (stop < 2) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
k3.dispatch(pass, k3Bound, bound.plan, [offset]);
|
|
644
|
+
if (stop < STAGE_K5) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
648
|
+
if (stop < STAGE_TO_SCENE) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.recordToScene(batch, bound, offset);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* The grid tier's iteration (PD-16): three compute passes before toScene; no K4.
|
|
656
|
+
* @param batch - the batch being recorded
|
|
657
|
+
* @param bound - the bound model
|
|
658
|
+
* @param offset - the Fa2Params dynamic offset of the iteration
|
|
659
|
+
* @param stop - the FR_STAGES index to stop after
|
|
660
|
+
*/
|
|
661
|
+
private recordGridIteration(batch: CommandBatch, bound: BoundModel, offset: number, stop: number): void {
|
|
662
|
+
const { grid } = bound;
|
|
663
|
+
if (grid === null) {
|
|
664
|
+
throw new WebGpuGraphError("E_NOT_LOADED", "the Fruchterman-Reingold model was bound on the exact tier", {
|
|
665
|
+
state: "loaded",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
this.openPass = null;
|
|
669
|
+
bound.k1.dispatch(batch.pass("fr-k1"), bound.k1Bound, ONE_WORKGROUP, [offset]);
|
|
670
|
+
if (stop < 1) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.recordK2(batch.pass("fr-attraction"), bound, offset);
|
|
674
|
+
if (stop < STAGE_G1) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const pass = batch.pass("fr-grid");
|
|
678
|
+
const gridStop = stop < STAGE_K4 ? (FR_STAGES[stop] as GridStage) : undefined;
|
|
679
|
+
grid.recordRepulsion(pass, bound.n, offset, gridStop);
|
|
680
|
+
if (stop < STAGE_K5) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
bound.k5.dispatch(pass, bound.k5Bound, bound.plan, [offset]);
|
|
684
|
+
if (stop < STAGE_TO_SCENE) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
this.recordToScene(batch, bound, offset);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* K2's tier dispatches, or the fill of force when the graph has no arcs (spec 7.5).
|
|
692
|
+
* @param pass - the open compute pass
|
|
693
|
+
* @param bound - the bound model
|
|
694
|
+
* @param offset - the Fa2Params dynamic offset
|
|
695
|
+
*/
|
|
696
|
+
private recordK2(pass: GPUComputePassEncoder, bound: BoundModel, offset: number): void {
|
|
697
|
+
if (bound.attraction !== null) {
|
|
698
|
+
recordAttraction(pass, bound.attraction, offset);
|
|
699
|
+
} else if (bound.fillForceBound !== null) {
|
|
700
|
+
bound.fill.dispatch(pass, bound.fillForceBound, bound.fillPlan, [0]);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* The toScene pass that ends the iteration's pass; the batch is complete after it, so nothing reuses the pass.
|
|
706
|
+
* @param batch - the batch
|
|
707
|
+
* @param bound - the bound model
|
|
708
|
+
* @param offset - the Fa2Params dynamic offset
|
|
709
|
+
*/
|
|
710
|
+
private recordToScene(batch: CommandBatch, bound: BoundModel, offset: number): void {
|
|
711
|
+
this.openPass = null;
|
|
712
|
+
const scenePass = batch.pass("fa2-to-scene");
|
|
713
|
+
bound.toScene.dispatch(scenePass, bound.toSceneBound, bound.plan, [offset]);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* temperature = 0.1 (what stats reads before the first batch lands), kineticEnergy = 0; the temperature index
|
|
718
|
+
* restarts at 0 (PD-5). On a grid load the frame of the first build (K1 folds nothing on the first iteration).
|
|
719
|
+
* @param state - the state writer of the simulation
|
|
720
|
+
*/
|
|
721
|
+
onLoad(state: StateWriter): void {
|
|
722
|
+
this.resetAdaptive(state);
|
|
723
|
+
state.set("kineticEnergy", 0);
|
|
724
|
+
this.tempOrigin = 0;
|
|
725
|
+
this.pendingReheat = false;
|
|
726
|
+
if (this.nextGrid !== null) {
|
|
727
|
+
writeGridFrame(state, this.nextGrid, this.tuning.extentFactor);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* temperature = 0.1, the adaptive controller's energy = +infinity (the first fold always counts as a fall) and
|
|
733
|
+
* its progress counter = 0: the state K1 reads under `cooling: "adaptive"`, and what stats reads before the first
|
|
734
|
+
* batch lands under either schedule.
|
|
735
|
+
* @param state - the state writer of the simulation
|
|
736
|
+
*/
|
|
737
|
+
private resetAdaptive(state: StateWriter): void {
|
|
738
|
+
state.set("temperature", FR_START_TEMPERATURE);
|
|
739
|
+
state.set("frEnergy", Number.POSITIVE_INFINITY);
|
|
740
|
+
state.set("frProgress", 0);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Arms the re-anchoring of the temperature index at floor(0.7 * iterations), placed by the next paramsFor (PD-5);
|
|
745
|
+
* under `cooling: "adaptive"` restarts the controller at the start temperature instead.
|
|
746
|
+
* @param state - the state writer of the simulation
|
|
747
|
+
*/
|
|
748
|
+
onReheat(state: StateWriter): void {
|
|
749
|
+
this.pendingReheat = true;
|
|
750
|
+
if (this.current.cooling === "adaptive") {
|
|
751
|
+
this.resetAdaptive(state);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Replaces the record (a new k or budget takes effect at the next paramsFor: a new dt, a new frK; no recompile).
|
|
757
|
+
* The PD-16 `fixed` check already ran in the simulation's resolve callback.
|
|
758
|
+
* @param patch - the setParams patch
|
|
759
|
+
* @param _state - the state writer of the simulation (nothing to write)
|
|
760
|
+
*/
|
|
761
|
+
onSetParams(patch: Partial<FruchtermanReingoldOptions>, _state: StateWriter): void {
|
|
762
|
+
this.current = resolveFruchtermanReingoldOptions(patch, this.current);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Decodes the state header and the k trace records of a completed batch into FruchtermanReingoldStats:
|
|
767
|
+
* `repulsionTier` is the bound tier, the grid fields are the header's on the grid tier and null on the exact
|
|
768
|
+
* tier, msPerIteration null (the simulation owns the clock), the temperature K1 wrote.
|
|
769
|
+
* @param state - a DataView over the 256-byte state header
|
|
770
|
+
* @param trace - a DataView over the k Fa2Trace records of the batch
|
|
771
|
+
* @returns the stats
|
|
772
|
+
*/
|
|
773
|
+
readStats(state: DataView, trace: DataView): FruchtermanReingoldStats {
|
|
774
|
+
const header = FA2_STATE.read(state);
|
|
775
|
+
const centroid = vector(header, "centroid");
|
|
776
|
+
const records: FruchtermanReingoldTraceRecord[] = [];
|
|
777
|
+
const count = Math.floor(trace.byteLength / TRACE_RECORD_BYTES);
|
|
778
|
+
for (let i = 0; i < count; i++) {
|
|
779
|
+
const record = FA2_TRACE.read(trace, i * TRACE_RECORD_BYTES);
|
|
780
|
+
records.push({
|
|
781
|
+
temperature: scalar(record, "modelScalar"),
|
|
782
|
+
meanDisplacement: scalar(record, "meanDisplacement"),
|
|
783
|
+
settledCount: scalar(record, "settledCount"),
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
const grid = this.resources?.tier === "grid";
|
|
787
|
+
return {
|
|
788
|
+
iteration: scalar(header, "iteration"),
|
|
789
|
+
meanDisplacement: scalar(header, "meanDisplacement"),
|
|
790
|
+
rmsRadius: scalar(header, "rmsRadius"),
|
|
791
|
+
layoutRadius: scalar(header, "radius"),
|
|
792
|
+
centroid: [centroid[0], centroid[1], centroid[2]],
|
|
793
|
+
repulsionTier: grid ? "grid" : "exact",
|
|
794
|
+
maxCellOccupancy: grid ? scalar(header, "maxCellOccupancy") : null,
|
|
795
|
+
outsideGrid: grid ? scalar(header, "outsideGrid") : null,
|
|
796
|
+
msPerIteration: null,
|
|
797
|
+
temperature: scalar(header, "temperature"),
|
|
798
|
+
trace: records,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* The temperature of a global iteration (PD-5): a pending reheat re-anchors tempOrigin so this iteration's
|
|
804
|
+
* temperature index is floor(0.7 * iterations); then max(0, 0.1 - dt * index), dt = 0.1 / (iterations + 1).
|
|
805
|
+
* @param global - the global iteration index
|
|
806
|
+
* @param resolved - the current record
|
|
807
|
+
* @returns the temperature (f64; the uniform slot rounds it to f32)
|
|
808
|
+
*/
|
|
809
|
+
private temperatureAt(global: number, resolved: ResolvedFruchtermanReingoldOptions): number {
|
|
810
|
+
if (this.pendingReheat) {
|
|
811
|
+
this.tempOrigin = global - Math.floor(FR_REHEAT_FRACTION * resolved.iterations);
|
|
812
|
+
this.pendingReheat = false;
|
|
813
|
+
}
|
|
814
|
+
const dt = FR_START_TEMPERATURE / (resolved.iterations + 1);
|
|
815
|
+
return Math.max(0, FR_START_TEMPERATURE - dt * (global - this.tempOrigin));
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The resources of the last bind(), or E_NOT_LOADED before it.
|
|
820
|
+
* @returns the resources
|
|
821
|
+
*/
|
|
822
|
+
private requireResources(): ModelResources {
|
|
823
|
+
if (this.resources === null) {
|
|
824
|
+
throw new WebGpuGraphError(
|
|
825
|
+
"E_NOT_LOADED",
|
|
826
|
+
"the Fruchterman-Reingold model has not been bound (load() first)",
|
|
827
|
+
{ state: "created" },
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
return this.resources;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* The index of a stage name in FR_STAGES, or E_INVALID_ARGUMENT.
|
|
835
|
+
* @param upTo - the stage name
|
|
836
|
+
* @returns its index
|
|
837
|
+
*/
|
|
838
|
+
private stageIndex(upTo: string): number {
|
|
839
|
+
for (let i = 0; i < FR_STAGES.length; i++) {
|
|
840
|
+
if (FR_STAGES[i] === upTo) {
|
|
841
|
+
return i;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
throw invalid("upTo", upTo, FR_STAGES.join(" | "));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/** Releases the grid stage's lease and the bind groups (the simulation calls it from dispose() once every in-flight batch has settled). */
|
|
848
|
+
dispose(): void {
|
|
849
|
+
this.dropBound();
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Drops the bind groups of the previous bind() (the buffers changed), releases the grid stage's lease and forgets the pass of a batch recorded before the rebind. */
|
|
853
|
+
private dropBound(): void {
|
|
854
|
+
this.openPass = null;
|
|
855
|
+
const { bound } = this;
|
|
856
|
+
if (bound === null) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
for (const kernel of [bound.k1, bound.k5, bound.toScene, bound.fill]) {
|
|
860
|
+
kernel.invalidate();
|
|
861
|
+
}
|
|
862
|
+
bound.k3?.invalidate();
|
|
863
|
+
for (const [kernel] of bound.attraction?.kernels ?? []) {
|
|
864
|
+
kernel.invalidate();
|
|
865
|
+
}
|
|
866
|
+
bound.grid?.dispose();
|
|
867
|
+
this.bound = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ============================================================ the factory
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* The resolve callback of the simulation's setParams: the PD-16 `fixed` check first (fixed is applied at load; a
|
|
875
|
+
* rejected patch leaves the simulation unchanged because ForceSimulation.setParams runs this before any state moves),
|
|
876
|
+
* then the patch over the current record, re-validated.
|
|
877
|
+
* @param patch - the setParams patch
|
|
878
|
+
* @param current - the simulation's current option record
|
|
879
|
+
* @returns the new record
|
|
880
|
+
*/
|
|
881
|
+
function resolvePatch(
|
|
882
|
+
patch: Partial<FruchtermanReingoldOptions>,
|
|
883
|
+
current: FruchtermanReingoldOptions,
|
|
884
|
+
): FruchtermanReingoldOptions {
|
|
885
|
+
if ("fixed" in patch) {
|
|
886
|
+
throw new WebGpuGraphError("E_INVALID_ARGUMENT", "setParams({ fixed }) is not a live option (spec 7.12)", {
|
|
887
|
+
argument: "fixed",
|
|
888
|
+
value: patch.fixed,
|
|
889
|
+
expected: "absent",
|
|
890
|
+
hint: "use setFixed(mask); fixed is applied at load()",
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return resolveFruchtermanReingoldOptions(patch, resolveFruchtermanReingoldOptions(current));
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Spec 3.3 createFruchtermanReingold, verbatim: a GpuLayoutSimulation running Fruchterman-Reingold on the exact or
|
|
898
|
+
* the grid repulsion tier (spec 7.8) with the option defaults of spec 7.20 and the GPU-only tuning of GpuLayoutTuning.
|
|
899
|
+
* @param ctx - the context (E_DISPOSED / E_DEVICE_LOST through assertReady)
|
|
900
|
+
* @param options - the Fruchterman-Reingold options and the GPU-only tuning knobs in one record
|
|
901
|
+
* @returns the simulation in state "created"; load() next
|
|
902
|
+
*/
|
|
903
|
+
export function createFruchtermanReingold(
|
|
904
|
+
ctx: GpuContext,
|
|
905
|
+
options?: FruchtermanReingoldOptions & GpuLayoutTuning,
|
|
906
|
+
): GpuLayoutSimulation<FruchtermanReingoldOptions, FruchtermanReingoldStats> {
|
|
907
|
+
ctx.assertReady();
|
|
908
|
+
const resolved = resolveFruchtermanReingoldOptions(options);
|
|
909
|
+
const tuning = resolveLayoutTuning(options);
|
|
910
|
+
const model = new FruchtermanReingoldModel(tuning, resolved);
|
|
911
|
+
return new ForceSimulation<FruchtermanReingoldOptions, FruchtermanReingoldStats>(
|
|
912
|
+
ctx,
|
|
913
|
+
model,
|
|
914
|
+
resolved,
|
|
915
|
+
tuning,
|
|
916
|
+
resolvePatch,
|
|
917
|
+
);
|
|
918
|
+
}
|