<div class="controls">
<label>Bowl shape:
<select id="shape">
<option value="1,1">Circular (a=1, b=1)</option>
<option value="0.5,4" selected>Elongated (a=0.5, b=4)</option>
<option value="0.2,5">Very elongated (a=0.2, b=5)</option>
</select>
</label>
<label>Speed:
<select id="speed">
<option value="600">Slow</option>
<option value="300" selected>Normal</option>
<option value="120">Fast</option>
</select>
</label>
</div>
<canvas id="canvas" width="380" height="320"></canvas>
<div class="info" id="info">Click the canvas to set start point, then step.</div>
<div class="btns">
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; font-size: .88rem; }
label { display: flex; align-items: center; gap: .4rem; }
select { font-size: .88rem; border: 1px solid #b0b8c1; border-radius: 6px; padding: .25rem .5rem; }
canvas { display: block; border: 1px solid #d0d6dd; border-radius: 10px;
background: #f8fafc; cursor: crosshair; max-width: 100%; touch-action: none; }
.info { font-size: .9rem; color: #444; min-height: 1.5em; margin: .5rem 0; }
.info.done { color: #0a7d33; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Coordinate descent on f(x,y) = a*x^2 + b*y^2.
// Minimum is at (0,0). Each step fixes one coordinate and minimizes the 1-D slice:
// fix y -> optimal x = 0 (df/dx = 2a*x = 0)
// fix x -> optimal y = 0 (df/dy = 2b*y = 0)
// For a general quadratic A*x^2 + B*y^2 the exact coordinate update is:
// x_new = -gradient_x / (2a) = 0, y_new = -gradient_y / (2b) = 0
// which for this bowl means both coordinates go directly to 0 in one step each.
// To make the zigzag visible we perturb with a coupling term:
// f(x,y) = a*x^2 + b*y^2 + c*x*y
// Optimal x with y fixed: df/dx = 2a*x + c*y = 0 -> x = -c*y/(2a)
// Optimal y with x fixed: df/dy = 2b*y + c*x = 0 -> y = -c*x/(2b)
const C = 1.8; // coupling strength — makes the zigzag vivid
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 = 60; // pixels per unit
let a = 0.5, b = 4;
let path = []; // [{x,y}] in math coords
let step = 0; // which coordinate to update next (0=x, 1=y)
let animId = null;
function f(x, y) { return a*x*x + b*y*y + C*x*y; }
// Math coords -> canvas coords
function toCanvas(x, y) { return [CX + x*SCALE, CY - y*SCALE]; }
function toMath(px, py) { return [(px-CX)/SCALE, (CY-py)/SCALE]; }
// Coordinate descent step
function cdStep(pt) {
const {x, y} = pt;
if (step % 2 === 0) {
// Minimize over x: d/dx(a*x^2 + C*x*y) = 2a*x + C*y = 0 => x = -C*y/(2a)
const nx = -C*y / (2*a);
step++;
return {x: nx, y: y};
} else {
// Minimize over y: d/dy(b*y^2 + C*x*y) = 2b*y + C*x = 0 => y = -C*x/(2b)
const ny = -C*x / (2*b);
step++;
return {x: x, y: ny};
}
}
// Draw contours of f
function drawContours() {
const levels = [0.5, 1.5, 3, 5.5, 9, 14, 20];
for (const lv of levels) {
ctx.beginPath();
let first = true;
// Approximate ellipse: 2a*x^2 + 2b*y^2 + 2C*x*y = 2*lv (quadratic form)
// Parametric: sweep angle t, find r(t) on the level curve
for (let t = 0; t <= 2*Math.PI+0.01; t += 0.04) {
const dx = Math.cos(t), dy = Math.sin(t);
const A = a*dx*dx + b*dy*dy + C*dx*dy;
if (A <= 1e-9) continue;
const r = Math.sqrt(lv/A);
const [px, py] = toCanvas(r*dx, r*dy);
first ? ctx.moveTo(px,py) : ctx.lineTo(px,py);
first = false;
}
ctx.closePath();
ctx.strokeStyle = 'rgba(100,140,200,0.35)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Minimum marker
const [mx, my] = toCanvas(0, 0);
ctx.beginPath(); ctx.arc(mx, my, 5, 0, 2*Math.PI);
ctx.fillStyle = '#0a7d33'; ctx.fill();
}
function drawPath() {
if (path.length < 1) return;
ctx.lineWidth = 2;
// Horizontal steps (x changes) in blue, vertical (y changes) in orange
for (let i = 1; i < path.length; i++) {
const [x0, y0] = toCanvas(path[i-1].x, path[i-1].y);
const [x1, y1] = toCanvas(path[i].x, path[i].y);
// Determine direction: if x changed it was a horizontal step, else vertical
const horizontal = Math.abs(path[i].x - path[i-1].x) > Math.abs(path[i].y - path[i-1].y);
ctx.beginPath();
ctx.moveTo(x0, y0); ctx.lineTo(x1, y1);
ctx.strokeStyle = horizontal ? '#1d6fa5' : '#e07020';
ctx.lineWidth = 2.5;
ctx.stroke();
// Arrow tip
const angle = Math.atan2(y1-y0, x1-x0);
ctx.save();
ctx.translate(x1, y1);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0,0); ctx.lineTo(-10, -5); ctx.lineTo(-10, 5); ctx.closePath();
ctx.fillStyle = horizontal ? '#1d6fa5' : '#e07020';
ctx.fill();
ctx.restore();
}
// Start marker
const [sx, sy] = toCanvas(path[0].x, path[0].y);
ctx.beginPath(); ctx.arc(sx, sy, 6, 0, 2*Math.PI);
ctx.fillStyle = '#c92f3c'; ctx.fill();
// Current position
const last = path[path.length-1];
const [lx, ly] = toCanvas(last.x, last.y);
ctx.beginPath(); ctx.arc(lx, ly, 6, 0, 2*Math.PI);
ctx.fillStyle = '#e07020'; ctx.fill();
}
function drawAxes() {
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0,CY); ctx.lineTo(W,CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX,0); ctx.lineTo(CX,H); ctx.stroke();
ctx.fillStyle = '#999'; ctx.font = '11px system-ui';
ctx.fillText('x', W-12, CY-6);
ctx.fillText('y', CX+6, 12);
}
function render() {
ctx.clearRect(0, 0, W, H);
drawAxes();
drawContours();
drawPath();
}
function dist2(p) { return p.x*p.x + p.y*p.y; }
function updateInfo() {
const last = path[path.length-1];
const fval = last ? f(last.x, last.y).toFixed(4) : '—';
const d = last ? Math.sqrt(dist2(last)).toFixed(4) : '—';
const done = last && dist2(last) < 0.0001;
const el = document.getElementById('info');
el.className = done ? 'info done' : 'info';
el.textContent = done
? fmt('Converged! f = %fval% (distance to min = %d%)', { fval: fval, d: d })
: last
? fmt('Step %v% | f(x,y) = %fval% | dist = %d% | next: %v2%', { v: path.length-1, fval: fval, d: d, v2: step%2===0?'minimize x':'minimize y' })
: 'Click on the canvas to set a starting point.';
}
function doStep() {
if (path.length === 0) { updateInfo(); return; }
const last = path[path.length-1];
if (dist2(last) < 0.0001) { updateInfo(); return; }
path.push(cdStep(last));
render();
updateInfo();
}
function parseShape() {
const [av, bv] = document.getElementById('shape').value.split(',').map(Number);
a = av; b = bv;
}
document.getElementById('shape').addEventListener('change', () => {
parseShape(); path = []; step = 0; render(); updateInfo();
});
document.getElementById('stepBtn').addEventListener('click', () => {
stopAnim(); doStep();
});
document.getElementById('runBtn').addEventListener('click', () => {
if (animId) { stopAnim(); return; }
if (path.length === 0) { updateInfo(); return; }
runAnim();
});
document.getElementById('resetBtn').addEventListener('click', () => {
stopAnim(); path = []; step = 0; render(); updateInfo();
});
function stopAnim() {
if (animId) { clearInterval(animId); animId = null; }
document.getElementById('runBtn').textContent = 'Run';
}
function runAnim() {
document.getElementById('runBtn').textContent = 'Pause';
const delay = parseInt(document.getElementById('speed').value, 10);
animId = setInterval(() => {
const last = path[path.length-1];
if (!last || dist2(last) < 0.0001) { stopAnim(); return; }
doStep();
}, delay);
}
function handleClick(e) {
stopAnim();
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
const [mx, my] = toMath(px, py);
path = [{x: mx, y: my}];
step = 0;
render();
updateInfo();
}
canvas.addEventListener('click', handleClick);
// Initial render
parseShape();
render();
updateInfo();
// Verification log (used by test harness)
(function verifyCD() {
// Verify: for f(x,y)=a*x^2+b*y^2+C*x*y, starting from (3,3)
// After step 1 (min over x): x = -C*y/(2a) = -1.8*3/(2*0.5) = -5.4
// After step 2 (min over y): y = -C*x/(2b) = -1.8*(-5.4)/(2*4) = 1.215
// Check these by running the cdStep logic
const startX = 3, startY = 3;
let px = startX, py = startY;
let s = 0;
function step1(x, y, st) {
if (st % 2 === 0) return {x: -C*y/(2*a), y: y};
return {x: x, y: -C*x/(2*b)};
}
const r1 = step1(px, py, s); s++;
const expectedX1 = -C*startY/(2*a); // = -1.8*3/1 = -5.4
const r2 = step1(r1.x, r1.y, s); s++;
const expectedY2 = -C*r1.x/(2*b); // = -1.8*(-5.4)/8 = 1.215
console.assert(Math.abs(r1.x - expectedX1) < 1e-9, 'Step 1 x update failed');
console.assert(Math.abs(r2.y - expectedY2) < 1e-9, 'Step 2 y update failed');
console.log('CD verification: step1 x =', r1.x.toFixed(4), '(expected', expectedX1.toFixed(4)+')');
console.log('CD verification: step2 y =', r2.y.toFixed(4), '(expected', expectedY2.toFixed(4)+')');
console.log('Coordinate descent demo verified OK');
})();
<div class="controls">
<label>Bowl shape:
<select id="shape">
<option value="1,1">Circular (a=1, b=1)</option>
<option value="0.5,4" selected>Elongated (a=0.5, b=4)</option>
<option value="0.2,5">Very elongated (a=0.2, b=5)</option>
</select>
</label>
<label>Speed:
<select id="speed">
<option value="600">Slow</option>
<option value="300" selected>Normal</option>
<option value="120">Fast</option>
</select>
</label>
</div>
<canvas id="canvas" width="380" height="320"></canvas>
<div class="info" id="info">Click the canvas to set start point, then step.</div>
<div class="btns">
<button id="stepBtn" type="button">Step</button>
<button id="runBtn" type="button">Run</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.controls { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .6rem; font-size: .88rem; }
label { display: flex; align-items: center; gap: .4rem; }
select { font-size: .88rem; border: 1px solid #b0b8c1; border-radius: 6px; padding: .25rem .5rem; }
canvas { display: block; border: 1px solid #d0d6dd; border-radius: 10px;
background: #f8fafc; cursor: crosshair; max-width: 100%; touch-action: none; }
.info { font-size: .9rem; color: #444; min-height: 1.5em; margin: .5rem 0; }
.info.done { color: #0a7d33; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Coordinate descent on f(x,y) = a*x^2 + b*y^2.
// Minimum is at (0,0). Each step fixes one coordinate and minimizes the 1-D slice:
// fix y -> optimal x = 0 (df/dx = 2a*x = 0)
// fix x -> optimal y = 0 (df/dy = 2b*y = 0)
// For a general quadratic A*x^2 + B*y^2 the exact coordinate update is:
// x_new = -gradient_x / (2a) = 0, y_new = -gradient_y / (2b) = 0
// which for this bowl means both coordinates go directly to 0 in one step each.
// To make the zigzag visible we perturb with a coupling term:
// f(x,y) = a*x^2 + b*y^2 + c*x*y
// Optimal x with y fixed: df/dx = 2a*x + c*y = 0 -> x = -c*y/(2a)
// Optimal y with x fixed: df/dy = 2b*y + c*x = 0 -> y = -c*x/(2b)
const C = 1.8; // coupling strength — makes the zigzag vivid
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 = 60; // pixels per unit
let a = 0.5, b = 4;
let path = []; // [{x,y}] in math coords
let step = 0; // which coordinate to update next (0=x, 1=y)
let animId = null;
function f(x, y) { return a*x*x + b*y*y + C*x*y; }
// Math coords -> canvas coords
function toCanvas(x, y) { return [CX + x*SCALE, CY - y*SCALE]; }
function toMath(px, py) { return [(px-CX)/SCALE, (CY-py)/SCALE]; }
// Coordinate descent step
function cdStep(pt) {
const {x, y} = pt;
if (step % 2 === 0) {
// Minimize over x: d/dx(a*x^2 + C*x*y) = 2a*x + C*y = 0 => x = -C*y/(2a)
const nx = -C*y / (2*a);
step++;
return {x: nx, y: y};
} else {
// Minimize over y: d/dy(b*y^2 + C*x*y) = 2b*y + C*x = 0 => y = -C*x/(2b)
const ny = -C*x / (2*b);
step++;
return {x: x, y: ny};
}
}
// Draw contours of f
function drawContours() {
const levels = [0.5, 1.5, 3, 5.5, 9, 14, 20];
for (const lv of levels) {
ctx.beginPath();
let first = true;
// Approximate ellipse: 2a*x^2 + 2b*y^2 + 2C*x*y = 2*lv (quadratic form)
// Parametric: sweep angle t, find r(t) on the level curve
for (let t = 0; t <= 2*Math.PI+0.01; t += 0.04) {
const dx = Math.cos(t), dy = Math.sin(t);
const A = a*dx*dx + b*dy*dy + C*dx*dy;
if (A <= 1e-9) continue;
const r = Math.sqrt(lv/A);
const [px, py] = toCanvas(r*dx, r*dy);
first ? ctx.moveTo(px,py) : ctx.lineTo(px,py);
first = false;
}
ctx.closePath();
ctx.strokeStyle = 'rgba(100,140,200,0.35)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Minimum marker
const [mx, my] = toCanvas(0, 0);
ctx.beginPath(); ctx.arc(mx, my, 5, 0, 2*Math.PI);
ctx.fillStyle = '#0a7d33'; ctx.fill();
}
function drawPath() {
if (path.length < 1) return;
ctx.lineWidth = 2;
// Horizontal steps (x changes) in blue, vertical (y changes) in orange
for (let i = 1; i < path.length; i++) {
const [x0, y0] = toCanvas(path[i-1].x, path[i-1].y);
const [x1, y1] = toCanvas(path[i].x, path[i].y);
// Determine direction: if x changed it was a horizontal step, else vertical
const horizontal = Math.abs(path[i].x - path[i-1].x) > Math.abs(path[i].y - path[i-1].y);
ctx.beginPath();
ctx.moveTo(x0, y0); ctx.lineTo(x1, y1);
ctx.strokeStyle = horizontal ? '#1d6fa5' : '#e07020';
ctx.lineWidth = 2.5;
ctx.stroke();
// Arrow tip
const angle = Math.atan2(y1-y0, x1-x0);
ctx.save();
ctx.translate(x1, y1);
ctx.rotate(angle);
ctx.beginPath();
ctx.moveTo(0,0); ctx.lineTo(-10, -5); ctx.lineTo(-10, 5); ctx.closePath();
ctx.fillStyle = horizontal ? '#1d6fa5' : '#e07020';
ctx.fill();
ctx.restore();
}
// Start marker
const [sx, sy] = toCanvas(path[0].x, path[0].y);
ctx.beginPath(); ctx.arc(sx, sy, 6, 0, 2*Math.PI);
ctx.fillStyle = '#c92f3c'; ctx.fill();
// Current position
const last = path[path.length-1];
const [lx, ly] = toCanvas(last.x, last.y);
ctx.beginPath(); ctx.arc(lx, ly, 6, 0, 2*Math.PI);
ctx.fillStyle = '#e07020'; ctx.fill();
}
function drawAxes() {
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0,CY); ctx.lineTo(W,CY); ctx.stroke();
ctx.beginPath(); ctx.moveTo(CX,0); ctx.lineTo(CX,H); ctx.stroke();
ctx.fillStyle = '#999'; ctx.font = '11px system-ui';
ctx.fillText('x', W-12, CY-6);
ctx.fillText('y', CX+6, 12);
}
function render() {
ctx.clearRect(0, 0, W, H);
drawAxes();
drawContours();
drawPath();
}
function dist2(p) { return p.x*p.x + p.y*p.y; }
function updateInfo() {
const last = path[path.length-1];
const fval = last ? f(last.x, last.y).toFixed(4) : '—';
const d = last ? Math.sqrt(dist2(last)).toFixed(4) : '—';
const done = last && dist2(last) < 0.0001;
const el = document.getElementById('info');
el.className = done ? 'info done' : 'info';
el.textContent = done
? fmt('Converged! f = %fval% (distance to min = %d%)', { fval: fval, d: d })
: last
? fmt('Step %v% | f(x,y) = %fval% | dist = %d% | next: %v2%', { v: path.length-1, fval: fval, d: d, v2: step%2===0?'minimize x':'minimize y' })
: 'Click on the canvas to set a starting point.';
}
function doStep() {
if (path.length === 0) { updateInfo(); return; }
const last = path[path.length-1];
if (dist2(last) < 0.0001) { updateInfo(); return; }
path.push(cdStep(last));
render();
updateInfo();
}
function parseShape() {
const [av, bv] = document.getElementById('shape').value.split(',').map(Number);
a = av; b = bv;
}
document.getElementById('shape').addEventListener('change', () => {
parseShape(); path = []; step = 0; render(); updateInfo();
});
document.getElementById('stepBtn').addEventListener('click', () => {
stopAnim(); doStep();
});
document.getElementById('runBtn').addEventListener('click', () => {
if (animId) { stopAnim(); return; }
if (path.length === 0) { updateInfo(); return; }
runAnim();
});
document.getElementById('resetBtn').addEventListener('click', () => {
stopAnim(); path = []; step = 0; render(); updateInfo();
});
function stopAnim() {
if (animId) { clearInterval(animId); animId = null; }
document.getElementById('runBtn').textContent = 'Run';
}
function runAnim() {
document.getElementById('runBtn').textContent = 'Pause';
const delay = parseInt(document.getElementById('speed').value, 10);
animId = setInterval(() => {
const last = path[path.length-1];
if (!last || dist2(last) < 0.0001) { stopAnim(); return; }
doStep();
}, delay);
}
function handleClick(e) {
stopAnim();
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width;
const scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
const [mx, my] = toMath(px, py);
path = [{x: mx, y: my}];
step = 0;
render();
updateInfo();
}
canvas.addEventListener('click', handleClick);
// Initial render
parseShape();
render();
updateInfo();
// Verification log (used by test harness)
(function verifyCD() {
// Verify: for f(x,y)=a*x^2+b*y^2+C*x*y, starting from (3,3)
// After step 1 (min over x): x = -C*y/(2a) = -1.8*3/(2*0.5) = -5.4
// After step 2 (min over y): y = -C*x/(2b) = -1.8*(-5.4)/(2*4) = 1.215
// Check these by running the cdStep logic
const startX = 3, startY = 3;
let px = startX, py = startY;
let s = 0;
function step1(x, y, st) {
if (st % 2 === 0) return {x: -C*y/(2*a), y: y};
return {x: x, y: -C*x/(2*b)};
}
const r1 = step1(px, py, s); s++;
const expectedX1 = -C*startY/(2*a); // = -1.8*3/1 = -5.4
const r2 = step1(r1.x, r1.y, s); s++;
const expectedY2 = -C*r1.x/(2*b); // = -1.8*(-5.4)/8 = 1.215
console.assert(Math.abs(r1.x - expectedX1) < 1e-9, 'Step 1 x update failed');
console.assert(Math.abs(r2.y - expectedY2) < 1e-9, 'Step 2 y update failed');
console.log('CD verification: step1 x =', r1.x.toFixed(4), '(expected', expectedX1.toFixed(4)+')');
console.log('CD verification: step2 y =', r2.y.toFixed(4), '(expected', expectedY2.toFixed(4)+')');
console.log('Coordinate descent demo verified OK');
})();
Comments
Loading comments...