Skip to main content

ScientificFlowTracer (Experimental)

ScientificFlowTracer traces explicitly seeded particles through regular 2D vector fields on WebGPU. It adds adaptive embedded Runge–Kutta–Fehlberg 4(5) integration and persistent termination states alongside the visualization-oriented ScientificFlowGraph. Import it from @luma.gl/graph/experimental/simulation.

const tracer = new ScientificFlowTracer(device, {
input, // normalized scientific grid: dimensions, shape, eastward, northward, valid
bounds: [0, 0, 10000, 10000],
coordinates: 'cartesian',
positions: new Float32Array([0.25, 0.5, 0.75, 0.5]), // normalized x/y pairs
tolerance: 0.01, // absolute local position-error tolerance in bounds units
maximumTimeStep: 60,
maximumAttempts: 32
});

let result = await tracer.advance(3600);
while (!result.complete) result = await tracer.continue();
// tracer.time is now 3600; output remains on the GPU.
tracer.destroy();

Accuracy and field support​

The tolerance compares the embedded fourth- and fifth-order displacement estimates; accepted steps use the fifth-order update. It controls estimated local integration error, not accumulated trajectory error or grid-interpolation error. Cartesian errors use bounds units. With coordinates: 'lnglat', velocities and tolerance use metres per second and metres; longitude displacement accounts for latitude.

Spatial sampling is bilinear and requires all four grid corners to be valid. Optional nextInput and timeRange: [start, end] enable linear interpolation in time at every RK stage. Both snapshots must share dimensions, bounds and coordinates. At an interval endpoint, setNextSnapshot(input, endTime) rotates the pair without resetting particles. No extrapolation is permitted.

The controller halves rejected steps and doubles accepted steps whose estimated error is below one tenth of the tolerance. A half-cell travel limit also caps the step. minimumTimeStep defaults to 1e-4 seconds, maximumTimeStep to 1 second, and initialTimeStep to the maximum. Request and lifetime endpoints may clip a step below the minimum. An unmet tolerance is never force-accepted at the minimum. Invalid trial positions first trigger step reduction; an unrecoverable trial stops the particle at its last accepted position.

Computation uses float32. A non-finite calculation or an increment too small to advance the particle's age/request clock produces NumericalFailure. A final roundoff-sized request remainder is still integrated and checked against the tolerance, even when it is smaller than the age clock can represent. Very strict tolerances can reach float32 limits. This API supports forward tracing on regular collocated 2D grids; curvilinear/staggered grids, 3D fields, diffusion, boundary sliding and global trajectory-error guarantees are outside its current scope.

Bounded work and clocks​

Each awaited advance() or continue() submits one dispatch, with at most maximumAttempts trials per particle (default 32, range 1–256). Rejected trials consume that budget. complete: false means work remains for the same requested horizon. Resume with continue() before starting another request or rotating a snapshot. Calls must not overlap. Await the current call before destroying resources.

The committed time stays unchanged while a request is pending. Individual GPU particles may be at different intermediate times until completion. Stopped particles retain their earlier ages even after the horizon commits. Completion means every particle reached the horizon or explicitly stopped; inspect status counts to distinguish these outcomes. remainingTime bounds the next request for a time-dependent field. Stationary fields have no interval bound.

Each dispatch reads back nine counters (36 bytes), never particle positions. Results include pending, counts for each status, and per-dispatch acceptedSteps and rejectedSteps. If the device enables timestamp-query, gpuMilliseconds reports compute-pass duration; it excludes readback latency and rendering. A submission/readback failure disables further advancement of that tracer; destroy it and create a new instance.

Graph inspection and tracing​

Every advance() and continue() executes the same compiled commandGraph: reset-trace-counters copies nine zero counters, then trace-particles-rk45 runs the bounded adaptive kernel. The graph declares particle, previous-state, diagnostic, counter, uniform and velocity-texture access, including the reset-to-compute dependency. Snapshot rotation rebinds both velocity textures on the next encoding. Use advance() and continue() for execution; encoding commandGraph directly bypasses the tracer's request and readback lifecycle.

