Task 2 of 6
Euler is wrong. The useful question is how wrong, and what more steps
buy you. Explicit Euler is a first-order method: its error at a fixed
end time is proportional to dt. Halve the step, halve the error. Pay
twice, get twice — which is a terrible exchange rate, and you can only find that out
by measuring it.
Measuring needs the same trajectories run at several step sizes, which is what the
second output axis is for. output: [256, 4] launches a grid:
this.thread.x picks the trajectory, this.thread.y picks the
refinement level — 50, 100, 200 or 400 steps, every level covering the
same t = 0…5, so dt is 5 / n. One launch, the
whole convergence study.
Notice what that does to the loop: the trip count now differs from thread to thread.
gpu.js compiles a non-constant bound as a fixed loopMaxIterations loop with
an early exit, so the 50-step threads still march through all 400 iterations alongside
their neighbours. That is not a gpu.js quirk — lockstep hardware behaves that way
everywhere.
dt bought.levelSteps[this.thread.y], and dt = this.constants.tEnd / nn explicit-Euler steps, exactly as in the last taskstartX·cos(tEnd) + startV·sin(tEnd)console.log the three ratios means[level − 1] / means[level]The row index is the level:
const n = levelSteps[this.thread.y];
const dt = this.constants.tEnd / n;
Every row ends at the same time; only the number of steps it took to get there differs.
Math.cos and Math.sin both work inside kernels:
const exact = startX[this.thread.x] * Math.cos(this.constants.tEnd)
+ startV[this.thread.x] * Math.sin(this.constants.tEnd);
return Math.abs(x - exact);
Math.abs matters: without it half the trajectories report a positive
error and half a negative one, and the row average cancels to nearly nothing.
A 2D kernel returns rows, so errors[level] is a whole row of
trajectory errors:
const means = [];
for (let level = 0; level < errors.length; level++) {
let total = 0;
for (let i = 0; i < errors[level].length; i++) total += errors[level][i];
means.push(total / errors[level].length);
}
for (let level = 1; level < means.length; level++) {
console.log((means[level - 1] / means[level]).toFixed(3));
}dim3 grid, WebGPU's dispatch dimensions, Metal's threadgroup grid. The
divergence lesson travels too: a warp, a wavefront or a subgroup runs its loop until the
last lane is finished, so a per-thread trip count is a hint about work, never a
promise about time.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.