The demo below runs the ellipsoid algorithm on a small 2D linear program. The blue region is the feasible polytope — the set of points satisfying all constraints. The algorithm starts with a large ellipse guaranteed to contain the optimum and, at each step, cuts the ellipse in half with the most-violated constraint, then replaces it with the smallest ellipse containing the surviving half.
<p class="hint">A 2D linear program: maximize <b>c·x</b> subject to <b>Ax ≤ b</b>.
The blue region is the feasible polytope. The ellipse shrinks each step by cutting
at the most-violated constraint and taking the minimum enclosing ellipse of the half
that could still contain the optimum.</p>
<canvas id="cv" width="340" height="280"></canvas>
<div class="info" id="info">Press <b>Step</b> or <b>Run</b> to start.</div>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; max-width: 100%; }
.info { font-size: .9rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1d3557; }
.info.done { color: #0a7d33; }
.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] ?? ''));
// Ellipsoid method in 2D (simplified but correct).
// Problem: maximize c·x subject to A x <= b (x in R^2).
// We find the minimum by minimizing -c·x.
// Constraints define a feasible polygon rendered in blue.
//
// State: ellipsoid E = { x : (x-c)^T P^{-1} (x-c) <= 1 }
// represented by center vector 'ctr' and 2×2 positive-definite matrix 'M'
// (M = P, the shape matrix).
//
// Update rule (Khachiyan): given a violated constraint a·x > b at center c:
// g = P a / sqrt(a^T P a) (normalised gradient in ellipsoid metric)
// c_new = c - (1/(n+1)) g
// P_new = (n^2/(n^2-1)) ( P - (2/(n+1)) g g^T )
// Volume reduction factor: e^{-1/(2n)} per step (n=2 => ~0.78 per step).
const W = 340, H = 280;
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
// World coordinates: x in [-1, 10], y in [-1, 10]
const XMIN = -1, XMAX = 10, YMIN = -1, YMAX = 10;
function toScreen(wx, wy) {
return [
(wx - XMIN) / (XMAX - XMIN) * W,
H - (wy - YMIN) / (YMAX - YMIN) * H
];
}
function toWorld(sx, sy) {
return [
sx / W * (XMAX - XMIN) + XMIN,
(H - sy) / H * (YMAX - YMIN) + YMIN
];
}
// Feasible region: A x <= b (each row of A, each entry of b)
// x >= 0, y >= 0, x + y <= 8, 2x + y <= 12, x + 2y <= 12
const A = [
[-1, 0], // -x <= 0 => x >= 0
[ 0, -1], // -y <= 0 => y >= 0
[ 1, 1], // x + y <= 8
[ 2, 1], // 2x + y <= 12
[ 1, 2], // x + 2y <= 12
];
const b = [0, 0, 8, 12, 12];
// Objective: maximize c·x = maximize x + y => minimise -(x+y)
const objC = [1, 1]; // objective direction (maximise)
const gradMin = [-1, -1]; // gradient for minimisation (-objC)
// True optimum: x=4, y=4 (corner of x+y=8, x+2y=12 => y=4, x=4)
const OPT = [4, 4];
// 2×2 matrix helpers
function mScale(s, M) { return [[s*M[0][0], s*M[0][1]], [s*M[1][0], s*M[1][1]]]; }
function mAdd(A2, B2) { return [[A2[0][0]+B2[0][0], A2[0][1]+B2[0][1]], [A2[1][0]+B2[1][0], A2[1][1]+B2[1][1]]]; }
function mSub(A2, B2) { return [[A2[0][0]-B2[0][0], A2[0][1]-B2[0][1]], [A2[1][0]-B2[1][0], A2[1][1]-B2[1][1]]]; }
function mMulVec(M, v) { return [M[0][0]*v[0]+M[0][1]*v[1], M[1][0]*v[0]+M[1][1]*v[1]]; }
function dot(u, v) { return u[0]*v[0] + u[1]*v[1]; }
function outerProduct(u, v) { return [[u[0]*v[0], u[0]*v[1]], [u[1]*v[0], u[1]*v[1]]]; }
function vScale(s, v) { return [s*v[0], s*v[1]]; }
function vSub(u, v) { return [u[0]-v[0], u[1]-v[1]]; }
function vAdd(u, v) { return [u[0]+v[0], u[1]+v[1]]; }
// Find a violated constraint at point x; returns index or -1 if feasible.
function findViolated(x) {
let worst = -1, worstSlack = 0;
for (let i = 0; i < A.length; i++) {
const slack = dot(A[i], x) - b[i];
if (slack > worstSlack) { worstSlack = slack; worst = i; }
}
return worst;
}
// Ellipsoid update: minimising gradMin direction, or cutting at constraint i.
// If x is feasible, we cut in the objective gradient direction to improve.
// Returns { ctr, M } for new ellipsoid.
function ellipsoidStep(ctr, M) {
const n = 2;
let a; // the separating hyperplane normal
const vi = findViolated(ctr);
if (vi >= 0) {
a = A[vi]; // violated constraint gradient
} else {
a = gradMin; // feasible but can improve (cut towards optimum)
}
const Pa = mMulVec(M, a);
const aPa = dot(a, Pa);
if (aPa <= 0) return { ctr, M }; // degenerate
const sqrtAPa = Math.sqrt(aPa);
// g = Pa / sqrt(a^T P a)
const g = vScale(1 / sqrtAPa, Pa);
// c_new = c - (1/(n+1)) g
const ctrNew = vSub(ctr, vScale(1 / (n + 1), g));
// P_new = (n^2/(n^2-1)) * (P - (2/(n+1)) * g g^T)
const factor = (n * n) / (n * n - 1);
const ggT = outerProduct(g, g);
const Mnew = mScale(factor, mSub(M, mScale(2 / (n + 1), ggT)));
return { ctr: ctrNew, M: Mnew };
}
// Initial ellipsoid: large ball centred at (5, 5), radius 9 (covers feasible region)
const R0 = 9;
function initialState() {
return {
ctr: [5, 5],
M: [[R0*R0, 0], [0, R0*R0]],
step: 0,
done: false,
history: []
};
}
let state = initialState();
// Compute vertices of feasible polygon for drawing (clipped to world bounds).
function feasiblePolygon() {
// Grid-sample to approximate polygon boundary (good enough for display).
const pts = [];
const eps = 0.05;
for (let xi = XMIN; xi <= XMAX; xi += eps) {
for (let yi = YMIN; yi <= YMAX; yi += eps) {
const x = [xi, yi];
let ok = true;
for (let i = 0; i < A.length; i++) if (dot(A[i], x) > b[i] + 1e-6) { ok = false; break; }
if (ok) pts.push([xi, yi]);
}
}
return pts;
}
// Compute actual polygon corners (intersections of constraint pairs).
function feasibleCorners() {
const corners = [];
for (let i = 0; i < A.length; i++) {
for (let j = i+1; j < A.length; j++) {
const det = A[i][0]*A[j][1] - A[i][1]*A[j][0];
if (Math.abs(det) < 1e-9) continue;
const x = (b[i]*A[j][1] - b[j]*A[i][1]) / det;
const y = (A[i][0]*b[j] - A[j][0]*b[i]) / det;
let feasible = true;
for (let k = 0; k < A.length; k++) if (dot(A[k], [x,y]) > b[k] + 1e-6) { feasible = false; break; }
if (feasible && x >= XMIN-1 && x <= XMAX+1 && y >= YMIN-1 && y <= YMAX+1) {
corners.push([x, y]);
}
}
}
// Sort corners by angle for polygon drawing
const cx = corners.reduce((s,p)=>s+p[0],0)/corners.length;
const cy = corners.reduce((s,p)=>s+p[1],0)/corners.length;
corners.sort((a,c) => Math.atan2(a[1]-cy,a[0]-cx) - Math.atan2(c[1]-cy,c[0]-cx));
return corners;
}
const corners = feasibleCorners();
function drawEllipse(ctr, M) {
// Eigendecompose M to get axes and angle.
// For 2x2 symmetric [[a,b],[b,d]]:
const a = M[0][0], bv = M[0][1], d = M[1][1];
const tr = a + d, det = a*d - bv*bv;
const disc = Math.sqrt(Math.max(0, tr*tr/4 - det));
const lam1 = tr/2 + disc;
const lam2 = tr/2 - disc;
if (lam1 <= 0 || lam2 <= 0) return;
const rx = Math.sqrt(lam1), ry = Math.sqrt(Math.max(lam2, 1e-9));
// Eigenvector for lam1: (M - lam1 I) v = 0
let angle = 0;
if (Math.abs(bv) > 1e-10) {
angle = Math.atan2(lam1 - a, bv);
} else {
angle = (a >= d) ? 0 : Math.PI/2;
}
// Convert axes from world to screen scale (approximate — aspect ratio)
const scaleX = W / (XMAX - XMIN);
const scaleY = H / (YMAX - YMIN);
// Centre in screen
const [cx, cy] = toScreen(ctr[0], ctr[1]);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-angle); // screen y flipped
ctx.scale(rx * scaleX, ry * scaleY);
ctx.beginPath();
ctx.arc(0, 0, 1, 0, 2*Math.PI);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw feasible polygon
if (corners.length > 2) {
ctx.beginPath();
const [sx, sy] = toScreen(corners[0][0], corners[0][1]);
ctx.moveTo(sx, sy);
for (let i = 1; i < corners.length; i++) {
const [px, py] = toScreen(corners[i][0], corners[i][1]);
ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fillStyle = 'rgba(29, 53, 87, 0.15)';
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// Draw grid axes
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 0.8;
const [ox, oy] = toScreen(0, 0);
ctx.beginPath(); ctx.moveTo(ox, 0); ctx.lineTo(ox, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, oy); ctx.lineTo(W, oy); ctx.stroke();
// Draw ellipse (current)
drawEllipse(state.ctr, state.M);
ctx.strokeStyle = 'rgba(230, 57, 70, 0.7)';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = 'rgba(230, 57, 70, 0.08)';
ctx.fill();
// Draw history ellipses (faded)
for (let h of state.history) {
drawEllipse(h.ctr, h.M);
ctx.strokeStyle = 'rgba(230, 57, 70, 0.2)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Draw optimum star
const [ox2, oy2] = toScreen(OPT[0], OPT[1]);
ctx.fillStyle = '#0a7d33';
ctx.beginPath(); ctx.arc(ox2, oy2, 5, 0, 2*Math.PI); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(ox2, oy2, 2, 0, 2*Math.PI); ctx.fill();
// Draw current center
const [ccx, ccy] = toScreen(state.ctr[0], state.ctr[1]);
ctx.fillStyle = '#e63946';
ctx.beginPath(); ctx.arc(ccx, ccy, 4, 0, 2*Math.PI); ctx.fill();
}
function isFeasible(x) {
for (let i = 0; i < A.length; i++) if (dot(A[i], x) > b[i] + 1e-4) return false;
return true;
}
function step() {
if (state.done) return;
state.history.push({ ctr: state.ctr, M: state.M });
if (state.history.length > 30) state.history.shift();
const result = ellipsoidStep(state.ctr, state.M);
state.ctr = result.ctr;
state.M = result.M;
state.step++;
// Check if volume is negligibly small (converged)
const vol = Math.sqrt(Math.max(0, state.M[0][0]*state.M[1][1] - state.M[0][1]*state.M[1][0]));
const feasible = isFeasible(state.ctr);
const info = document.getElementById('info');
if (vol < 0.01 || state.step >= 120) {
state.done = true;
const dist = Math.sqrt((state.ctr[0]-OPT[0])**2 + (state.ctr[1]-OPT[1])**2);
info.textContent = fmt('Converged in %step% steps — centre (%v%, %v2%), dist to opt: %dist%', { step: state.step, v: state.ctr[0].toFixed(2), v2: state.ctr[1].toFixed(2), dist: dist.toFixed(3) });
info.className = 'info done';
} else {
info.textContent = fmt('Step %step% — centre (%v%, %v2%)%v3% | vol ∝ %vol%', { step: state.step, v: state.ctr[0].toFixed(2), v2: state.ctr[1].toFixed(2), v3: feasible ? ' [feasible]' : ' [infeasible]', vol: vol.toFixed(3) });
info.className = 'info';
}
draw();
}
let timer = null;
document.getElementById('btnStep').addEventListener('click', () => {
clearInterval(timer); timer = null;
step();
});
document.getElementById('btnRun').addEventListener('click', () => {
if (timer) { clearInterval(timer); timer = null; return; }
timer = setInterval(() => { step(); if (state.done) { clearInterval(timer); timer = null; } }, 120);
});
document.getElementById('btnReset').addEventListener('click', () => {
clearInterval(timer); timer = null;
state = initialState();
document.getElementById('info').textContent = 'Press Step or Run to start.';
document.getElementById('info').className = 'info';
draw();
});
draw();
console.log('Ellipsoid method demo loaded. True optimum:', OPT, 'obj =', dot(objC, OPT));
<p class="hint">A 2D linear program: maximize <b>c·x</b> subject to <b>Ax ≤ b</b>.
The blue region is the feasible polytope. The ellipse shrinks each step by cutting
at the most-violated constraint and taking the minimum enclosing ellipse of the half
that could still contain the optimum.</p>
<canvas id="cv" width="340" height="280"></canvas>
<div class="info" id="info">Press <b>Step</b> or <b>Run</b> to start.</div>
<div class="btns">
<button id="btnStep" type="button">Step</button>
<button id="btnRun" type="button">Run</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; max-width: 100%; }
.info { font-size: .9rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; color: #1d3557; }
.info.done { color: #0a7d33; }
.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] ?? ''));
// Ellipsoid method in 2D (simplified but correct).
// Problem: maximize c·x subject to A x <= b (x in R^2).
// We find the minimum by minimizing -c·x.
// Constraints define a feasible polygon rendered in blue.
//
// State: ellipsoid E = { x : (x-c)^T P^{-1} (x-c) <= 1 }
// represented by center vector 'ctr' and 2×2 positive-definite matrix 'M'
// (M = P, the shape matrix).
//
// Update rule (Khachiyan): given a violated constraint a·x > b at center c:
// g = P a / sqrt(a^T P a) (normalised gradient in ellipsoid metric)
// c_new = c - (1/(n+1)) g
// P_new = (n^2/(n^2-1)) ( P - (2/(n+1)) g g^T )
// Volume reduction factor: e^{-1/(2n)} per step (n=2 => ~0.78 per step).
const W = 340, H = 280;
const canvas = document.getElementById('cv');
const ctx = canvas.getContext('2d');
// World coordinates: x in [-1, 10], y in [-1, 10]
const XMIN = -1, XMAX = 10, YMIN = -1, YMAX = 10;
function toScreen(wx, wy) {
return [
(wx - XMIN) / (XMAX - XMIN) * W,
H - (wy - YMIN) / (YMAX - YMIN) * H
];
}
function toWorld(sx, sy) {
return [
sx / W * (XMAX - XMIN) + XMIN,
(H - sy) / H * (YMAX - YMIN) + YMIN
];
}
// Feasible region: A x <= b (each row of A, each entry of b)
// x >= 0, y >= 0, x + y <= 8, 2x + y <= 12, x + 2y <= 12
const A = [
[-1, 0], // -x <= 0 => x >= 0
[ 0, -1], // -y <= 0 => y >= 0
[ 1, 1], // x + y <= 8
[ 2, 1], // 2x + y <= 12
[ 1, 2], // x + 2y <= 12
];
const b = [0, 0, 8, 12, 12];
// Objective: maximize c·x = maximize x + y => minimise -(x+y)
const objC = [1, 1]; // objective direction (maximise)
const gradMin = [-1, -1]; // gradient for minimisation (-objC)
// True optimum: x=4, y=4 (corner of x+y=8, x+2y=12 => y=4, x=4)
const OPT = [4, 4];
// 2×2 matrix helpers
function mScale(s, M) { return [[s*M[0][0], s*M[0][1]], [s*M[1][0], s*M[1][1]]]; }
function mAdd(A2, B2) { return [[A2[0][0]+B2[0][0], A2[0][1]+B2[0][1]], [A2[1][0]+B2[1][0], A2[1][1]+B2[1][1]]]; }
function mSub(A2, B2) { return [[A2[0][0]-B2[0][0], A2[0][1]-B2[0][1]], [A2[1][0]-B2[1][0], A2[1][1]-B2[1][1]]]; }
function mMulVec(M, v) { return [M[0][0]*v[0]+M[0][1]*v[1], M[1][0]*v[0]+M[1][1]*v[1]]; }
function dot(u, v) { return u[0]*v[0] + u[1]*v[1]; }
function outerProduct(u, v) { return [[u[0]*v[0], u[0]*v[1]], [u[1]*v[0], u[1]*v[1]]]; }
function vScale(s, v) { return [s*v[0], s*v[1]]; }
function vSub(u, v) { return [u[0]-v[0], u[1]-v[1]]; }
function vAdd(u, v) { return [u[0]+v[0], u[1]+v[1]]; }
// Find a violated constraint at point x; returns index or -1 if feasible.
function findViolated(x) {
let worst = -1, worstSlack = 0;
for (let i = 0; i < A.length; i++) {
const slack = dot(A[i], x) - b[i];
if (slack > worstSlack) { worstSlack = slack; worst = i; }
}
return worst;
}
// Ellipsoid update: minimising gradMin direction, or cutting at constraint i.
// If x is feasible, we cut in the objective gradient direction to improve.
// Returns { ctr, M } for new ellipsoid.
function ellipsoidStep(ctr, M) {
const n = 2;
let a; // the separating hyperplane normal
const vi = findViolated(ctr);
if (vi >= 0) {
a = A[vi]; // violated constraint gradient
} else {
a = gradMin; // feasible but can improve (cut towards optimum)
}
const Pa = mMulVec(M, a);
const aPa = dot(a, Pa);
if (aPa <= 0) return { ctr, M }; // degenerate
const sqrtAPa = Math.sqrt(aPa);
// g = Pa / sqrt(a^T P a)
const g = vScale(1 / sqrtAPa, Pa);
// c_new = c - (1/(n+1)) g
const ctrNew = vSub(ctr, vScale(1 / (n + 1), g));
// P_new = (n^2/(n^2-1)) * (P - (2/(n+1)) * g g^T)
const factor = (n * n) / (n * n - 1);
const ggT = outerProduct(g, g);
const Mnew = mScale(factor, mSub(M, mScale(2 / (n + 1), ggT)));
return { ctr: ctrNew, M: Mnew };
}
// Initial ellipsoid: large ball centred at (5, 5), radius 9 (covers feasible region)
const R0 = 9;
function initialState() {
return {
ctr: [5, 5],
M: [[R0*R0, 0], [0, R0*R0]],
step: 0,
done: false,
history: []
};
}
let state = initialState();
// Compute vertices of feasible polygon for drawing (clipped to world bounds).
function feasiblePolygon() {
// Grid-sample to approximate polygon boundary (good enough for display).
const pts = [];
const eps = 0.05;
for (let xi = XMIN; xi <= XMAX; xi += eps) {
for (let yi = YMIN; yi <= YMAX; yi += eps) {
const x = [xi, yi];
let ok = true;
for (let i = 0; i < A.length; i++) if (dot(A[i], x) > b[i] + 1e-6) { ok = false; break; }
if (ok) pts.push([xi, yi]);
}
}
return pts;
}
// Compute actual polygon corners (intersections of constraint pairs).
function feasibleCorners() {
const corners = [];
for (let i = 0; i < A.length; i++) {
for (let j = i+1; j < A.length; j++) {
const det = A[i][0]*A[j][1] - A[i][1]*A[j][0];
if (Math.abs(det) < 1e-9) continue;
const x = (b[i]*A[j][1] - b[j]*A[i][1]) / det;
const y = (A[i][0]*b[j] - A[j][0]*b[i]) / det;
let feasible = true;
for (let k = 0; k < A.length; k++) if (dot(A[k], [x,y]) > b[k] + 1e-6) { feasible = false; break; }
if (feasible && x >= XMIN-1 && x <= XMAX+1 && y >= YMIN-1 && y <= YMAX+1) {
corners.push([x, y]);
}
}
}
// Sort corners by angle for polygon drawing
const cx = corners.reduce((s,p)=>s+p[0],0)/corners.length;
const cy = corners.reduce((s,p)=>s+p[1],0)/corners.length;
corners.sort((a,c) => Math.atan2(a[1]-cy,a[0]-cx) - Math.atan2(c[1]-cy,c[0]-cx));
return corners;
}
const corners = feasibleCorners();
function drawEllipse(ctr, M) {
// Eigendecompose M to get axes and angle.
// For 2x2 symmetric [[a,b],[b,d]]:
const a = M[0][0], bv = M[0][1], d = M[1][1];
const tr = a + d, det = a*d - bv*bv;
const disc = Math.sqrt(Math.max(0, tr*tr/4 - det));
const lam1 = tr/2 + disc;
const lam2 = tr/2 - disc;
if (lam1 <= 0 || lam2 <= 0) return;
const rx = Math.sqrt(lam1), ry = Math.sqrt(Math.max(lam2, 1e-9));
// Eigenvector for lam1: (M - lam1 I) v = 0
let angle = 0;
if (Math.abs(bv) > 1e-10) {
angle = Math.atan2(lam1 - a, bv);
} else {
angle = (a >= d) ? 0 : Math.PI/2;
}
// Convert axes from world to screen scale (approximate — aspect ratio)
const scaleX = W / (XMAX - XMIN);
const scaleY = H / (YMAX - YMIN);
// Centre in screen
const [cx, cy] = toScreen(ctr[0], ctr[1]);
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-angle); // screen y flipped
ctx.scale(rx * scaleX, ry * scaleY);
ctx.beginPath();
ctx.arc(0, 0, 1, 0, 2*Math.PI);
ctx.restore();
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw feasible polygon
if (corners.length > 2) {
ctx.beginPath();
const [sx, sy] = toScreen(corners[0][0], corners[0][1]);
ctx.moveTo(sx, sy);
for (let i = 1; i < corners.length; i++) {
const [px, py] = toScreen(corners[i][0], corners[i][1]);
ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fillStyle = 'rgba(29, 53, 87, 0.15)';
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
}
// Draw grid axes
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 0.8;
const [ox, oy] = toScreen(0, 0);
ctx.beginPath(); ctx.moveTo(ox, 0); ctx.lineTo(ox, H); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, oy); ctx.lineTo(W, oy); ctx.stroke();
// Draw ellipse (current)
drawEllipse(state.ctr, state.M);
ctx.strokeStyle = 'rgba(230, 57, 70, 0.7)';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = 'rgba(230, 57, 70, 0.08)';
ctx.fill();
// Draw history ellipses (faded)
for (let h of state.history) {
drawEllipse(h.ctr, h.M);
ctx.strokeStyle = 'rgba(230, 57, 70, 0.2)';
ctx.lineWidth = 1;
ctx.stroke();
}
// Draw optimum star
const [ox2, oy2] = toScreen(OPT[0], OPT[1]);
ctx.fillStyle = '#0a7d33';
ctx.beginPath(); ctx.arc(ox2, oy2, 5, 0, 2*Math.PI); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(ox2, oy2, 2, 0, 2*Math.PI); ctx.fill();
// Draw current center
const [ccx, ccy] = toScreen(state.ctr[0], state.ctr[1]);
ctx.fillStyle = '#e63946';
ctx.beginPath(); ctx.arc(ccx, ccy, 4, 0, 2*Math.PI); ctx.fill();
}
function isFeasible(x) {
for (let i = 0; i < A.length; i++) if (dot(A[i], x) > b[i] + 1e-4) return false;
return true;
}
function step() {
if (state.done) return;
state.history.push({ ctr: state.ctr, M: state.M });
if (state.history.length > 30) state.history.shift();
const result = ellipsoidStep(state.ctr, state.M);
state.ctr = result.ctr;
state.M = result.M;
state.step++;
// Check if volume is negligibly small (converged)
const vol = Math.sqrt(Math.max(0, state.M[0][0]*state.M[1][1] - state.M[0][1]*state.M[1][0]));
const feasible = isFeasible(state.ctr);
const info = document.getElementById('info');
if (vol < 0.01 || state.step >= 120) {
state.done = true;
const dist = Math.sqrt((state.ctr[0]-OPT[0])**2 + (state.ctr[1]-OPT[1])**2);
info.textContent = fmt('Converged in %step% steps — centre (%v%, %v2%), dist to opt: %dist%', { step: state.step, v: state.ctr[0].toFixed(2), v2: state.ctr[1].toFixed(2), dist: dist.toFixed(3) });
info.className = 'info done';
} else {
info.textContent = fmt('Step %step% — centre (%v%, %v2%)%v3% | vol ∝ %vol%', { step: state.step, v: state.ctr[0].toFixed(2), v2: state.ctr[1].toFixed(2), v3: feasible ? ' [feasible]' : ' [infeasible]', vol: vol.toFixed(3) });
info.className = 'info';
}
draw();
}
let timer = null;
document.getElementById('btnStep').addEventListener('click', () => {
clearInterval(timer); timer = null;
step();
});
document.getElementById('btnRun').addEventListener('click', () => {
if (timer) { clearInterval(timer); timer = null; return; }
timer = setInterval(() => { step(); if (state.done) { clearInterval(timer); timer = null; } }, 120);
});
document.getElementById('btnReset').addEventListener('click', () => {
clearInterval(timer); timer = null;
state = initialState();
document.getElementById('info').textContent = 'Press Step or Run to start.';
document.getElementById('info').className = 'info';
draw();
});
draw();
console.log('Ellipsoid method demo loaded. True optimum:', OPT, 'obj =', dot(objC, OPT));
Notice two things. First, the ellipse always contains at least one optimal point — the algorithm never misses the target. Second, each iteration shrinks the volume by a fixed factor, so after polynomially many steps the ellipse is too small to hold anything except (approximately) the optimum. The red dot marks the current center; when it lands inside the feasible region it becomes the candidate solution.
Comments
Loading comments...