<div class="lp-header">
<div class="lp-info">
<span class="lp-label">Maximize:</span>
<span class="lp-obj" id="obj-display">3x + 2y</span>
</div>
<div class="lp-constraints">
<span class="lp-label">Subject to:</span>
<span>x ≤ 6 | y ≤ 5 | x + y ≤ 9 | x,y ≥ 0</span>
</div>
</div>
<canvas id="lp-canvas" width="340" height="300"></canvas>
<div class="lp-status">
<div class="status-row">
<span class="status-label">Current vertex:</span>
<span id="vertex-label" class="vertex-val">—</span>
</div>
<div class="status-row">
<span class="status-label">Objective value:</span>
<span id="obj-val" class="obj-val">—</span>
</div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="btn-pivot" type="button">Next Pivot</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.lp-header { background: #eef2f7; border-radius: 8px; padding: .55rem .8rem; margin-bottom: .5rem; font-size: .88rem; line-height: 1.7; }
.lp-label { font-weight: 700; margin-right: .3rem; color: #1d3557; }
.lp-obj { font-family: ui-monospace, monospace; color: #1d3557; font-weight: 600; }
.lp-constraints { color: #444; }
#lp-canvas { display: block; border-radius: 10px; background: #f8fafc; border: 1px solid #dde4ec; max-width: 100%; }
.lp-status { display: flex; flex-direction: column; gap: .2rem; margin: .55rem 0; font-size: .93rem; }
.status-row { display: flex; align-items: center; gap: .4rem; }
.status-label { color: #555; font-size: .86rem; }
.vertex-val { font-family: ui-monospace, monospace; font-weight: 700; color: #1d3557; font-size: 1rem; }
.obj-val { font-family: ui-monospace, monospace; font-weight: 700; color: #e63946; font-size: 1rem; }
.msg { font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #0a7d33; }
.msg.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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] ?? ''));
// Linear Program: maximize 3x + 2y
// subject to: x <= 6, y <= 5, x + y <= 9, x >= 0, y >= 0
//
// Vertices of the feasible polytope (computed by intersecting constraints):
// (0,0), (6,0), (6,3), (4,5), (0,5)
// Objective values: 0, 18, 24, 22, 10
// Optimum: (6,3) with value 24
const OBJ = ([x, y]) => 3 * x + 2 * y;
// Named vertices in order around the polytope
const VERTICES = [
{ id: 0, x: 0, y: 0, label: '(0, 0)' },
{ id: 1, x: 6, y: 0, label: '(6, 0)' },
{ id: 2, x: 6, y: 3, label: '(6, 3)' },
{ id: 3, x: 4, y: 5, label: '(4, 5)' },
{ id: 4, x: 0, y: 5, label: '(0, 5)' }
];
// Adjacency: polytope edges (undirected)
const EDGES = [[0,1],[1,2],[2,3],[3,4],[4,0]];
// Neighbors of each vertex
const NEIGHBORS = {};
VERTICES.forEach(v => { NEIGHBORS[v.id] = []; });
EDGES.forEach(([a, b]) => { NEIGHBORS[a].push(b); NEIGHBORS[b].push(a); });
// Simplex: from current vertex, pick the neighbor with the highest objective
// (greedy steepest-edge pivot). Returns null if already optimal.
function simplexPivot(vertexId) {
const neighbors = NEIGHBORS[vertexId];
const curVal = OBJ([VERTICES[vertexId].x, VERTICES[vertexId].y]);
let best = null, bestVal = curVal;
for (const nid of neighbors) {
const v = VERTICES[nid];
const val = OBJ([v.x, v.y]);
if (val > bestVal) { best = nid; bestVal = val; }
}
return best; // null = optimal
}
// Canvas drawing
const canvas = document.getElementById('lp-canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Map LP coords to canvas pixels
const MARGIN = 44;
const scaleX = (W - 2 * MARGIN) / 9; // x domain [0,9]
const scaleY = (H - 2 * MARGIN) / 7; // y domain [0,7]
function toCanvas(x, y) {
return [MARGIN + x * scaleX, H - MARGIN - y * scaleY];
}
function drawScene(currentId, visitedIds, optimalId) {
ctx.clearRect(0, 0, W, H);
// Draw axes
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath();
const [ox, oy] = toCanvas(0, 0);
ctx.moveTo(ox, MARGIN - 10); ctx.lineTo(ox, oy);
ctx.lineTo(W - MARGIN + 10, oy);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#888'; ctx.font = '11px system-ui';
ctx.textAlign = 'center';
for (let i = 0; i <= 9; i += 3) {
const [px, py] = toCanvas(i, 0);
ctx.fillText(i, px, py + 14);
}
ctx.textAlign = 'right';
for (let j = 0; j <= 6; j += 2) {
const [px, py] = toCanvas(0, j);
ctx.fillText(j, px - 6, py + 4);
}
// Draw feasible region (filled polygon)
ctx.beginPath();
VERTICES.forEach((v, i) => {
const [px, py] = toCanvas(v.x, v.y);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
});
ctx.closePath();
ctx.fillStyle = 'rgba(29,53,87,0.09)';
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
// Highlight visited path edges
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.5;
const visited = [...visitedIds];
for (let i = 0; i < visited.length - 1; i++) {
const va = VERTICES[visited[i]], vb = VERTICES[visited[i + 1]];
const [ax, ay] = toCanvas(va.x, va.y);
const [bx, by] = toCanvas(vb.x, vb.y);
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
}
// Draw vertices
VERTICES.forEach(v => {
const [px, py] = toCanvas(v.x, v.y);
const val = OBJ([v.x, v.y]);
const isCurrent = v.id === currentId;
const isVisited = visitedIds.has(v.id);
const isOpt = v.id === optimalId;
ctx.beginPath();
ctx.arc(px, py, isCurrent ? 7 : 5, 0, 2 * Math.PI);
ctx.fillStyle = isCurrent ? '#e63946' : isVisited ? '#e6394688' : '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.stroke();
// Label
ctx.font = `${isCurrent ? 700 : 400} 10.5px system-ui`;
ctx.fillStyle = '#1d3557';
ctx.textAlign = 'left';
const lx = px + 9, ly = py - 6;
ctx.fillText(v.label + ' → ' + val, lx, ly);
});
// Optimal star if reached
if (currentId === optimalId) {
const [px, py] = toCanvas(VERTICES[optimalId].x, VERTICES[optimalId].y);
ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('★', px + 1, py - 14);
}
}
// State
let currentId = 0; // start at origin
let visited = new Set([0]);
let done = false;
const OPTIMAL_ID = 2; // vertex (6,3), obj=24
function updateUI() {
const v = VERTICES[currentId];
document.getElementById('vertex-label').textContent = v.label;
document.getElementById('obj-val').textContent = OBJ([v.x, v.y]);
drawScene(currentId, visited, OPTIMAL_ID);
}
function pivot() {
if (done) return;
const next = simplexPivot(currentId);
const msg = document.getElementById('msg');
if (next === null) {
done = true;
msg.textContent = '★ Optimal! No neighbor improves the objective.';
msg.className = 'msg';
document.getElementById('btn-pivot').disabled = true;
} else {
currentId = next;
visited.add(next);
const val = OBJ([VERTICES[next].x, VERTICES[next].y]);
msg.textContent = fmt('Pivoted to %label% — objective = %val%', { label: VERTICES[next].label, val: val });
msg.className = 'msg';
if (next === OPTIMAL_ID) {
done = true;
msg.textContent = fmt('★ Optimal reached at %label%! Objective = %val%', { label: VERTICES[next].label, val: val });
document.getElementById('btn-pivot').disabled = true;
}
}
updateUI();
}
function reset() {
currentId = 0;
visited = new Set([0]);
done = false;
document.getElementById('msg').textContent = 'Starting at (0, 0). Click "Next Pivot" to step.';
document.getElementById('msg').className = 'msg';
document.getElementById('btn-pivot').disabled = false;
updateUI();
}
document.getElementById('btn-pivot').addEventListener('click', pivot);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial render
document.getElementById('msg').textContent = 'Starting at (0, 0). Click "Next Pivot" to step.';
updateUI();
// Verify: check that greedy pivot from origin reaches the true optimum
let checkId = 0, steps = 0;
while (true) {
const nxt = simplexPivot(checkId);
if (nxt === null) break;
checkId = nxt; steps++;
if (steps > 100) throw new Error('pivot loop not terminating');
}
if (checkId !== OPTIMAL_ID) throw new Error('simplex did not reach optimum: got id=' + checkId);
console.log(fmt('Simplex demo verified: reached optimum %label% (obj=%OBJ%) in %steps% pivots.', { label: VERTICES[OPTIMAL_ID].label, OBJ: OBJ([VERTICES[OPTIMAL_ID].x, VERTICES[OPTIMAL_ID].y]), steps: steps }));
<div class="lp-header">
<div class="lp-info">
<span class="lp-label">Maximize:</span>
<span class="lp-obj" id="obj-display">3x + 2y</span>
</div>
<div class="lp-constraints">
<span class="lp-label">Subject to:</span>
<span>x ≤ 6 | y ≤ 5 | x + y ≤ 9 | x,y ≥ 0</span>
</div>
</div>
<canvas id="lp-canvas" width="340" height="300"></canvas>
<div class="lp-status">
<div class="status-row">
<span class="status-label">Current vertex:</span>
<span id="vertex-label" class="vertex-val">—</span>
</div>
<div class="status-row">
<span class="status-label">Objective value:</span>
<span id="obj-val" class="obj-val">—</span>
</div>
<div id="msg" class="msg"></div>
</div>
<div class="btns">
<button id="btn-pivot" type="button">Next Pivot</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.lp-header { background: #eef2f7; border-radius: 8px; padding: .55rem .8rem; margin-bottom: .5rem; font-size: .88rem; line-height: 1.7; }
.lp-label { font-weight: 700; margin-right: .3rem; color: #1d3557; }
.lp-obj { font-family: ui-monospace, monospace; color: #1d3557; font-weight: 600; }
.lp-constraints { color: #444; }
#lp-canvas { display: block; border-radius: 10px; background: #f8fafc; border: 1px solid #dde4ec; max-width: 100%; }
.lp-status { display: flex; flex-direction: column; gap: .2rem; margin: .55rem 0; font-size: .93rem; }
.status-row { display: flex; align-items: center; gap: .4rem; }
.status-label { color: #555; font-size: .86rem; }
.vertex-val { font-family: ui-monospace, monospace; font-weight: 700; color: #1d3557; font-size: 1rem; }
.obj-val { font-family: ui-monospace, monospace; font-weight: 700; color: #e63946; font-size: 1rem; }
.msg { font-size: .9rem; font-weight: 600; min-height: 1.3em; color: #0a7d33; }
.msg.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
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] ?? ''));
// Linear Program: maximize 3x + 2y
// subject to: x <= 6, y <= 5, x + y <= 9, x >= 0, y >= 0
//
// Vertices of the feasible polytope (computed by intersecting constraints):
// (0,0), (6,0), (6,3), (4,5), (0,5)
// Objective values: 0, 18, 24, 22, 10
// Optimum: (6,3) with value 24
const OBJ = ([x, y]) => 3 * x + 2 * y;
// Named vertices in order around the polytope
const VERTICES = [
{ id: 0, x: 0, y: 0, label: '(0, 0)' },
{ id: 1, x: 6, y: 0, label: '(6, 0)' },
{ id: 2, x: 6, y: 3, label: '(6, 3)' },
{ id: 3, x: 4, y: 5, label: '(4, 5)' },
{ id: 4, x: 0, y: 5, label: '(0, 5)' }
];
// Adjacency: polytope edges (undirected)
const EDGES = [[0,1],[1,2],[2,3],[3,4],[4,0]];
// Neighbors of each vertex
const NEIGHBORS = {};
VERTICES.forEach(v => { NEIGHBORS[v.id] = []; });
EDGES.forEach(([a, b]) => { NEIGHBORS[a].push(b); NEIGHBORS[b].push(a); });
// Simplex: from current vertex, pick the neighbor with the highest objective
// (greedy steepest-edge pivot). Returns null if already optimal.
function simplexPivot(vertexId) {
const neighbors = NEIGHBORS[vertexId];
const curVal = OBJ([VERTICES[vertexId].x, VERTICES[vertexId].y]);
let best = null, bestVal = curVal;
for (const nid of neighbors) {
const v = VERTICES[nid];
const val = OBJ([v.x, v.y]);
if (val > bestVal) { best = nid; bestVal = val; }
}
return best; // null = optimal
}
// Canvas drawing
const canvas = document.getElementById('lp-canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Map LP coords to canvas pixels
const MARGIN = 44;
const scaleX = (W - 2 * MARGIN) / 9; // x domain [0,9]
const scaleY = (H - 2 * MARGIN) / 7; // y domain [0,7]
function toCanvas(x, y) {
return [MARGIN + x * scaleX, H - MARGIN - y * scaleY];
}
function drawScene(currentId, visitedIds, optimalId) {
ctx.clearRect(0, 0, W, H);
// Draw axes
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath();
const [ox, oy] = toCanvas(0, 0);
ctx.moveTo(ox, MARGIN - 10); ctx.lineTo(ox, oy);
ctx.lineTo(W - MARGIN + 10, oy);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#888'; ctx.font = '11px system-ui';
ctx.textAlign = 'center';
for (let i = 0; i <= 9; i += 3) {
const [px, py] = toCanvas(i, 0);
ctx.fillText(i, px, py + 14);
}
ctx.textAlign = 'right';
for (let j = 0; j <= 6; j += 2) {
const [px, py] = toCanvas(0, j);
ctx.fillText(j, px - 6, py + 4);
}
// Draw feasible region (filled polygon)
ctx.beginPath();
VERTICES.forEach((v, i) => {
const [px, py] = toCanvas(v.x, v.y);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
});
ctx.closePath();
ctx.fillStyle = 'rgba(29,53,87,0.09)';
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
// Highlight visited path edges
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.5;
const visited = [...visitedIds];
for (let i = 0; i < visited.length - 1; i++) {
const va = VERTICES[visited[i]], vb = VERTICES[visited[i + 1]];
const [ax, ay] = toCanvas(va.x, va.y);
const [bx, by] = toCanvas(vb.x, vb.y);
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(bx, by); ctx.stroke();
}
// Draw vertices
VERTICES.forEach(v => {
const [px, py] = toCanvas(v.x, v.y);
const val = OBJ([v.x, v.y]);
const isCurrent = v.id === currentId;
const isVisited = visitedIds.has(v.id);
const isOpt = v.id === optimalId;
ctx.beginPath();
ctx.arc(px, py, isCurrent ? 7 : 5, 0, 2 * Math.PI);
ctx.fillStyle = isCurrent ? '#e63946' : isVisited ? '#e6394688' : '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.stroke();
// Label
ctx.font = `${isCurrent ? 700 : 400} 10.5px system-ui`;
ctx.fillStyle = '#1d3557';
ctx.textAlign = 'left';
const lx = px + 9, ly = py - 6;
ctx.fillText(v.label + ' → ' + val, lx, ly);
});
// Optimal star if reached
if (currentId === optimalId) {
const [px, py] = toCanvas(VERTICES[optimalId].x, VERTICES[optimalId].y);
ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('★', px + 1, py - 14);
}
}
// State
let currentId = 0; // start at origin
let visited = new Set([0]);
let done = false;
const OPTIMAL_ID = 2; // vertex (6,3), obj=24
function updateUI() {
const v = VERTICES[currentId];
document.getElementById('vertex-label').textContent = v.label;
document.getElementById('obj-val').textContent = OBJ([v.x, v.y]);
drawScene(currentId, visited, OPTIMAL_ID);
}
function pivot() {
if (done) return;
const next = simplexPivot(currentId);
const msg = document.getElementById('msg');
if (next === null) {
done = true;
msg.textContent = '★ Optimal! No neighbor improves the objective.';
msg.className = 'msg';
document.getElementById('btn-pivot').disabled = true;
} else {
currentId = next;
visited.add(next);
const val = OBJ([VERTICES[next].x, VERTICES[next].y]);
msg.textContent = fmt('Pivoted to %label% — objective = %val%', { label: VERTICES[next].label, val: val });
msg.className = 'msg';
if (next === OPTIMAL_ID) {
done = true;
msg.textContent = fmt('★ Optimal reached at %label%! Objective = %val%', { label: VERTICES[next].label, val: val });
document.getElementById('btn-pivot').disabled = true;
}
}
updateUI();
}
function reset() {
currentId = 0;
visited = new Set([0]);
done = false;
document.getElementById('msg').textContent = 'Starting at (0, 0). Click "Next Pivot" to step.';
document.getElementById('msg').className = 'msg';
document.getElementById('btn-pivot').disabled = false;
updateUI();
}
document.getElementById('btn-pivot').addEventListener('click', pivot);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial render
document.getElementById('msg').textContent = 'Starting at (0, 0). Click "Next Pivot" to step.';
updateUI();
// Verify: check that greedy pivot from origin reaches the true optimum
let checkId = 0, steps = 0;
while (true) {
const nxt = simplexPivot(checkId);
if (nxt === null) break;
checkId = nxt; steps++;
if (steps > 100) throw new Error('pivot loop not terminating');
}
if (checkId !== OPTIMAL_ID) throw new Error('simplex did not reach optimum: got id=' + checkId);
console.log(fmt('Simplex demo verified: reached optimum %label% (obj=%OBJ%) in %steps% pivots.', { label: VERTICES[OPTIMAL_ID].label, OBJ: OBJ([VERTICES[OPTIMAL_ID].x, VERTICES[OPTIMAL_ID].y]), steps: steps }));
Notice: the path is short and direct. Even if you pick the worst starting corner, simplex finds the optimum in just a few pivots on this small example. On a 2D polytope with 4 constraints there are at most 4 corners — trivial. Scale up to millions of variables and millions of constraints, and the question of how many pivots are needed becomes profound.
Comments
Loading comments...