<div class="controls">
<label>Correlation: <span id="rho-val">0.85</span>
<input type="range" id="rho" min="-0.99" max="0.99" step="0.01" value="0.85">
</label>
<div class="btns">
<button id="step-btn" type="button">Step</button>
<button id="run-btn" type="button">Run</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="420" height="300"></canvas>
<div class="info" id="info">Press Step or Run to start sampling.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .5rem; font-size: .9rem; }
label { display: flex; flex-direction: column; gap: .2rem; }
input[type=range] { width: 140px; cursor: pointer; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; display: block; }
.info { font-size: .85rem; color: #444; margin-top: .4rem; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gibbs sampling on a 2D correlated Gaussian.
// Target: (x,y) ~ N(0, Sigma), Sigma = [[1, rho],[rho, 1]]
// Conditionals:
// x | y ~ N(rho*y, 1 - rho^2)
// y | x ~ N(rho*x, 1 - rho^2)
// These are exact 1D Gaussians — easy to sample directly.
// No joint density ever needed.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const CX = W / 2, CY = H / 2;
const SCALE = 50; // pixels per unit std-dev
let rho = 0.85;
let samples = [];
let x = 0, y = 0;
let turn = 0; // 0 = update x, 1 = update y
let running = false;
let runTimer = null;
let totalSteps = 0;
const rhoSlider = document.getElementById('rho');
const rhoVal = document.getElementById('rho-val');
const info = document.getElementById('info');
rhoSlider.addEventListener('input', () => {
rho = parseFloat(rhoSlider.value);
rhoVal.textContent = rho.toFixed(2);
});
// Box-Muller transform: sample standard normal
function randn() {
let u, v;
do { u = Math.random(); } while (u === 0);
v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Convert distribution coords to canvas pixels
function toCanvas(dx, dy) {
return [CX + dx * SCALE, CY - dy * SCALE];
}
function drawBackground() {
ctx.clearRect(0, 0, W, H);
// Draw faint grid lines
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 0.8;
for (let i = -4; i <= 4; i++) {
const [px] = toCanvas(i, 0); const [, py] = toCanvas(0, i);
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke();
}
// Axes
ctx.strokeStyle = '#a0adb8'; ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
// Axis labels
ctx.fillStyle = '#7a8a99'; ctx.font = '11px system-ui';
ctx.fillText('x', W - 14, CY - 6);
ctx.fillText('y', CX + 6, 14);
// Draw true ellipse (1-sigma and 2-sigma contours of the target Gaussian)
// For N(0,Sigma) the 1-sigma ellipse satisfies x^2 - 2*rho*x*y + y^2 = (1-rho^2)
// We draw it parametrically: the covariance matrix has eigenvalues 1±rho,
const lambda1 = 1 + rho, lambda2 = 1 - rho;
for (const scale of [1, 2]) {
ctx.beginPath();
for (let t = 0; t <= 2 * Math.PI + 0.01; t += 0.04) {
// In eigenbasis: a*cos(t)*sqrt(lambda1), b*sin(t)*sqrt(lambda2)
const a = scale * Math.sqrt(Math.max(lambda1, 0));
const b = scale * Math.sqrt(Math.max(lambda2, 0));
const u1 = a * Math.cos(t), u2 = b * Math.sin(t);
// Rotate back (eigenvectors at 45 degrees)
const dx = (u1 + u2) / Math.sqrt(2);
const dy = (u1 - u2) / Math.sqrt(2);
const [px, py] = toCanvas(dx, dy);
t === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.closePath();
ctx.strokeStyle = scale === 1 ? 'rgba(29,53,87,0.25)' : 'rgba(29,53,87,0.12)';
ctx.lineWidth = 1;
ctx.stroke();
}
}
function drawSamples() {
const n = samples.length;
samples.forEach(([sx, sy], i) => {
const [px, py] = toCanvas(sx, sy);
const age = (n - i) / Math.max(n, 1);
const alpha = Math.max(0.12, 1 - age * 0.85);
ctx.beginPath();
ctx.arc(px, py, 2.5, 0, 2 * Math.PI);
ctx.fillStyle = `rgba(230,57,70,${alpha.toFixed(2)})`;
ctx.fill();
});
}
function drawCurrent() {
if (samples.length === 0) return;
const [px, py] = toCanvas(x, y);
ctx.beginPath(); ctx.arc(px, py, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557'; ctx.fill();
}
function redraw() {
drawBackground();
drawSamples();
drawCurrent();
}
function gibbsStep() {
const sd = Math.sqrt(Math.max(1 - rho * rho, 1e-9));
if (turn === 0) {
// Sample x | y
x = rho * y + sd * randn();
} else {
// Sample y | x
y = rho * x + sd * randn();
samples.push([x, y]);
if (samples.length > 500) samples.shift(); // keep display manageable
}
turn = 1 - turn;
totalSteps++;
}
function step() {
gibbsStep();
redraw();
info.textContent = fmt('Steps: %totalSteps% | Samples: %length% | Current: (%x%, %y%)', { totalSteps: totalSteps, length: samples.length, x: x.toFixed(2), y: y.toFixed(2) });
}
function startRun() {
if (running) return;
running = true;
document.getElementById('run-btn').textContent = 'Pause';
runTimer = setInterval(() => {
for (let i = 0; i < 4; i++) gibbsStep();
redraw();
info.textContent = fmt('Steps: %totalSteps% | Samples: %length% | Current: (%x%, %y%)', { totalSteps: totalSteps, length: samples.length, x: x.toFixed(2), y: y.toFixed(2) });
}, 40);
}
function pauseRun() {
running = false;
clearInterval(runTimer);
document.getElementById('run-btn').textContent = 'Run';
}
function reset() {
pauseRun();
samples = []; x = 0; y = 0; turn = 0; totalSteps = 0;
redraw();
info.textContent = 'Press Step or Run to start sampling.';
}
document.getElementById('step-btn').addEventListener('click', () => { pauseRun(); step(); });
document.getElementById('run-btn').addEventListener('click', () => { running ? pauseRun() : startRun(); });
document.getElementById('reset-btn').addEventListener('click', reset);
// Verify: conditional means must satisfy E[x|y] = rho*y and E[y|x] = rho*x
// Quick numerical check: run 10000 steps, verify sample mean near (0,0)
// and sample correlation near rho. (Logged to console.)
(function verify() {
let vx = 0, vy = 0, vt = 0, sx2 = 0, sy2 = 0, sxy = 0;
let tx = 0, ty = 0, tt = 0;
const vRho = 0.7;
const vsd = Math.sqrt(1 - vRho * vRho);
for (let i = 0; i < 20000; i++) {
if (tt === 0) { tx = vRho * ty + vsd * randn(); }
else { ty = vRho * tx + vsd * randn(); vx += tx; vy += ty; vt++; sx2 += tx*tx; sy2 += ty*ty; sxy += tx*ty; }
tt = 1 - tt;
}
const mx = vx/vt, my = vy/vt;
const rx = Math.sqrt(sx2/vt - mx*mx), ry = Math.sqrt(sy2/vt - my*my);
const corr = (sxy/vt - mx*my) / (rx * ry);
const ok = Math.abs(mx) < 0.05 && Math.abs(my) < 0.05 && Math.abs(corr - vRho) < 0.05;
console.log(fmt('[gibbs-verify] mean=(%mx%,%my%) corr=%corr% target_rho=%vRho% PASS=%ok%', { mx: mx.toFixed(3), my: my.toFixed(3), corr: corr.toFixed(3), vRho: vRho, ok: ok }));
})();
redraw();
<div class="controls">
<label>Correlation: <span id="rho-val">0.85</span>
<input type="range" id="rho" min="-0.99" max="0.99" step="0.01" value="0.85">
</label>
<div class="btns">
<button id="step-btn" type="button">Step</button>
<button id="run-btn" type="button">Run</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="420" height="300"></canvas>
<div class="info" id="info">Press Step or Run to start sampling.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .5rem; font-size: .9rem; }
label { display: flex; flex-direction: column; gap: .2rem; }
input[type=range] { width: 140px; cursor: pointer; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; display: block; }
.info { font-size: .85rem; color: #444; margin-top: .4rem; min-height: 1.3em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gibbs sampling on a 2D correlated Gaussian.
// Target: (x,y) ~ N(0, Sigma), Sigma = [[1, rho],[rho, 1]]
// Conditionals:
// x | y ~ N(rho*y, 1 - rho^2)
// y | x ~ N(rho*x, 1 - rho^2)
// These are exact 1D Gaussians — easy to sample directly.
// No joint density ever needed.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const CX = W / 2, CY = H / 2;
const SCALE = 50; // pixels per unit std-dev
let rho = 0.85;
let samples = [];
let x = 0, y = 0;
let turn = 0; // 0 = update x, 1 = update y
let running = false;
let runTimer = null;
let totalSteps = 0;
const rhoSlider = document.getElementById('rho');
const rhoVal = document.getElementById('rho-val');
const info = document.getElementById('info');
rhoSlider.addEventListener('input', () => {
rho = parseFloat(rhoSlider.value);
rhoVal.textContent = rho.toFixed(2);
});
// Box-Muller transform: sample standard normal
function randn() {
let u, v;
do { u = Math.random(); } while (u === 0);
v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// Convert distribution coords to canvas pixels
function toCanvas(dx, dy) {
return [CX + dx * SCALE, CY - dy * SCALE];
}
function drawBackground() {
ctx.clearRect(0, 0, W, H);
// Draw faint grid lines
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 0.8;
for (let i = -4; i <= 4; i++) {
const [px] = toCanvas(i, 0); const [, py] = toCanvas(0, i);
ctx.beginPath(); ctx.moveTo(px, 0); ctx.lineTo(px, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, py); ctx.lineTo(W, py); ctx.stroke();
}
// Axes
ctx.strokeStyle = '#a0adb8'; ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(0, CY); ctx.lineTo(W, CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX, 0); ctx.lineTo(CX, H); ctx.stroke();
// Axis labels
ctx.fillStyle = '#7a8a99'; ctx.font = '11px system-ui';
ctx.fillText('x', W - 14, CY - 6);
ctx.fillText('y', CX + 6, 14);
// Draw true ellipse (1-sigma and 2-sigma contours of the target Gaussian)
// For N(0,Sigma) the 1-sigma ellipse satisfies x^2 - 2*rho*x*y + y^2 = (1-rho^2)
// We draw it parametrically: the covariance matrix has eigenvalues 1±rho,
const lambda1 = 1 + rho, lambda2 = 1 - rho;
for (const scale of [1, 2]) {
ctx.beginPath();
for (let t = 0; t <= 2 * Math.PI + 0.01; t += 0.04) {
// In eigenbasis: a*cos(t)*sqrt(lambda1), b*sin(t)*sqrt(lambda2)
const a = scale * Math.sqrt(Math.max(lambda1, 0));
const b = scale * Math.sqrt(Math.max(lambda2, 0));
const u1 = a * Math.cos(t), u2 = b * Math.sin(t);
// Rotate back (eigenvectors at 45 degrees)
const dx = (u1 + u2) / Math.sqrt(2);
const dy = (u1 - u2) / Math.sqrt(2);
const [px, py] = toCanvas(dx, dy);
t === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.closePath();
ctx.strokeStyle = scale === 1 ? 'rgba(29,53,87,0.25)' : 'rgba(29,53,87,0.12)';
ctx.lineWidth = 1;
ctx.stroke();
}
}
function drawSamples() {
const n = samples.length;
samples.forEach(([sx, sy], i) => {
const [px, py] = toCanvas(sx, sy);
const age = (n - i) / Math.max(n, 1);
const alpha = Math.max(0.12, 1 - age * 0.85);
ctx.beginPath();
ctx.arc(px, py, 2.5, 0, 2 * Math.PI);
ctx.fillStyle = `rgba(230,57,70,${alpha.toFixed(2)})`;
ctx.fill();
});
}
function drawCurrent() {
if (samples.length === 0) return;
const [px, py] = toCanvas(x, y);
ctx.beginPath(); ctx.arc(px, py, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557'; ctx.fill();
}
function redraw() {
drawBackground();
drawSamples();
drawCurrent();
}
function gibbsStep() {
const sd = Math.sqrt(Math.max(1 - rho * rho, 1e-9));
if (turn === 0) {
// Sample x | y
x = rho * y + sd * randn();
} else {
// Sample y | x
y = rho * x + sd * randn();
samples.push([x, y]);
if (samples.length > 500) samples.shift(); // keep display manageable
}
turn = 1 - turn;
totalSteps++;
}
function step() {
gibbsStep();
redraw();
info.textContent = fmt('Steps: %totalSteps% | Samples: %length% | Current: (%x%, %y%)', { totalSteps: totalSteps, length: samples.length, x: x.toFixed(2), y: y.toFixed(2) });
}
function startRun() {
if (running) return;
running = true;
document.getElementById('run-btn').textContent = 'Pause';
runTimer = setInterval(() => {
for (let i = 0; i < 4; i++) gibbsStep();
redraw();
info.textContent = fmt('Steps: %totalSteps% | Samples: %length% | Current: (%x%, %y%)', { totalSteps: totalSteps, length: samples.length, x: x.toFixed(2), y: y.toFixed(2) });
}, 40);
}
function pauseRun() {
running = false;
clearInterval(runTimer);
document.getElementById('run-btn').textContent = 'Run';
}
function reset() {
pauseRun();
samples = []; x = 0; y = 0; turn = 0; totalSteps = 0;
redraw();
info.textContent = 'Press Step or Run to start sampling.';
}
document.getElementById('step-btn').addEventListener('click', () => { pauseRun(); step(); });
document.getElementById('run-btn').addEventListener('click', () => { running ? pauseRun() : startRun(); });
document.getElementById('reset-btn').addEventListener('click', reset);
// Verify: conditional means must satisfy E[x|y] = rho*y and E[y|x] = rho*x
// Quick numerical check: run 10000 steps, verify sample mean near (0,0)
// and sample correlation near rho. (Logged to console.)
(function verify() {
let vx = 0, vy = 0, vt = 0, sx2 = 0, sy2 = 0, sxy = 0;
let tx = 0, ty = 0, tt = 0;
const vRho = 0.7;
const vsd = Math.sqrt(1 - vRho * vRho);
for (let i = 0; i < 20000; i++) {
if (tt === 0) { tx = vRho * ty + vsd * randn(); }
else { ty = vRho * tx + vsd * randn(); vx += tx; vy += ty; vt++; sx2 += tx*tx; sy2 += ty*ty; sxy += tx*ty; }
tt = 1 - tt;
}
const mx = vx/vt, my = vy/vt;
const rx = Math.sqrt(sx2/vt - mx*mx), ry = Math.sqrt(sy2/vt - my*my);
const corr = (sxy/vt - mx*my) / (rx * ry);
const ok = Math.abs(mx) < 0.05 && Math.abs(my) < 0.05 && Math.abs(corr - vRho) < 0.05;
console.log(fmt('[gibbs-verify] mean=(%mx%,%my%) corr=%corr% target_rho=%vRho% PASS=%ok%', { mx: mx.toFixed(3), my: my.toFixed(3), corr: corr.toFixed(3), vRho: vRho, ok: ok }));
})();
redraw();
Comments
Loading comments...