Below is a 16×16 Ising model — a grid of spins, each +1 (white) or −1 (black). The Metropolis algorithm proposes flipping one random spin per step and accepts the flip with probability min(1,e−ΔE/T). At low temperature the system orders into domains; at high temperature it disorders.
<div class="controls">
<label>Temperature: <span id="tval">2.5</span>
<input type="range" id="temp" min="0.5" max="5.0" step="0.05" value="2.5">
</label>
<div class="btns">
<button id="runBtn" type="button">▶ Run</button>
<button id="stepBtn" type="button">Step ×100</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="320" height="320"></canvas>
<div class="stats">
<span>Steps: <b id="stepCount">0</b></span>
<span>|M|: <b id="mag">—</b></span>
<span>⟨E⟩/spin: <b id="eng">—</b></span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-bottom: .5rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 130px; accent-color: #1d3557; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; max-width: 100%; }
.stats { display: flex; gap: 1rem; font-size: .82rem; margin-top: .4rem; color: #444; }
.stats b { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Metropolis algorithm on a 2-D Ising model (periodic boundaries).
// Spin +1 (white) or -1 (black). Hamiltonian H = -J * sum_{<i,j>} s_i * s_j (J=1).
// Accept flip with probability min(1, exp(-dE / T)).
// The Boltzmann distribution is the unique stationary distribution (detailed balance).
const N = 32; // grid size NxN
const CELL = 10; // pixels per spin
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = N * CELL;
canvas.height = N * CELL;
let spin = new Int8Array(N * N); // +1 or -1
let steps = 0;
let running = false;
let rafId = null;
function idx(r, c) { return ((r + N) % N) * N + ((c + N) % N); }
function initRandom() {
for (let i = 0; i < N * N; i++) spin[i] = Math.random() < 0.5 ? 1 : -1;
steps = 0;
}
function siteEnergy(r, c) {
const s = spin[idx(r, c)];
const nb = spin[idx(r-1,c)] + spin[idx(r+1,c)] + spin[idx(r,c-1)] + spin[idx(r,c+1)];
return -s * nb; // J=1, divide by 2 later for total energy
}
function metropolisStep(T) {
// N*N attempted flips = 1 Monte Carlo sweep
for (let k = 0; k < N * N; k++) {
const i = (Math.random() * N * N) | 0;
const r = (i / N) | 0;
const c = i % N;
const dE = -2 * siteEnergy(r, c); // energy change if we flip
if (dE <= 0 || Math.random() < Math.exp(-dE / T)) {
spin[i] = -spin[i];
}
}
steps += N * N;
}
function draw() {
const img = ctx.createImageData(N * CELL, N * CELL);
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const v = spin[idx(r, c)] === 1 ? 240 : 30;
for (let dr = 0; dr < CELL; dr++) {
for (let dc = 0; dc < CELL; dc++) {
const p = ((r * CELL + dr) * N * CELL + c * CELL + dc) * 4;
img.data[p] = v; img.data[p+1] = v; img.data[p+2] = v; img.data[p+3] = 255;
}
}
}
}
ctx.putImageData(img, 0, 0);
}
function updateStats() {
let m = 0, e = 0;
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
m += spin[idx(r, c)];
const nb = spin[idx(r-1,c)] + spin[idx(r+1,c)] + spin[idx(r,c-1)] + spin[idx(r,c+1)];
e += -spin[idx(r,c)] * nb;
}
}
e /= 2; // each bond counted twice
const nn = N * N;
document.getElementById('mag').textContent = Math.abs(m / nn).toFixed(3);
document.getElementById('eng').textContent = (e / nn).toFixed(3);
document.getElementById('stepCount').textContent = fmt('%v% sweeps', { v: (steps / (N * N)).toFixed(0) });
}
function getT() { return parseFloat(document.getElementById('temp').value); }
function loop() {
if (!running) return;
metropolisStep(getT());
draw();
updateStats();
rafId = requestAnimationFrame(loop);
}
document.getElementById('runBtn').addEventListener('click', () => {
running = !running;
document.getElementById('runBtn').textContent = running ? '⏸ Pause' : '▶ Run';
if (running) loop();
});
document.getElementById('stepBtn').addEventListener('click', () => {
const T = getT();
for (let i = 0; i < 100; i++) metropolisStep(T);
draw(); updateStats();
});
document.getElementById('resetBtn').addEventListener('click', () => {
running = false;
if (rafId) cancelAnimationFrame(rafId);
document.getElementById('runBtn').textContent = '▶ Run';
initRandom(); draw(); updateStats();
});
document.getElementById('temp').addEventListener('input', () => {
document.getElementById('tval').textContent = getT().toFixed(2);
});
// Boot
initRandom();
draw();
updateStats();
<div class="controls">
<label>Temperature: <span id="tval">2.5</span>
<input type="range" id="temp" min="0.5" max="5.0" step="0.05" value="2.5">
</label>
<div class="btns">
<button id="runBtn" type="button">▶ Run</button>
<button id="stepBtn" type="button">Step ×100</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="320" height="320"></canvas>
<div class="stats">
<span>Steps: <b id="stepCount">0</b></span>
<span>|M|: <b id="mag">—</b></span>
<span>⟨E⟩/spin: <b id="eng">—</b></span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .6rem; align-items: center; margin-bottom: .5rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 130px; accent-color: #1d3557; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; max-width: 100%; }
.stats { display: flex; gap: 1rem; font-size: .82rem; margin-top: .4rem; color: #444; }
.stats b { color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Metropolis algorithm on a 2-D Ising model (periodic boundaries).
// Spin +1 (white) or -1 (black). Hamiltonian H = -J * sum_{<i,j>} s_i * s_j (J=1).
// Accept flip with probability min(1, exp(-dE / T)).
// The Boltzmann distribution is the unique stationary distribution (detailed balance).
const N = 32; // grid size NxN
const CELL = 10; // pixels per spin
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = N * CELL;
canvas.height = N * CELL;
let spin = new Int8Array(N * N); // +1 or -1
let steps = 0;
let running = false;
let rafId = null;
function idx(r, c) { return ((r + N) % N) * N + ((c + N) % N); }
function initRandom() {
for (let i = 0; i < N * N; i++) spin[i] = Math.random() < 0.5 ? 1 : -1;
steps = 0;
}
function siteEnergy(r, c) {
const s = spin[idx(r, c)];
const nb = spin[idx(r-1,c)] + spin[idx(r+1,c)] + spin[idx(r,c-1)] + spin[idx(r,c+1)];
return -s * nb; // J=1, divide by 2 later for total energy
}
function metropolisStep(T) {
// N*N attempted flips = 1 Monte Carlo sweep
for (let k = 0; k < N * N; k++) {
const i = (Math.random() * N * N) | 0;
const r = (i / N) | 0;
const c = i % N;
const dE = -2 * siteEnergy(r, c); // energy change if we flip
if (dE <= 0 || Math.random() < Math.exp(-dE / T)) {
spin[i] = -spin[i];
}
}
steps += N * N;
}
function draw() {
const img = ctx.createImageData(N * CELL, N * CELL);
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const v = spin[idx(r, c)] === 1 ? 240 : 30;
for (let dr = 0; dr < CELL; dr++) {
for (let dc = 0; dc < CELL; dc++) {
const p = ((r * CELL + dr) * N * CELL + c * CELL + dc) * 4;
img.data[p] = v; img.data[p+1] = v; img.data[p+2] = v; img.data[p+3] = 255;
}
}
}
}
ctx.putImageData(img, 0, 0);
}
function updateStats() {
let m = 0, e = 0;
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
m += spin[idx(r, c)];
const nb = spin[idx(r-1,c)] + spin[idx(r+1,c)] + spin[idx(r,c-1)] + spin[idx(r,c+1)];
e += -spin[idx(r,c)] * nb;
}
}
e /= 2; // each bond counted twice
const nn = N * N;
document.getElementById('mag').textContent = Math.abs(m / nn).toFixed(3);
document.getElementById('eng').textContent = (e / nn).toFixed(3);
document.getElementById('stepCount').textContent = fmt('%v% sweeps', { v: (steps / (N * N)).toFixed(0) });
}
function getT() { return parseFloat(document.getElementById('temp').value); }
function loop() {
if (!running) return;
metropolisStep(getT());
draw();
updateStats();
rafId = requestAnimationFrame(loop);
}
document.getElementById('runBtn').addEventListener('click', () => {
running = !running;
document.getElementById('runBtn').textContent = running ? '⏸ Pause' : '▶ Run';
if (running) loop();
});
document.getElementById('stepBtn').addEventListener('click', () => {
const T = getT();
for (let i = 0; i < 100; i++) metropolisStep(T);
draw(); updateStats();
});
document.getElementById('resetBtn').addEventListener('click', () => {
running = false;
if (rafId) cancelAnimationFrame(rafId);
document.getElementById('runBtn').textContent = '▶ Run';
initRandom(); draw(); updateStats();
});
document.getElementById('temp').addEventListener('input', () => {
document.getElementById('tval').textContent = getT().toFixed(2);
});
// Boot
initRandom();
draw();
updateStats();
Try dragging the Temperature slider. Below the critical temperature (~2.27 in these units) large ordered domains form — the system "magnetizes." Above it, thermal noise wins and spins randomize. Watch the Average magnetization track the phase transition in real time.
Comments
Loading comments...