# Halve the Step, Halve the Error

*Task 2 of 6 · [ODE Integrators](https://gpu.rocks/learn/ode-integrators-62f4a3ff.md) · GPU.js Learn*

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.

## Goal

**Goal:** return each thread's absolute error against the closed form,
then average each row in JavaScript and log what each halving of `dt` bought.

## Requirements

- Take the step count from the level: `levelSteps[this.thread.y]`, and `dt = this.constants.tEnd / n`
- Integrate `n` explicit-Euler steps, exactly as in the last task
- Return the *absolute* error against `startX·cos(tEnd) + startV·sin(tEnd)`
- In JavaScript: average each row, then `console.log` the three ratios `means[level − 1] / means[level]`

## Hint 1 — which step size is mine?

The row index *is* the level:

```js
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.

## Hint 2 — the truth to subtract

`Math.cos` and `Math.sin` both work inside kernels:

```js
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.

## Hint 3 — the JavaScript half

A 2D kernel returns rows, so `errors[level]` is a whole row of
trajectory errors:

```js
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));
}
```

## Same idea elsewhere

Running an entire convergence study as one dispatch is the GPU-native form of a
parameter sweep, and 2D launch grids are how every platform spells it — CUDA's
`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.

## Starter code

```js
// x = trajectory, y = refinement level. 256 oscillators × 4 step sizes,
// every level covering the same t = 0…5. One launch, one whole study.
const gpu = new GPU({ mode });

const errorOf = gpu.createKernel(function (startX, startV, levelSteps) {
  // TODO: this row's step count and step size
  const n = 1;
  const dt = 1;

  let x = startX[this.thread.x];
  let v = startV[this.thread.x];
  for (let s = 0; s < n; s++) {
    const a = -x;
    x = x + v * dt;
    v = v + a * dt;
  }

  // TODO: the exact answer is startX·cos(tEnd) + startV·sin(tEnd).
  // Return how far this trajectory ended up from it — an absolute distance.
  return x;
}, {
  output: [256, 4],
  constants: { tEnd: 5 },
  loopMaxIterations: 400,          // the largest level; a bigger one would be truncated
});

const errors = await errorOf(startX, startV, levelSteps);
console.log('levels:', errors.length, '× trajectories:', errors[0].length);

// TODO: average each row into means[], log each one, then log the ratios
// means[level - 1] / means[level] — that number is the method's order.
```

---

Interactive version: https://gpu.rocks/learn/ode-integrators-62f4a3ff/2

[Previous task](https://gpu.rocks/learn/ode-integrators-62f4a3ff/1.md) · [Next task](https://gpu.rocks/learn/ode-integrators-62f4a3ff/3.md)
