# Drag the Temperature Across Tc

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

Everything is wired. The payoff is one number you can move with your finger.

Start from the coldest configuration there is — every spin up — and run 150 coloured
sweeps at a temperature you choose. Below the **critical temperature** the lattice
keeps its order: thermal noise chews holes in it, but the holes heal and the magnetisation
`m`, the average spin, sits stubbornly near `±1`. Above it the order
does not survive at all — the lattice dissolves into salt and pepper and `m` falls
to zero. In between there is no gentle slope; the whole thing turns over inside a few tenths
of a degree. Onsager solved this exactly in 1944 and the answer is
`Tc = 2 / ln(1 + √2) ≈ 2.269`, in units of `J / k_B`.

Two honest caveats, because a lattice of 16,384 spins is not infinite and 150 sweeps is not
forever. The crossover you can see sits a little above `2.269` — finite lattices
round a transition off, and a cold start clings to its order — and right at `Tc`
the model slows to a crawl, which is not a bug in the simulation but the defining symptom of a
critical point. Drag slowly through `2.3`–`2.5` and watch the domains
grow to the size of the whole box just before they let go.

## Figures

- **the exact answer drops off a cliff at 2.269; 16,384 spins in 150 sweeps take the corner wide** — Magnetisation against temperature, with the exact Onsager curve dropping vertically to zero at 2.269 and the measured 128 by 128 curve hanging on until about 2.4 before collapsing.

## Goal

**Goal:** declare the temperature slider, paint the lattice, and render a
frame every ten sweeps so the run becomes something you can scrub through.

## Requirements

- Declare the control: `slider('temperature', { min: 1.5, max: 3.5, value: 2.25, step: 0.05 })`
- Paint up spins `this.color(0.97, 0.58, 0.26, 1)` and down spins `this.color(0.09, 0.14, 0.28, 1)`
- Every tenth sweep, `await paint(s)` and then `render(paint.canvas)`
- Leave the prewired sweep loop and `plot()` call alone

## Hint 1 — the slider is the program

`slider()` returns the value this run is using and puts the control under
the console; moving it re-runs the whole program from the top. That is the entire model —
your code is a pure function of its controls, so there is no event loop to write.

```js
const temperature = slider('temperature',
  { min: 1.5, max: 3.5, value: 2.25, step: 0.05 });
```

## Hint 2 — three renders make a scrubber

Consecutive `render()` calls collapse into a frame strip with a slider
under it, so rendering inside the loop costs you nothing and gives you the whole history:

```js
if (k % 10 === 0) {
  await paint(s);
  render(paint.canvas);
}
```

Keep the two lines adjacent and do not `console.log` between them — a log line in
the middle breaks the run of frames into separate images.

## Same idea elsewhere

A control that re-runs the whole computation is the interaction model of every GPU
toy worth playing with, and it works for the same reason here as in a shader: the frame is
cheap enough that recomputing it beats maintaining incremental state. The physics travels
further than the code. Order parameters, critical exponents and finite-size scaling are the
vocabulary of everything from magnets to percolation thresholds to the training dynamics of
large models, and the Ising lattice is where all of it was worked out first.

## Starter code

```js
// The whole model, with a dial on it. Drag the temperature past 2.269.
const gpu = new GPU({ mode });

// TODO 1: declare the control. slider() returns the value THIS run is using.
const temperature = 2.25;

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];
  // parity as a NUMBER — gpu.js cannot keep a boolean in a kernel variable
  const parity = (x + y) % 2;
  if (parity !== 1) 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 paint = gpu.createKernel(function (s) {
  const spin = s[this.thread.y][this.thread.x];
  // TODO 2: up spins this.color(0.97, 0.58, 0.26, 1),
  //         down spins this.color(0.09, 0.14, 0.28, 1).
  this.color(1, 0, 1, 1);
}, { output: [128, 128], graphical: true });

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

// The coldest possible start: every spin up. 150 sweeps at your temperature.
let s = alignedLattice;
const trace = [];
for (let k = 0; k < 150; k++) {
  s = await black(await red(s, temperature, k * 2), temperature, k * 2 + 1);
  trace.push(meanOf(s));
  // TODO 3: every tenth sweep, await paint(s) and then render(paint.canvas).
}

console.log('temperature', temperature, '— Tc is 2.269');
console.log('magnetisation after 150 sweeps:', trace[149]);
plot({ magnetisation: trace }, { title: 'magnetisation per sweep' });
```

---

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

[Previous task](https://gpu.rocks/learn/ising-model-1f12d841/5.md)
