# Two Halves Make a Sweep

*Task 5 of 6 · [The Ising Model: Colour to Break the Race](https://gpu.rocks/learn/ising-model-1f12d841.md) · GPU.js Learn*

The red half is not a sweep — half the lattice has not been offered a move. The black
half is the same kernel with its parity test flipped, and the ordering that matters is in the
**chaining**: `black(red(s))`. The black cells read the lattice the red
half produced, so their `ΔE` accounts for the reds that just moved. That is not an
optimisation, it is the correctness argument: a black cell's four neighbours are all red, and
the reds have finished.

Give the two halves different seeds too — `2k` and `2k + 1` — or every
black cell would draw the same number its red neighbours just used.

Then run it. Same starting lattice as task 3, same temperature, same thirty sweeps, and
the energy that climbed from `−0.01` to `+1.30` now falls to about
`−1.84`, sweep after sweep — 26 of the 29 steps go down, and the three that do not
tick back up by less than `0.004`. That wobble is the temperature doing its job:
`T = 1.5` is cold, not zero, so a handful of uphill moves are accepted every sweep
and the energy is allowed to breathe. Nothing about the physics changed and nothing got
slower: the same 16,384 spins are offered the same moves. All that changed is *which of
them are allowed to move at the same time*.

## Goal

**Goal:** write the black half-sweep and chain the two halves into one full
sweep, `black(red(s))`.

## Requirements

- The black kernel is the red kernel with its parity test flipped — parity `1` moves, everything else passes through
- Create the red kernel first and the black kernel second
- One full sweep is `await black(await red(s, T, 2k), T, 2k + 1)` — the black half reads what the red half wrote

## Hint 1 — the black kernel

Copy the red kernel and change one digit:

```js
const parity = (x + y) % 2;
if (parity !== 1) return spin;
```

Still a number, never a boolean — the WebGL backend rejects `const isBlack = …`
exactly as it rejects `isRed`.

## Hint 2 — the chain is the lesson

```js
const afterRed = await red(s, temperature, k * 2);
s = await black(afterRed, temperature, k * 2 + 1);   // NOT black(s, …)
```

Or in one line, `await black(await red(s, T, 2 * k), T, 2 * k + 1)`. The inner
`await` is not decoration: an un-awaited kernel call hands the black half a
Promise instead of a lattice.

## Same idea elsewhere

Two dispatches with a dependency between them is the ordinary shape of GPU work —
WebGPU puts a barrier between compute passes, CUDA orders them on a stream, Vulkan wants an
explicit pipeline barrier. What no platform will do is order threads *inside* one
dispatch, which is exactly why the update had to be split in two. Production lattice Monte
Carlo goes one step further and launches only the cells of one colour, so the half that would
copy itself is never scheduled at all; the trade is a strided memory access pattern against
half the threads, and which wins is a benchmark, not an argument.

## Starter code

```js
// One full sweep = the reds, then the blacks reading what the reds just wrote.
const gpu = new GPU({ mode });

const red = gpu.createKernel(function (s, temperature, seed) {
  const x = this.thread.x;
  const y = this.thread.y;
  const n = this.constants.size;
  const spin = s[y][x];
  // parity as a NUMBER — gpu.js cannot keep a boolean in a kernel variable
  const parity = (x + y) % 2;
  if (parity !== 0) return spin;
  const sum = s[y][(x + 1) % n] + s[y][(x + n - 1) % n]
            + s[(y + 1) % n][x] + s[(y + n - 1) % n][x];
  const dE = 2 * spin * sum;
  // a random number for THIS thread, this sweep — no shared state, no ordering
  let h = (x * 1103 + y * 2749 + seed * 3571) % 65536;
  let q = h % 2048;
  h = (h + q * q) % 65536;
  h = (h % 256) * 256 + Math.floor(h / 256);
  h = (h * 253 + 30011) % 65536;
  q = h % 2048;
  h = (h + q * q) % 65536;
  const u = h / 65536;
  if (dE <= 0) return -spin;
  if (u < Math.exp(-dE / temperature)) return -spin;
  return spin;
}, { output: [128, 128], constants: { size: 128 } });

const black = gpu.createKernel(function (s, temperature, seed) {
  const x = this.thread.x;
  const y = this.thread.y;
  const n = this.constants.size;
  const spin = s[y][x];
  // TODO 1: same body as the red kernel, with the parity test flipped —
  //         the cells where (x + y) % 2 is 1 take the Metropolis decision.
  return spin;
}, { output: [128, 128], constants: { size: 128 } });

const bondEnergy = gpu.createKernel(function (s) {
  const x = this.thread.x;
  const y = this.thread.y;
  const n = this.constants.size;
  const sum = s[y][(x + 1) % n] + s[y][(x + n - 1) % n]
            + s[(y + 1) % n][x] + s[(y + n - 1) % n][x];
  return -0.5 * s[y][x] * sum;
}, { output: [128, 128], constants: { size: 128 } });

function meanOf(grid) {
  let total = 0;
  for (let y = 0; y < 128; y++) {
    for (let x = 0; x < 128; x++) total += grid[y][x];
  }
  return total / (128 * 128);
}

let s = lattice;
const trace = [];
for (let k = 0; k < 30; k++) {
  const afterRed = await red(s, 1.5, k * 2);
  // TODO 2: finish the sweep. The black half must read afterRed, not s.
  s = afterRed;
  trace.push(meanOf(await bondEnergy(s)));
}

console.log('energy per spin, sweep by sweep:', trace);
console.log('after 30 red-black sweeps: E =', trace[29]);
plot({ 'energy per spin': trace }, { title: 'checkerboard Metropolis at T = 1.5' });
```

---

Interactive version: https://gpu.rocks/learn/ising-model-1f12d841/5

[Previous task](https://gpu.rocks/learn/ising-model-1f12d841/4.md) · [Next task](https://gpu.rocks/learn/ising-model-1f12d841/6.md)
