The grid below is a 40×40 lattice of spins. Each spin is +1 (light) or −1 (dark). The Metropolis algorithm runs continuously: it picks a random spin, computes the energy cost of flipping it, and flips it with a probability that depends on temperature.
<div class="controls">
<label>Temperature: <span id="temp-val">2.5</span>
<input type="range" id="temp" min="0.5" max="5.0" step="0.05" value="2.5">
</label>
<button id="toggleBtn" type="button">Pause</button>
<button id="resetBtn" type="button" class="ghost">Randomize</button>
</div>
<canvas id="canvas" width="400" height="400"></canvas>
<div class="stats">
<div class="stat-row">
<span class="stat-label">Magnetization</span>
<div class="bar-wrap"><div class="bar-fill" id="mag-bar"></div></div>
<span class="stat-num" id="mag-num">0.00</span>
</div>
<div class="stat-row">
<span class="stat-label">Energy/spin</span>
<span class="stat-num wide" id="energy-num">—</span>
</div>
</div>
<p class="hint">T < 2.27: spins align (ordered phase). T > 2.27: disorder wins. Watch magnetization snap at the critical temperature.</p>
* { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; background: #fff; padding: 8px; }
.controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
label { display: flex; align-items: center; gap: 6px; font-size: 13px; flex: 1; min-width: 220px; }
input[type=range] { flex: 1; cursor: pointer; }
button { font: 600 13px system-ui; padding: .35rem .8rem; 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;
width: 400px; height: 400px; max-width: 100%; image-rendering: pixelated; }
.stats { margin: 8px 0 4px; display: flex; flex-direction: column; gap: 4px; }
.stat-row { display: flex; align-items: center; gap: 8px; }
.stat-label { font-size: 12px; color: #555; width: 90px; flex-shrink: 0; }
.bar-wrap { flex: 1; height: 12px; background: #e8eef3; border-radius: 6px; overflow: hidden; }
.bar-fill { height: 100%; background: #1d3557; border-radius: 6px; transition: width .15s; }
.stat-num { font: 600 12px ui-monospace, monospace; width: 48px; text-align: right; }
.stat-num.wide { width: auto; }
.hint { font-size: 12px; color: #555; line-height: 1.5; margin-top: 6px; }
// 2D Ising model — Metropolis algorithm
// Spins: +1 (up) or -1 (down) on an N×N periodic lattice.
// Energy: E = -J * sum_{<i,j>} s_i * s_j (J=1, ferromagnetic)
// Metropolis: flip spin i if ΔE ≤ 0, else flip with prob exp(-ΔE / T)
// Critical temperature (exact, Onsager 1944): Tc = 2/ln(1+sqrt(2)) ≈ 2.2692
const N = 40;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const CELL = canvas.width / N; // 10px per cell
let T = 2.5;
let running = true;
let spins = new Int8Array(N * N); // +1 or -1
let frameId;
function idx(r, c) { return ((r + N) % N) * N + ((c + N) % N); }
function randomize() {
for (let i = 0; i < N * N; i++) spins[i] = Math.random() < 0.5 ? 1 : -1;
}
function deltaE(r, c) {
// ΔE when flipping spin at (r,c) = 2 * s * (sum of 4 neighbors)
const s = spins[idx(r, c)];
const nb = spins[idx(r-1,c)] + spins[idx(r+1,c)] + spins[idx(r,c-1)] + spins[idx(r,c+1)];
return 2 * s * nb;
}
// Lookup table for exp(-dE/T) for dE in {4,8} (only positive dE values matter)
let expCache = {};
function updateCache() {
expCache[4] = Math.exp(-4 / T);
expCache[8] = Math.exp(-8 / T);
}
function metropolisStep(steps) {
for (let k = 0; k < steps; k++) {
const r = (Math.random() * N) | 0;
const c = (Math.random() * N) | 0;
const dE = deltaE(r, c);
if (dE <= 0 || Math.random() < expCache[dE]) {
spins[idx(r, c)] *= -1;
}
}
}
function draw() {
const imgData = ctx.createImageData(N, N);
const d = imgData.data;
for (let i = 0; i < N * N; i++) {
const v = spins[i] === 1 ? 220 : 50;
d[i*4] = v;
d[i*4+1] = v;
d[i*4+2] = spins[i] === 1 ? 235 : 80;
d[i*4+3] = 255;
}
// Scale N×N to canvas size
const tmp = document.createElement('canvas');
tmp.width = N; tmp.height = N;
tmp.getContext('2d').putImageData(imgData, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(tmp, 0, 0, canvas.width, canvas.height);
}
function stats() {
let sum = 0, energy = 0;
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
const s = spins[idx(r,c)];
sum += s;
energy += -s * (spins[idx(r+1,c)] + spins[idx(r,c+1)]);
}
const mag = sum / (N * N);
const ePerSpin = energy / (N * N);
const absMag = Math.abs(mag);
document.getElementById('mag-bar').style.width = (absMag * 100).toFixed(1) + '%';
document.getElementById('mag-num').textContent = mag.toFixed(2);
document.getElementById('energy-num').textContent = ePerSpin.toFixed(3);
}
let lastTime = 0;
function loop(ts) {
if (!running) return;
const dt = ts - lastTime;
lastTime = ts;
const steps = Math.max(1, (dt / 16) | 0) * N * N;
metropolisStep(steps);
draw();
stats();
frameId = requestAnimationFrame(loop);
}
document.getElementById('temp').addEventListener('input', function() {
T = parseFloat(this.value);
document.getElementById('temp-val').textContent = T.toFixed(2);
updateCache();
});
document.getElementById('toggleBtn').addEventListener('click', function() {
running = !running;
this.textContent = running ? 'Pause' : 'Resume';
if (running) { lastTime = performance.now(); frameId = requestAnimationFrame(loop); }
});
document.getElementById('resetBtn').addEventListener('click', function() {
randomize();
if (!running) { draw(); stats(); }
});
// Init
updateCache();
randomize();
draw();
stats();
lastTime = performance.now();
frameId = requestAnimationFrame(loop);
// Expose for node verification
if (typeof module !== 'undefined') module.exports = { N, deltaE, idx, randomize, metropolisStep, spins };
<div class="controls">
<label>Temperature: <span id="temp-val">2.5</span>
<input type="range" id="temp" min="0.5" max="5.0" step="0.05" value="2.5">
</label>
<button id="toggleBtn" type="button">Pause</button>
<button id="resetBtn" type="button" class="ghost">Randomize</button>
</div>
<canvas id="canvas" width="400" height="400"></canvas>
<div class="stats">
<div class="stat-row">
<span class="stat-label">Magnetization</span>
<div class="bar-wrap"><div class="bar-fill" id="mag-bar"></div></div>
<span class="stat-num" id="mag-num">0.00</span>
</div>
<div class="stat-row">
<span class="stat-label">Energy/spin</span>
<span class="stat-num wide" id="energy-num">—</span>
</div>
</div>
<p class="hint">T < 2.27: spins align (ordered phase). T > 2.27: disorder wins. Watch magnetization snap at the critical temperature.</p>
* { box-sizing: border-box; margin: 0; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; background: #fff; padding: 8px; }
.controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
label { display: flex; align-items: center; gap: 6px; font-size: 13px; flex: 1; min-width: 220px; }
input[type=range] { flex: 1; cursor: pointer; }
button { font: 600 13px system-ui; padding: .35rem .8rem; 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;
width: 400px; height: 400px; max-width: 100%; image-rendering: pixelated; }
.stats { margin: 8px 0 4px; display: flex; flex-direction: column; gap: 4px; }
.stat-row { display: flex; align-items: center; gap: 8px; }
.stat-label { font-size: 12px; color: #555; width: 90px; flex-shrink: 0; }
.bar-wrap { flex: 1; height: 12px; background: #e8eef3; border-radius: 6px; overflow: hidden; }
.bar-fill { height: 100%; background: #1d3557; border-radius: 6px; transition: width .15s; }
.stat-num { font: 600 12px ui-monospace, monospace; width: 48px; text-align: right; }
.stat-num.wide { width: auto; }
.hint { font-size: 12px; color: #555; line-height: 1.5; margin-top: 6px; }
// 2D Ising model — Metropolis algorithm
// Spins: +1 (up) or -1 (down) on an N×N periodic lattice.
// Energy: E = -J * sum_{<i,j>} s_i * s_j (J=1, ferromagnetic)
// Metropolis: flip spin i if ΔE ≤ 0, else flip with prob exp(-ΔE / T)
// Critical temperature (exact, Onsager 1944): Tc = 2/ln(1+sqrt(2)) ≈ 2.2692
const N = 40;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const CELL = canvas.width / N; // 10px per cell
let T = 2.5;
let running = true;
let spins = new Int8Array(N * N); // +1 or -1
let frameId;
function idx(r, c) { return ((r + N) % N) * N + ((c + N) % N); }
function randomize() {
for (let i = 0; i < N * N; i++) spins[i] = Math.random() < 0.5 ? 1 : -1;
}
function deltaE(r, c) {
// ΔE when flipping spin at (r,c) = 2 * s * (sum of 4 neighbors)
const s = spins[idx(r, c)];
const nb = spins[idx(r-1,c)] + spins[idx(r+1,c)] + spins[idx(r,c-1)] + spins[idx(r,c+1)];
return 2 * s * nb;
}
// Lookup table for exp(-dE/T) for dE in {4,8} (only positive dE values matter)
let expCache = {};
function updateCache() {
expCache[4] = Math.exp(-4 / T);
expCache[8] = Math.exp(-8 / T);
}
function metropolisStep(steps) {
for (let k = 0; k < steps; k++) {
const r = (Math.random() * N) | 0;
const c = (Math.random() * N) | 0;
const dE = deltaE(r, c);
if (dE <= 0 || Math.random() < expCache[dE]) {
spins[idx(r, c)] *= -1;
}
}
}
function draw() {
const imgData = ctx.createImageData(N, N);
const d = imgData.data;
for (let i = 0; i < N * N; i++) {
const v = spins[i] === 1 ? 220 : 50;
d[i*4] = v;
d[i*4+1] = v;
d[i*4+2] = spins[i] === 1 ? 235 : 80;
d[i*4+3] = 255;
}
// Scale N×N to canvas size
const tmp = document.createElement('canvas');
tmp.width = N; tmp.height = N;
tmp.getContext('2d').putImageData(imgData, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(tmp, 0, 0, canvas.width, canvas.height);
}
function stats() {
let sum = 0, energy = 0;
for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
const s = spins[idx(r,c)];
sum += s;
energy += -s * (spins[idx(r+1,c)] + spins[idx(r,c+1)]);
}
const mag = sum / (N * N);
const ePerSpin = energy / (N * N);
const absMag = Math.abs(mag);
document.getElementById('mag-bar').style.width = (absMag * 100).toFixed(1) + '%';
document.getElementById('mag-num').textContent = mag.toFixed(2);
document.getElementById('energy-num').textContent = ePerSpin.toFixed(3);
}
let lastTime = 0;
function loop(ts) {
if (!running) return;
const dt = ts - lastTime;
lastTime = ts;
const steps = Math.max(1, (dt / 16) | 0) * N * N;
metropolisStep(steps);
draw();
stats();
frameId = requestAnimationFrame(loop);
}
document.getElementById('temp').addEventListener('input', function() {
T = parseFloat(this.value);
document.getElementById('temp-val').textContent = T.toFixed(2);
updateCache();
});
document.getElementById('toggleBtn').addEventListener('click', function() {
running = !running;
this.textContent = running ? 'Pause' : 'Resume';
if (running) { lastTime = performance.now(); frameId = requestAnimationFrame(loop); }
});
document.getElementById('resetBtn').addEventListener('click', function() {
randomize();
if (!running) { draw(); stats(); }
});
// Init
updateCache();
randomize();
draw();
stats();
lastTime = performance.now();
frameId = requestAnimationFrame(loop);
// Expose for node verification
if (typeof module !== 'undefined') module.exports = { N, deltaE, idx, randomize, metropolisStep, spins };
Drag the Temperature slider all the way right (hot) and the grid scrambles — disorder wins. Drag it left past the critical temperature (~2.27 in model units) and watch the spins spontaneously organize into large aligned domains. The Magnetization bar shows the average spin: near zero at high temperature, pinned to +1 or −1 at low temperature. The transition between those two regimes is sharp — that sharpness is the phase transition.
Comments
Loading comments...