Below is a 1-D energy landscape. The curve shows E(x); the low valleys are where probability mass pools. A particle (the dot) samples from this landscape using Langevin dynamics: at each step it takes a small gradient-descent step (downhill on the energy) and adds a tiny random kick (the "thermal noise" that lets it escape shallow traps and explore).
<!-- Layout: landscape selector + canvas + info row -->
<div class="controls">
<span class="label">Landscape:</span>
<div class="btn-group">
<button id="btn-single" class="active" type="button">Single well</button>
<button id="btn-double" type="button">Double well</button>
<button id="btn-flat" type="button">Noisy flat</button>
</div>
<button id="btn-reset" class="ghost" type="button">Restart</button>
</div>
<canvas id="canvas" width="560" height="260"></canvas>
<div class="info-row">
<span id="status-txt" class="status-txt"></span>
<span id="step-counter" class="step-counter">Steps: 0</span>
</div>
<div class="hint-row">The red dot samples via Langevin dynamics: each step = gradient descent + Gaussian noise. Low-energy valleys attract probability mass — the particle spends most time there.</div>
/* Styles for the EBM Langevin sampling demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
canvas { display: block; border-radius: 10px; background: #f0f4f8;
border: 1px solid #cdd9e3; width: 100%; max-width: 560px; height: auto; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
margin-bottom: .5rem; }
.label { font-size: .85rem; color: #555; }
.btn-group { display: flex; gap: 3px; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border-radius: 7px;
border: 1px solid #1d3557; background: #fff; color: #1d3557; cursor: pointer; }
button.active { background: #1d3557; color: #fff; }
button.ghost { background: #eef2f6; border-color: #b0bec5; color: #555; }
.info-row { display: flex; justify-content: space-between; align-items: center;
margin: .4rem 0 .2rem; min-height: 1.5em; }
.status-txt { font-size: .9rem; font-weight: 600; color: #0a7d33; }
.step-counter { font-size: .8rem; color: #666; }
.hint-row { font-size: .82rem; color: #555; line-height: 1.45; }
// Energy-Based Model demo: Langevin dynamics sampling on a 1-D energy landscape.
// Langevin update: x_{t+1} = x_t - step_size * grad E(x_t) + noise
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = 560, H = 260;
const X_MIN = -4, X_MAX = 4; // domain of the 1-D energy landscape
// Three preset energy landscapes the user can switch between
const LANDSCAPES = {
single: x => (x * x - 2) * (x * x - 2) * 0.3 - 0.5 * Math.exp(-x * x),
double: x => 0.15 * (x * x - 3) * (x * x - 3) * (0.08 * x * x + 0.1),
flat: x => 0.1 * Math.sin(2.5 * x) + 0.05 * x * x
};
// Langevin hyperparameters (kept small for visual clarity)
const STEP_SIZE = 0.04; // gradient step size
const NOISE_STD = 0.18; // std of the Gaussian noise injection
const INTERVAL_MS = 30; // animation frame interval in ms
let landscape = 'single'; // which preset is active
let x = 0; // current particle position on the x-axis
let steps = 0; // total Langevin steps taken
let trail = []; // recent positions for the fading path
let animId = null; // requestAnimationFrame handle
// Map real coordinates to canvas pixels
function xToCanvas(xv) { return (xv - X_MIN) / (X_MAX - X_MIN) * W; }
function yToCanvas(yv) { return H * 0.5 - yv * 38; }
// Evaluate the active energy function at xv
function E(xv) { return LANDSCAPES[landscape](xv); }
// Numerical gradient via central differences
function gradE(xv) {
const h = 1e-4;
return (E(xv + h) - E(xv - h)) / (2 * h);
}
// One Langevin dynamics step
// x_{t+1} = x_t - step * grad E(x_t) + sqrt(2*step) * N(0,1)
function langevinStep() {
const noise = randn() * NOISE_STD;
x = x - STEP_SIZE * gradE(x) - noise;
// keep particle inside the visible domain
if (x < X_MIN + 0.1) x = X_MIN + 0.1;
if (x > X_MAX - 0.1) x = X_MAX - 0.1;
}
// Box-Muller transform for standard normal samples
function randn() {
const u = 1 - Math.random(), v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Render the canvas: energy curve, zero line, trail, particle
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw the energy landscape curve
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.5;
for (let px = 0; px <= W; px++) {
const xv = X_MIN + (px / W) * (X_MAX - X_MIN);
const yv = E(xv);
const cy = yToCanvas(yv);
if (px === 0) ctx.moveTo(px, cy); else ctx.lineTo(px, cy);
}
ctx.stroke();
// Dashed zero-energy reference line
ctx.beginPath();
ctx.strokeStyle = '#b0bec5';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.moveTo(0, yToCanvas(0));
ctx.lineTo(W, yToCanvas(0));
ctx.stroke();
ctx.setLineDash([]);
// Faded trail of recent particle positions
if (trail.length > 1) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(230,57,70,0.25)';
ctx.lineWidth = 1.2;
for (let i = 0; i < trail.length; i++) {
const cx = xToCanvas(trail[i]);
const cy = yToCanvas(E(trail[i]));
if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
}
ctx.stroke();
}
// Red dot for the current particle position
const cx = xToCanvas(x);
const cy = yToCanvas(E(x));
ctx.beginPath();
ctx.arc(cx, cy, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// Axis labels
ctx.font = '12px system-ui';
ctx.fillStyle = '#555';
ctx.fillText('E(x)', 8, 18);
ctx.fillText('x', W - 14, yToCanvas(0) + 14);
}
// Main animation loop
function animate() {
langevinStep();
trail.push(x);
if (trail.length > 120) trail.shift(); // keep only the last 120 positions
steps++;
document.getElementById('step-counter').textContent =
document.getElementById('step-counter').textContent.replace(/\d+$/, steps);
draw();
animId = requestAnimationFrame(animate);
}
// Reset particle, trail and step counter
function reset() {
if (animId) cancelAnimationFrame(animId);
x = (Math.random() - 0.5) * 4; // random starting position in [-2, 2]
steps = 0;
trail = [];
document.getElementById('step-counter').textContent =
document.getElementById('step-counter').textContent.replace(/\d+$/, 0);
document.getElementById('status-txt').textContent = '';
draw();
animId = requestAnimationFrame(animate);
}
// Wire up the landscape preset buttons
['single', 'double', 'flat'].forEach(id => {
document.getElementById('btn-' + id).addEventListener('click', () => {
landscape = id;
document.querySelectorAll('.btn-group button').forEach(b => b.classList.remove('active'));
document.getElementById('btn-' + id).classList.add('active');
reset();
});
});
document.getElementById('btn-reset').addEventListener('click', reset);
// Kick off the animation
reset();
<!-- Layout: landscape selector + canvas + info row -->
<div class="controls">
<span class="label">Landscape:</span>
<div class="btn-group">
<button id="btn-single" class="active" type="button">Single well</button>
<button id="btn-double" type="button">Double well</button>
<button id="btn-flat" type="button">Noisy flat</button>
</div>
<button id="btn-reset" class="ghost" type="button">Restart</button>
</div>
<canvas id="canvas" width="560" height="260"></canvas>
<div class="info-row">
<span id="status-txt" class="status-txt"></span>
<span id="step-counter" class="step-counter">Steps: 0</span>
</div>
<div class="hint-row">The red dot samples via Langevin dynamics: each step = gradient descent + Gaussian noise. Low-energy valleys attract probability mass — the particle spends most time there.</div>
/* Styles for the EBM Langevin sampling demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
canvas { display: block; border-radius: 10px; background: #f0f4f8;
border: 1px solid #cdd9e3; width: 100%; max-width: 560px; height: auto; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
margin-bottom: .5rem; }
.label { font-size: .85rem; color: #555; }
.btn-group { display: flex; gap: 3px; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border-radius: 7px;
border: 1px solid #1d3557; background: #fff; color: #1d3557; cursor: pointer; }
button.active { background: #1d3557; color: #fff; }
button.ghost { background: #eef2f6; border-color: #b0bec5; color: #555; }
.info-row { display: flex; justify-content: space-between; align-items: center;
margin: .4rem 0 .2rem; min-height: 1.5em; }
.status-txt { font-size: .9rem; font-weight: 600; color: #0a7d33; }
.step-counter { font-size: .8rem; color: #666; }
.hint-row { font-size: .82rem; color: #555; line-height: 1.45; }
// Energy-Based Model demo: Langevin dynamics sampling on a 1-D energy landscape.
// Langevin update: x_{t+1} = x_t - step_size * grad E(x_t) + noise
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = 560, H = 260;
const X_MIN = -4, X_MAX = 4; // domain of the 1-D energy landscape
// Three preset energy landscapes the user can switch between
const LANDSCAPES = {
single: x => (x * x - 2) * (x * x - 2) * 0.3 - 0.5 * Math.exp(-x * x),
double: x => 0.15 * (x * x - 3) * (x * x - 3) * (0.08 * x * x + 0.1),
flat: x => 0.1 * Math.sin(2.5 * x) + 0.05 * x * x
};
// Langevin hyperparameters (kept small for visual clarity)
const STEP_SIZE = 0.04; // gradient step size
const NOISE_STD = 0.18; // std of the Gaussian noise injection
const INTERVAL_MS = 30; // animation frame interval in ms
let landscape = 'single'; // which preset is active
let x = 0; // current particle position on the x-axis
let steps = 0; // total Langevin steps taken
let trail = []; // recent positions for the fading path
let animId = null; // requestAnimationFrame handle
// Map real coordinates to canvas pixels
function xToCanvas(xv) { return (xv - X_MIN) / (X_MAX - X_MIN) * W; }
function yToCanvas(yv) { return H * 0.5 - yv * 38; }
// Evaluate the active energy function at xv
function E(xv) { return LANDSCAPES[landscape](xv); }
// Numerical gradient via central differences
function gradE(xv) {
const h = 1e-4;
return (E(xv + h) - E(xv - h)) / (2 * h);
}
// One Langevin dynamics step
// x_{t+1} = x_t - step * grad E(x_t) + sqrt(2*step) * N(0,1)
function langevinStep() {
const noise = randn() * NOISE_STD;
x = x - STEP_SIZE * gradE(x) - noise;
// keep particle inside the visible domain
if (x < X_MIN + 0.1) x = X_MIN + 0.1;
if (x > X_MAX - 0.1) x = X_MAX - 0.1;
}
// Box-Muller transform for standard normal samples
function randn() {
const u = 1 - Math.random(), v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Render the canvas: energy curve, zero line, trail, particle
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw the energy landscape curve
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.5;
for (let px = 0; px <= W; px++) {
const xv = X_MIN + (px / W) * (X_MAX - X_MIN);
const yv = E(xv);
const cy = yToCanvas(yv);
if (px === 0) ctx.moveTo(px, cy); else ctx.lineTo(px, cy);
}
ctx.stroke();
// Dashed zero-energy reference line
ctx.beginPath();
ctx.strokeStyle = '#b0bec5';
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.moveTo(0, yToCanvas(0));
ctx.lineTo(W, yToCanvas(0));
ctx.stroke();
ctx.setLineDash([]);
// Faded trail of recent particle positions
if (trail.length > 1) {
ctx.beginPath();
ctx.strokeStyle = 'rgba(230,57,70,0.25)';
ctx.lineWidth = 1.2;
for (let i = 0; i < trail.length; i++) {
const cx = xToCanvas(trail[i]);
const cy = yToCanvas(E(trail[i]));
if (i === 0) ctx.moveTo(cx, cy); else ctx.lineTo(cx, cy);
}
ctx.stroke();
}
// Red dot for the current particle position
const cx = xToCanvas(x);
const cy = yToCanvas(E(x));
ctx.beginPath();
ctx.arc(cx, cy, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// Axis labels
ctx.font = '12px system-ui';
ctx.fillStyle = '#555';
ctx.fillText('E(x)', 8, 18);
ctx.fillText('x', W - 14, yToCanvas(0) + 14);
}
// Main animation loop
function animate() {
langevinStep();
trail.push(x);
if (trail.length > 120) trail.shift(); // keep only the last 120 positions
steps++;
document.getElementById('step-counter').textContent =
document.getElementById('step-counter').textContent.replace(/\d+$/, steps);
draw();
animId = requestAnimationFrame(animate);
}
// Reset particle, trail and step counter
function reset() {
if (animId) cancelAnimationFrame(animId);
x = (Math.random() - 0.5) * 4; // random starting position in [-2, 2]
steps = 0;
trail = [];
document.getElementById('step-counter').textContent =
document.getElementById('step-counter').textContent.replace(/\d+$/, 0);
document.getElementById('status-txt').textContent = '';
draw();
animId = requestAnimationFrame(animate);
}
// Wire up the landscape preset buttons
['single', 'double', 'flat'].forEach(id => {
document.getElementById('btn-' + id).addEventListener('click', () => {
landscape = id;
document.querySelectorAll('.btn-group button').forEach(b => b.classList.remove('active'));
document.getElementById('btn-' + id).classList.add('active');
reset();
});
});
document.getElementById('btn-reset').addEventListener('click', reset);
// Kick off the animation
reset();
Use the buttons to change the landscape and watch how the sampler behaves. Notice that the particle spends most of its time near the energy minima — that is exactly what p(x)∝e−E(x) predicts. A steep, narrow valley traps the chain; a wide, shallow valley is explored quickly. Multiple modes separated by high barriers can starve one mode entirely — the classic mixing problem that haunts all MCMC samplers.
Comments
Loading comments...