The grid below is an Ising lattice: blue cells are spin-up, red cells spin-down. Use the temperature slider to move toward or away from the critical point (Tc≈2.269 in Ising units). Switch between Metropolis (one spin at a time) and Wolff (one cluster per step) to feel the difference.
<!-- Ising lattice demo: Metropolis vs Wolff cluster algorithm -->
<div class="controls">
<label>
<span>Temperature T:</span>
<input type="range" id="tempSlider" min="1.0" max="4.0" step="0.05" value="2.27">
<span id="tempVal">2.27</span>
</label>
<label>
<span>Algorithm:</span>
<select id="algoSelect">
<option value="metropolis">Metropolis (single spin)</option>
<option value="wolff">Wolff (cluster)</option>
</select>
</label>
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="lattice" width="300" height="300"></canvas>
<div class="info-row">
<span id="algoLabel"></span>
<span id="clusterInfo"></span>
</div>
<div id="status" class="status"></div>
/* Styling for Ising demo: canvas + controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #f7f9fb; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .6rem; }
label { display: flex; align-items: center; gap: .35rem; font-size: .88rem; }
input[type=range] { width: 110px; }
select { font: inherit; padding: .2rem .4rem; border: 1px solid #bbb; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .38rem .8rem; background: #1d3557; color: #fff;
border: 1px solid #1d3557; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.active { background: #e63946; border-color: #e63946; }
canvas { display: block; border: 1px solid #ccd3dc; border-radius: 8px; background: #fff; cursor: pointer; }
.info-row { display: flex; gap: 1rem; font-size: .82rem; color: #555; margin: .4rem 0 .2rem; flex-wrap: wrap; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #1d6e3f; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Ising model Monte Carlo demo: compare Metropolis single-spin flips
// with the Wolff cluster algorithm. Near Tc both decorrelate, but Wolff does it in far fewer steps.
const N = 30; // 30x30 lattice
const J = 1; // coupling constant
let T = 2.27;
let algo = 'metropolis';
let running = false;
let rafId = null;
let lastClusterSize = 0;
// Initialize spins randomly +1 or -1
const spin = new Int8Array(N * N);
function idx(r, c) { return ((r + N) % N) * N + ((c + N) % N); }
function randomize() {
for (let i = 0; i < spin.length; i++) spin[i] = Math.random() < 0.5 ? 1 : -1;
}
randomize();
const canvas = document.getElementById('lattice');
const ctx = canvas.getContext('2d');
const CW = canvas.width / N;
// last Wolff cluster to highlight in orange
let highlightSet = null;
function draw() {
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const s = spin[idx(r, c)];
const inCluster = highlightSet && highlightSet.has(idx(r, c));
// cluster = orange, spin up = blue, spin down = red
ctx.fillStyle = inCluster ? '#f4a261'
: s === 1 ? '#4a90d9' : '#e63946';
ctx.fillRect(c * CW, r * CW, CW, CW);
}
}
}
// Single Metropolis step: pick a random spin, compute energy change, accept/reject.
function metropolisStep() {
const r = Math.floor(Math.random() * N);
const c = Math.floor(Math.random() * N);
const i = idx(r, c);
const s = spin[i];
const neighbors = [idx(r-1,c), idx(r+1,c), idx(r,c-1), idx(r,c+1)];
const sumNeigh = neighbors.reduce((a, j) => a + spin[j], 0);
const dE = 2 * J * s * sumNeigh;
if (dE <= 0 || Math.random() < Math.exp(-dE / T)) spin[i] = -s;
lastClusterSize = 1;
}
// Wolff cluster step: BFS growth with bond probability p = 1 - exp(-2J/T).
function wolffStep() {
const r0 = Math.floor(Math.random() * N);
const c0 = Math.floor(Math.random() * N);
const seed = idx(r0, c0);
const s0 = spin[seed];
const p = 1 - Math.exp(-2 * J / T);
const cluster = new Set([seed]);
const queue = [seed];
while (queue.length > 0) {
const cur = queue.pop();
const cr = Math.floor(cur / N), cc = cur % N;
const nbrs = [idx(cr-1,cc), idx(cr+1,cc), idx(cr,cc-1), idx(cr,cc+1)];
for (const nb of nbrs) {
if (!cluster.has(nb) && spin[nb] === s0 && Math.random() < p) {
cluster.add(nb);
queue.push(nb);
}
}
}
// Flip the entire cluster
for (const i of cluster) spin[i] = -s0;
lastClusterSize = cluster.size;
highlightSet = cluster;
}
function doStep() {
highlightSet = null;
if (algo === 'metropolis') {
// One 'sweep' = N*N individual Metropolis trials
for (let i = 0; i < N * N; i++) metropolisStep();
} else {
wolffStep();
}
draw();
updateInfo();
}
function updateInfo() {
const algoLabel = document.getElementById('algoLabel');
const clusterInfo = document.getElementById('clusterInfo');
const statusEl = document.getElementById('status');
if (algo === 'wolff') {
algoLabel.textContent = 'Algorithm: Wolff cluster';
clusterInfo.textContent = i18nFmt('Last cluster: %n% spins flipped', { n: lastClusterSize });
// Warn when near the critical temperature Tc ~ 2.269
const critMsg = (T > 2.15 && T < 2.4) ? 'Near Tc — watch clusters span the lattice!' : '';
statusEl.textContent = critMsg;
} else {
algoLabel.textContent = 'Algorithm: Metropolis';
clusterInfo.textContent = '';
const critMsg = (T > 2.15 && T < 2.4) ? 'Near Tc — Metropolis is freezing. Try Wolff!' : '';
statusEl.textContent = critMsg;
}
}
document.getElementById('tempSlider').addEventListener('input', function () {
T = parseFloat(this.value);
document.getElementById('tempVal').textContent = T.toFixed(2);
highlightSet = null;
draw();
updateInfo();
});
document.getElementById('algoSelect').addEventListener('change', function () {
algo = this.value;
highlightSet = null;
updateInfo();
});
document.getElementById('stepBtn').addEventListener('click', doStep);
const runBtn = document.getElementById('runBtn');
runBtn.addEventListener('click', function () {
running = !running;
if (running) {
runBtn.textContent = 'Pause';
runBtn.classList.add('active');
function loop() { if (!running) return; doStep(); rafId = requestAnimationFrame(loop); }
rafId = requestAnimationFrame(loop);
} else {
runBtn.textContent = 'Run';
runBtn.classList.remove('active');
if (rafId) cancelAnimationFrame(rafId);
}
});
document.getElementById('resetBtn').addEventListener('click', function () {
running = false;
runBtn.textContent = 'Run';
runBtn.classList.remove('active');
if (rafId) cancelAnimationFrame(rafId);
randomize();
highlightSet = null;
draw();
updateInfo();
});
draw();
updateInfo();
<!-- Ising lattice demo: Metropolis vs Wolff cluster algorithm -->
<div class="controls">
<label>
<span>Temperature T:</span>
<input type="range" id="tempSlider" min="1.0" max="4.0" step="0.05" value="2.27">
<span id="tempVal">2.27</span>
</label>
<label>
<span>Algorithm:</span>
<select id="algoSelect">
<option value="metropolis">Metropolis (single spin)</option>
<option value="wolff">Wolff (cluster)</option>
</select>
</label>
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="lattice" width="300" height="300"></canvas>
<div class="info-row">
<span id="algoLabel"></span>
<span id="clusterInfo"></span>
</div>
<div id="status" class="status"></div>
/* Styling for Ising demo: canvas + controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #f7f9fb; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .6rem; }
label { display: flex; align-items: center; gap: .35rem; font-size: .88rem; }
input[type=range] { width: 110px; }
select { font: inherit; padding: .2rem .4rem; border: 1px solid #bbb; border-radius: 6px; }
button { font: 600 13px system-ui; padding: .38rem .8rem; background: #1d3557; color: #fff;
border: 1px solid #1d3557; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button.active { background: #e63946; border-color: #e63946; }
canvas { display: block; border: 1px solid #ccd3dc; border-radius: 8px; background: #fff; cursor: pointer; }
.info-row { display: flex; gap: 1rem; font-size: .82rem; color: #555; margin: .4rem 0 .2rem; flex-wrap: wrap; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #1d6e3f; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Ising model Monte Carlo demo: compare Metropolis single-spin flips
// with the Wolff cluster algorithm. Near Tc both decorrelate, but Wolff does it in far fewer steps.
const N = 30; // 30x30 lattice
const J = 1; // coupling constant
let T = 2.27;
let algo = 'metropolis';
let running = false;
let rafId = null;
let lastClusterSize = 0;
// Initialize spins randomly +1 or -1
const spin = new Int8Array(N * N);
function idx(r, c) { return ((r + N) % N) * N + ((c + N) % N); }
function randomize() {
for (let i = 0; i < spin.length; i++) spin[i] = Math.random() < 0.5 ? 1 : -1;
}
randomize();
const canvas = document.getElementById('lattice');
const ctx = canvas.getContext('2d');
const CW = canvas.width / N;
// last Wolff cluster to highlight in orange
let highlightSet = null;
function draw() {
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
const s = spin[idx(r, c)];
const inCluster = highlightSet && highlightSet.has(idx(r, c));
// cluster = orange, spin up = blue, spin down = red
ctx.fillStyle = inCluster ? '#f4a261'
: s === 1 ? '#4a90d9' : '#e63946';
ctx.fillRect(c * CW, r * CW, CW, CW);
}
}
}
// Single Metropolis step: pick a random spin, compute energy change, accept/reject.
function metropolisStep() {
const r = Math.floor(Math.random() * N);
const c = Math.floor(Math.random() * N);
const i = idx(r, c);
const s = spin[i];
const neighbors = [idx(r-1,c), idx(r+1,c), idx(r,c-1), idx(r,c+1)];
const sumNeigh = neighbors.reduce((a, j) => a + spin[j], 0);
const dE = 2 * J * s * sumNeigh;
if (dE <= 0 || Math.random() < Math.exp(-dE / T)) spin[i] = -s;
lastClusterSize = 1;
}
// Wolff cluster step: BFS growth with bond probability p = 1 - exp(-2J/T).
function wolffStep() {
const r0 = Math.floor(Math.random() * N);
const c0 = Math.floor(Math.random() * N);
const seed = idx(r0, c0);
const s0 = spin[seed];
const p = 1 - Math.exp(-2 * J / T);
const cluster = new Set([seed]);
const queue = [seed];
while (queue.length > 0) {
const cur = queue.pop();
const cr = Math.floor(cur / N), cc = cur % N;
const nbrs = [idx(cr-1,cc), idx(cr+1,cc), idx(cr,cc-1), idx(cr,cc+1)];
for (const nb of nbrs) {
if (!cluster.has(nb) && spin[nb] === s0 && Math.random() < p) {
cluster.add(nb);
queue.push(nb);
}
}
}
// Flip the entire cluster
for (const i of cluster) spin[i] = -s0;
lastClusterSize = cluster.size;
highlightSet = cluster;
}
function doStep() {
highlightSet = null;
if (algo === 'metropolis') {
// One 'sweep' = N*N individual Metropolis trials
for (let i = 0; i < N * N; i++) metropolisStep();
} else {
wolffStep();
}
draw();
updateInfo();
}
function updateInfo() {
const algoLabel = document.getElementById('algoLabel');
const clusterInfo = document.getElementById('clusterInfo');
const statusEl = document.getElementById('status');
if (algo === 'wolff') {
algoLabel.textContent = 'Algorithm: Wolff cluster';
clusterInfo.textContent = i18nFmt('Last cluster: %n% spins flipped', { n: lastClusterSize });
// Warn when near the critical temperature Tc ~ 2.269
const critMsg = (T > 2.15 && T < 2.4) ? 'Near Tc — watch clusters span the lattice!' : '';
statusEl.textContent = critMsg;
} else {
algoLabel.textContent = 'Algorithm: Metropolis';
clusterInfo.textContent = '';
const critMsg = (T > 2.15 && T < 2.4) ? 'Near Tc — Metropolis is freezing. Try Wolff!' : '';
statusEl.textContent = critMsg;
}
}
document.getElementById('tempSlider').addEventListener('input', function () {
T = parseFloat(this.value);
document.getElementById('tempVal').textContent = T.toFixed(2);
highlightSet = null;
draw();
updateInfo();
});
document.getElementById('algoSelect').addEventListener('change', function () {
algo = this.value;
highlightSet = null;
updateInfo();
});
document.getElementById('stepBtn').addEventListener('click', doStep);
const runBtn = document.getElementById('runBtn');
runBtn.addEventListener('click', function () {
running = !running;
if (running) {
runBtn.textContent = 'Pause';
runBtn.classList.add('active');
function loop() { if (!running) return; doStep(); rafId = requestAnimationFrame(loop); }
rafId = requestAnimationFrame(loop);
} else {
runBtn.textContent = 'Run';
runBtn.classList.remove('active');
if (rafId) cancelAnimationFrame(rafId);
}
});
document.getElementById('resetBtn').addEventListener('click', function () {
running = false;
runBtn.textContent = 'Run';
runBtn.classList.remove('active');
if (rafId) cancelAnimationFrame(rafId);
randomize();
highlightSet = null;
draw();
updateInfo();
});
draw();
updateInfo();
Notice what happens near Tc: Metropolis updates barely change the large domains — the simulation is frozen. Switch to Wolff and a single step flips an entire correlated cluster, cutting right through the configuration. Far from Tc the two algorithms look similar; the advantage of Wolff is greatest exactly where it matters most.
Comments
Loading comments...