The returned encoding is the actual submitted GPUCommandGraphEncoding. Register tracer.commandGraph with GPUCommandGraphInspector and record result.encoding to inspect it without executing another simulation step. dispatchIndex counts submitted dispatches from one; isContinuation distinguishes resumed work, and targetTime identifies the requested horizon even while time is uncommitted.

const result = await tracer.advance(1);
const report = await result.encoding.readTimings();
// report.nodes contains reset CPU cost and adaptive compute CPU/GPU timings.

GPU timing uses immutable per-encoding timestamp snapshots, so later dispatches can reuse the query set without changing an earlier report. Without timestamp support, CPU encoding and resource inspection remain available. Copy nodes have CPU timing only. gpuMilliseconds measures the adaptive compute pass, excluding counter readback and trail rendering. Resource previews are explicit, additional GPU readbacks of current allocations, not historical snapshots of particle data; read them before destroying the tracer or rotating away their field textures.

Vector Observatory's Graph and Trace tabs show this graph for RK45 as well as RK2/RK4. They sample every 60 encodings and clear on reseed or integrator changes. Diagnostics shows the dispatch number, continuation state, target horizon, work counts and particle outcomes.

GPU buffers and particle identity​

Particle index is stable identity. There is no automatic respawn. lifetime defaults to 1e30 seconds; expiration clips the final step exactly to that age. Create a new tracer to seed another release.

BufferFour float32 values per particle
outputNormalized x, normalized y, age in seconds, generation (always 1)
previousState at the start of the most recent dispatch
diagnosticsElapsed seconds in the current request, proposed step, status, last accepted step

ScientificParticleStatus exports these stable numeric values:

ValueStatusMeaning
0ActiveMay continue tracing
1ExpiredReached configured lifetime
2OutsideDomainOutside the field or unable to keep a trial inside it
3MissingDataRequired interpolation samples are invalid
4ToleranceFailureError still exceeds tolerance at the minimum step
5NumericalFailureNon-finite arithmetic or float32 clock stagnation

Qualification and example​

Vector Observatory offers RK45, tolerance selection, accepted-step coloring, status coloring and stopped-particle markers. Adaptive mode performs one bounded dispatch per animation frame and displays pending particles and queued simulation time. Work can lag behind wall time; the clock shows only committed simulation time.

GPU tests cover analytic rotation and convergence, geographic units, temporal snapshot rotation, physical-hour requests, dispatch-budget independence, missing samples, domain exit, lifetime and numerical/tolerance failures. Run the opt-in accuracy/cost benchmark with:

LUMA_TEST_BROWSER_BENCHMARKS=true yarn test-headless --no-coverage --silent=false modules/graph/test/experimental/simulation/scientific-flow-benchmark.spec.ts

The benchmark compares 65,536 identical rotation trajectories over two seconds, requiring both fixed RK4 and adaptive RK45 to have final position error below 1e-5. It records three measured samples after warmup, alternates execution order, and reports work counts and optional GPU timestamps. This controlled case is not a general speedup claim; heterogeneous trajectories and counter readback can change end-to-end cost substantially.

The RKF45 stages and basic controller are adapted from the MIT-licensed Parcels AdvectionRK45 implementation. The shader retains its copyright and permission notice; the complete notice also ships in simulation/PARCELS-LICENSE.txt.

Periodic longitude​

Set periodicLongitude: true for a horizontally periodic field. The first and last grid columns must describe the same meridian. Sampling wraps every Runge–Kutta stage and accepted particle positions stay in normalized [0, 1) longitude, without respawning or changing particle identity. Latitude and missing-data boundaries retain their usual behavior. Snapshot rotation preserves this option. Renderers should unwrap segment displacement and split trails at the atlas seam; raw consecutive x coordinates can jump from near one to near zero.