The canvas below shows a 2D landscape as a contour map. The three colored dots are the corners of the simplex — the worst corner (red), the second-worst (orange), and the best (green). Press Step to apply one Nelder-Mead operation, or Run to animate until convergence.
<div class="controls">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
<span class="info" id="info">Step 0</span>
</div>
<canvas id="canvas" width="380" height="320"></canvas>
<div class="legend">
<span class="dot worst"></span> Worst
<span class="dot second"></span> 2nd-worst
<span class="dot best"></span> Best
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f4f7fa; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; padding: .5rem 0 .4rem; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.info { font-size: .88rem; color: #444; margin-left: .3rem; }
canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3;
background: #fff; max-width: 100%; }
.legend { font-size: .8rem; color: #555; margin-top: .4rem; display: flex; align-items: center; gap: .2rem; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; }
.dot.worst { background: #e63946; }
.dot.second { background: #f4a261; }
.dot.best { background: #2a9d8f; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Nelder-Mead simplex method in 2D — demo on the Himmelblau function.
// f(x,y) = (x^2+y-11)^2 + (x+y^2-7)^2 minima ≈ 0 at 4 symmetric points.
// We search in [-5,5]x[-5,5] and draw the simplex on a contour canvas.
const W = 380, H = 320;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Map: domain [-5,5] <-> canvas pixels
const DX = 10, DY = 10; // domain width/height
const px = x => ((x + 5) / DX) * W;
const py = y => H - ((y + 5) / DY) * H;
const ix = p => (p / W) * DX - 5;
const iy = p => 5 - (p / H) * DY;
function f(p) {
const x = p[0], y = p[1];
return Math.pow(x*x + y - 11, 2) + Math.pow(x + y*y - 7, 2);
}
// Precompute contour image
function drawContours() {
const img = ctx.createImageData(W, H);
for (let py2 = 0; py2 < H; py2++) {
for (let px2 = 0; px2 < W; px2++) {
const x = ix(px2), y = iy(py2);
const v = f([x, y]);
// log-scale color
const t = Math.min(1, Math.log(1 + v) / Math.log(1 + 500));
// gradient: deep blue (low) -> white (high)
const r = Math.round(220 * t + 30 * (1-t));
const g = Math.round(235 * t + 80 * (1-t));
const b = Math.round(255 * t + 160 * (1-t));
const i = (py2 * W + px2) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b; img.data[i+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
}
// ── Nelder-Mead constants ─────────────────────────────────────────────────
const ALPHA = 1.0, GAMMA = 2.0, RHO = 0.5, SIGMA = 0.5;
let simplex, stepCount, running, timer;
function initSimplex() {
// Random starting simplex in [-4,4]x[-4,4]
const cx = (Math.random() - 0.5) * 8;
const cy = (Math.random() - 0.5) * 8;
const r = 2 + Math.random();
simplex = [
[cx, cy],
[cx + r * Math.cos(0), cy + r * Math.sin(0)],
[cx + r * Math.cos(2.1), cy + r * Math.sin(2.1)]
];
stepCount = 0;
}
function clamp(p) {
return [Math.max(-5, Math.min(5, p[0])), Math.max(-5, Math.min(5, p[1]))];
}
function add(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
function sub(a, b) { return [a[0]-b[0], a[1]-b[1]]; }
function scale(a, s) { return [a[0]*s, a[1]*s]; }
function nmStep() {
// 1. Order
simplex.sort((a, b) => f(a) - f(b));
const [best, mid, worst] = simplex;
// 2. Centroid of best two
const cen = scale(add(best, mid), 0.5);
// 3. Reflect
const ref = clamp(add(cen, scale(sub(cen, worst), ALPHA)));
const fr = f(ref);
if (fr < f(best)) {
// 4. Expand
const exp = clamp(add(cen, scale(sub(ref, cen), GAMMA)));
simplex[2] = f(exp) < fr ? exp : ref;
} else if (fr < f(mid)) {
simplex[2] = ref;
} else {
// 5. Contract
const con = clamp(add(cen, scale(sub(worst, cen), RHO)));
if (f(con) < f(worst)) {
simplex[2] = con;
} else {
// 6. Shrink
for (let i = 1; i < 3; i++)
simplex[i] = clamp(add(best, scale(sub(simplex[i], best), SIGMA)));
}
}
stepCount++;
}
function size() {
// Largest edge length of simplex
const d = (a, b) => Math.hypot(a[0]-b[0], a[1]-b[1]);
return Math.max(d(simplex[0],simplex[1]), d(simplex[1],simplex[2]), d(simplex[0],simplex[2]));
}
function draw() {
ctx.drawImage(contourBuf, 0, 0);
simplex.sort((a, b) => f(a) - f(b));
const [best, mid, worst] = simplex;
// Draw simplex triangle
ctx.beginPath();
ctx.moveTo(px(worst[0]), py(worst[1]));
ctx.lineTo(px(mid[0]), py(mid[1]));
ctx.lineTo(px(best[0]), py(best[1]));
ctx.closePath();
ctx.strokeStyle = 'rgba(29,53,87,0.7)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = 'rgba(29,53,87,0.08)';
ctx.fill();
// Vertices
const dots = [
{ p: worst, color: '#e63946' },
{ p: mid, color: '#f4a261' },
{ p: best, color: '#2a9d8f' }
];
dots.forEach(({ p, color }) => {
ctx.beginPath();
ctx.arc(px(p[0]), py(p[1]), 6, 0, Math.PI*2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
// Step label
const bv = f(best).toFixed(4);
document.getElementById('info').textContent =
fmt('Step %stepCount% — best f = %bv%', { stepCount: stepCount, bv: bv });
}
// Pre-render contours to an offscreen canvas
const offscreen = document.createElement('canvas');
offscreen.width = W; offscreen.height = H;
const octx = offscreen.getContext('2d');
const realCtx = ctx;
// swap ctx temporarily
const savedCtx = ctx;
// Draw to offscreen
const offCtx = offscreen.getContext('2d');
(function() {
const img = offCtx.createImageData(W, H);
for (let py2 = 0; py2 < H; py2++) {
for (let px2 = 0; px2 < W; px2++) {
const x = ix(px2), y = iy(py2);
const v = f([x, y]);
const t = Math.min(1, Math.log(1 + v) / Math.log(1 + 500));
const r = Math.round(220 * t + 30 * (1-t));
const g = Math.round(235 * t + 80 * (1-t));
const b = Math.round(255 * t + 160 * (1-t));
const i = (py2 * W + px2) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b; img.data[i+3] = 255;
}
}
offCtx.putImageData(img, 0, 0);
})();
const contourBuf = offscreen;
function reset() {
clearInterval(timer); running = false;
document.getElementById('btnRun').textContent = 'Run';
document.getElementById('btnRun').disabled = false;
document.getElementById('btnStep').disabled = false;
initSimplex();
draw();
}
document.getElementById('btnStep').addEventListener('click', () => {
nmStep(); draw();
});
document.getElementById('btnRun').addEventListener('click', () => {
if (running) {
clearInterval(timer); running = false;
document.getElementById('btnRun').textContent = 'Run';
document.getElementById('btnStep').disabled = false;
} else {
running = true;
document.getElementById('btnRun').textContent = 'Pause';
document.getElementById('btnStep').disabled = true;
timer = setInterval(() => {
nmStep(); draw();
if (size() < 0.001 || f(simplex[0]) < 0.0001) {
clearInterval(timer); running = false;
document.getElementById('btnRun').textContent = 'Run';
document.getElementById('btnStep').disabled = false;
}
}, 80);
}
});
document.getElementById('btnReset').addEventListener('click', reset);
reset();
console.log('Nelder-Mead demo loaded. Himmelblau minima at ~(3,2),(−2.805,3.131),(−3.779,−3.283),(3.584,−1.848) with f≈0');
<div class="controls">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
<span class="info" id="info">Step 0</span>
</div>
<canvas id="canvas" width="380" height="320"></canvas>
<div class="legend">
<span class="dot worst"></span> Worst
<span class="dot second"></span> 2nd-worst
<span class="dot best"></span> Best
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f4f7fa; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; padding: .5rem 0 .4rem; }
button { font: 600 13px system-ui; padding: .38rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.info { font-size: .88rem; color: #444; margin-left: .3rem; }
canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3;
background: #fff; max-width: 100%; }
.legend { font-size: .8rem; color: #555; margin-top: .4rem; display: flex; align-items: center; gap: .2rem; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; }
.dot.worst { background: #e63946; }
.dot.second { background: #f4a261; }
.dot.best { background: #2a9d8f; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Nelder-Mead simplex method in 2D — demo on the Himmelblau function.
// f(x,y) = (x^2+y-11)^2 + (x+y^2-7)^2 minima ≈ 0 at 4 symmetric points.
// We search in [-5,5]x[-5,5] and draw the simplex on a contour canvas.
const W = 380, H = 320;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Map: domain [-5,5] <-> canvas pixels
const DX = 10, DY = 10; // domain width/height
const px = x => ((x + 5) / DX) * W;
const py = y => H - ((y + 5) / DY) * H;
const ix = p => (p / W) * DX - 5;
const iy = p => 5 - (p / H) * DY;
function f(p) {
const x = p[0], y = p[1];
return Math.pow(x*x + y - 11, 2) + Math.pow(x + y*y - 7, 2);
}
// Precompute contour image
function drawContours() {
const img = ctx.createImageData(W, H);
for (let py2 = 0; py2 < H; py2++) {
for (let px2 = 0; px2 < W; px2++) {
const x = ix(px2), y = iy(py2);
const v = f([x, y]);
// log-scale color
const t = Math.min(1, Math.log(1 + v) / Math.log(1 + 500));
// gradient: deep blue (low) -> white (high)
const r = Math.round(220 * t + 30 * (1-t));
const g = Math.round(235 * t + 80 * (1-t));
const b = Math.round(255 * t + 160 * (1-t));
const i = (py2 * W + px2) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b; img.data[i+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
}
// ── Nelder-Mead constants ─────────────────────────────────────────────────
const ALPHA = 1.0, GAMMA = 2.0, RHO = 0.5, SIGMA = 0.5;
let simplex, stepCount, running, timer;
function initSimplex() {
// Random starting simplex in [-4,4]x[-4,4]
const cx = (Math.random() - 0.5) * 8;
const cy = (Math.random() - 0.5) * 8;
const r = 2 + Math.random();
simplex = [
[cx, cy],
[cx + r * Math.cos(0), cy + r * Math.sin(0)],
[cx + r * Math.cos(2.1), cy + r * Math.sin(2.1)]
];
stepCount = 0;
}
function clamp(p) {
return [Math.max(-5, Math.min(5, p[0])), Math.max(-5, Math.min(5, p[1]))];
}
function add(a, b) { return [a[0]+b[0], a[1]+b[1]]; }
function sub(a, b) { return [a[0]-b[0], a[1]-b[1]]; }
function scale(a, s) { return [a[0]*s, a[1]*s]; }
function nmStep() {
// 1. Order
simplex.sort((a, b) => f(a) - f(b));
const [best, mid, worst] = simplex;
// 2. Centroid of best two
const cen = scale(add(best, mid), 0.5);
// 3. Reflect
const ref = clamp(add(cen, scale(sub(cen, worst), ALPHA)));
const fr = f(ref);
if (fr < f(best)) {
// 4. Expand
const exp = clamp(add(cen, scale(sub(ref, cen), GAMMA)));
simplex[2] = f(exp) < fr ? exp : ref;
} else if (fr < f(mid)) {
simplex[2] = ref;
} else {
// 5. Contract
const con = clamp(add(cen, scale(sub(worst, cen), RHO)));
if (f(con) < f(worst)) {
simplex[2] = con;
} else {
// 6. Shrink
for (let i = 1; i < 3; i++)
simplex[i] = clamp(add(best, scale(sub(simplex[i], best), SIGMA)));
}
}
stepCount++;
}
function size() {
// Largest edge length of simplex
const d = (a, b) => Math.hypot(a[0]-b[0], a[1]-b[1]);
return Math.max(d(simplex[0],simplex[1]), d(simplex[1],simplex[2]), d(simplex[0],simplex[2]));
}
function draw() {
ctx.drawImage(contourBuf, 0, 0);
simplex.sort((a, b) => f(a) - f(b));
const [best, mid, worst] = simplex;
// Draw simplex triangle
ctx.beginPath();
ctx.moveTo(px(worst[0]), py(worst[1]));
ctx.lineTo(px(mid[0]), py(mid[1]));
ctx.lineTo(px(best[0]), py(best[1]));
ctx.closePath();
ctx.strokeStyle = 'rgba(29,53,87,0.7)';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = 'rgba(29,53,87,0.08)';
ctx.fill();
// Vertices
const dots = [
{ p: worst, color: '#e63946' },
{ p: mid, color: '#f4a261' },
{ p: best, color: '#2a9d8f' }
];
dots.forEach(({ p, color }) => {
ctx.beginPath();
ctx.arc(px(p[0]), py(p[1]), 6, 0, Math.PI*2);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
});
// Step label
const bv = f(best).toFixed(4);
document.getElementById('info').textContent =
fmt('Step %stepCount% — best f = %bv%', { stepCount: stepCount, bv: bv });
}
// Pre-render contours to an offscreen canvas
const offscreen = document.createElement('canvas');
offscreen.width = W; offscreen.height = H;
const octx = offscreen.getContext('2d');
const realCtx = ctx;
// swap ctx temporarily
const savedCtx = ctx;
// Draw to offscreen
const offCtx = offscreen.getContext('2d');
(function() {
const img = offCtx.createImageData(W, H);
for (let py2 = 0; py2 < H; py2++) {
for (let px2 = 0; px2 < W; px2++) {
const x = ix(px2), y = iy(py2);
const v = f([x, y]);
const t = Math.min(1, Math.log(1 + v) / Math.log(1 + 500));
const r = Math.round(220 * t + 30 * (1-t));
const g = Math.round(235 * t + 80 * (1-t));
const b = Math.round(255 * t + 160 * (1-t));
const i = (py2 * W + px2) * 4;
img.data[i] = r; img.data[i+1] = g; img.data[i+2] = b; img.data[i+3] = 255;
}
}
offCtx.putImageData(img, 0, 0);
})();
const contourBuf = offscreen;
function reset() {
clearInterval(timer); running = false;
document.getElementById('btnRun').textContent = 'Run';
document.getElementById('btnRun').disabled = false;
document.getElementById('btnStep').disabled = false;
initSimplex();
draw();
}
document.getElementById('btnStep').addEventListener('click', () => {
nmStep(); draw();
});
document.getElementById('btnRun').addEventListener('click', () => {
if (running) {
clearInterval(timer); running = false;
document.getElementById('btnRun').textContent = 'Run';
document.getElementById('btnStep').disabled = false;
} else {
running = true;
document.getElementById('btnRun').textContent = 'Pause';
document.getElementById('btnStep').disabled = true;
timer = setInterval(() => {
nmStep(); draw();
if (size() < 0.001 || f(simplex[0]) < 0.0001) {
clearInterval(timer); running = false;
document.getElementById('btnRun').textContent = 'Run';
document.getElementById('btnStep').disabled = false;
}
}, 80);
}
});
document.getElementById('btnReset').addEventListener('click', reset);
reset();
console.log('Nelder-Mead demo loaded. Himmelblau minima at ~(3,2),(−2.805,3.131),(−3.779,−3.283),(3.584,−1.848) with f≈0');
Comments
Loading comments...