The demo below models a simple two-machine assignment problem. Two jobs must be assigned — one to each machine — but a coupling constraint says their start times must differ by at least one unit. That one constraint makes the problem hard.
<p class="hint">
Two jobs compete for two machines. A <b>coupling constraint</b> says their start times must differ by ≥ 1. We relax it by charging penalty <b>λ</b> per unit of violation. The relaxed cost is always ≤ the true optimum — a valid lower bound.
</p>
<div class="controls">
<label>λ = <span id="lam-val">0.00</span>
<input type="range" id="lam" min="0" max="6" step="0.05" value="0">
</label>
<button id="run-btn" type="button">Run subgradient</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="metrics">
<div class="metric"><span class="label">Dual bound L(λ)</span><span id="dual" class="val">—</span></div>
<div class="metric"><span class="label">Best bound L*</span><span id="best" class="val">—</span></div>
<div class="metric"><span class="label">True optimum</span><span id="true-opt" class="val">8</span></div>
<div class="metric"><span class="label">Duality gap</span><span id="gap" class="val">—</span></div>
</div>
<canvas id="chart" width="480" height="180"></canvas>
<div id="detail" class="detail"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { display: flex; align-items: center; gap: .4rem; font-size: .93rem; font-weight: 600; }
input[type=range] { width: 160px; cursor: pointer; }
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; }
.metrics { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
.metric { background: #eef2f6; border-radius: 8px; padding: .4rem .7rem; min-width: 110px; }
.metric .label { display: block; font-size: .75rem; color: #666; margin-bottom: .1rem; }
.metric .val { font: 700 1.05rem ui-monospace, monospace; color: #1d3557; }
#chart { display: block; width: 100%; max-width: 480px; border-radius: 8px;
background: #f7f9fb; border: 1px solid #dde3ea; }
.detail { font-size: .85rem; color: #444; margin-top: .5rem; min-height: 2.5em; line-height: 1.5; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Problem: assign Job A and Job B each to one of two machines.
// Job A on machine 1 costs 5, on machine 2 costs 9.
// Job B on machine 1 costs 7, on machine 2 costs 3.
// Coupling constraint: |startA - startB| >= 1 (startA in {0,1}, startB in {0,1})
// For simplicity we encode: xA in {0,1} (0=machine1, 1=machine2),
// xB in {0,1} (0=machine1, 1=machine2).
// Constraint: xA != xB (each job to a DIFFERENT machine: xA + xB = 1)
// encoded as coupling g(x) = (xA + xB - 1)^2 (= 0 iff exactly one is 1)
// True problem: min costA[xA] + costB[xB] s.t. xA + xB = 1
// Feasible solutions: (xA=0,xB=1)->5+3=8 or (xA=1,xB=0)->9+7=16
// True optimum = 8.
//
// Lagrangian relaxation: L(λ) = min over (xA,xB) in {0,1}^2 of
// costA[xA] + costB[xB] + λ*(xA + xB - 1)
// This decouples into two independent 1-variable minimizations.
const costA = [5, 9]; // costA[0]=machine1, costA[1]=machine2
const costB = [7, 3]; // costB[0]=machine1, costB[1]=machine2
const TRUE_OPT = 8; // xA=0,xB=1 -> 5+3=8
function lagrangian(lam) {
// Decoupled: choose xA to minimize costA[xA] + lam*xA
const fA0 = costA[0] + lam * 0;
const fA1 = costA[1] + lam * 1;
const xA = fA0 <= fA1 ? 0 : 1;
const valA = Math.min(fA0, fA1);
// Choose xB to minimize costB[xB] + lam*xB
const fB0 = costB[0] + lam * 0;
const fB1 = costB[1] + lam * 1;
const xB = fB0 <= fB1 ? 0 : 1;
const valB = Math.min(fB0, fB1);
const L = valA + valB - lam; // subtract lam (the -lambda constant term)
const violation = xA + xB - 1; // coupling constraint residual
return { L, xA, xB, violation };
}
// Pre-compute the dual curve for the chart
const STEPS = 120;
const LAM_MAX = 6;
const curve = [];
let bestL = -Infinity, bestLam = 0;
for (let i = 0; i <= STEPS; i++) {
const lam = (i / STEPS) * LAM_MAX;
const { L } = lagrangian(lam);
curve.push({ lam, L });
if (L > bestL) { bestL = L; bestLam = lam; }
}
// Canvas chart
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 18, right: 18, bottom: 28, left: 44 };
const minL = Math.min(...curve.map(p => p.L)) - 0.5;
const maxL = Math.max(...curve.map(p => p.L)) + 0.5;
function toX(lam) { return PAD.left + (lam / LAM_MAX) * (W - PAD.left - PAD.right); }
function toY(L) { return PAD.top + (1 - (L - minL) / (maxL - minL)) * (H - PAD.top - PAD.bottom); }
function drawChart(currentLam, history) {
ctx.clearRect(0, 0, W, H);
// Axes
ctx.strokeStyle = '#cdd9e3'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, H - PAD.bottom);
ctx.lineTo(W - PAD.right, H - PAD.bottom);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#888'; ctx.font = '11px system-ui'; ctx.textAlign = 'right';
const yTicks = 4;
for (let i = 0; i <= yTicks; i++) {
const val = minL + (i / yTicks) * (maxL - minL);
const y = toY(val);
ctx.fillText(val.toFixed(1), PAD.left - 4, y + 4);
}
ctx.textAlign = 'center';
for (let lam = 0; lam <= LAM_MAX; lam += 2) {
ctx.fillText(lam.toFixed(0), toX(lam), H - PAD.bottom + 14);
}
ctx.fillText('λ', W - PAD.right + 8, H - PAD.bottom + 4);
ctx.fillText('L(λ)', PAD.left - 32, PAD.top - 6);
// True optimum line
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.moveTo(PAD.left, toY(TRUE_OPT)); ctx.lineTo(W - PAD.right, toY(TRUE_OPT));
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#e63946'; ctx.font = '10px system-ui'; ctx.textAlign = 'left';
ctx.fillText(fmt('true opt=%TRUE_OPT%', { TRUE_OPT: TRUE_OPT }), PAD.left + 4, toY(TRUE_OPT) - 3);
// Dual curve
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2;
ctx.beginPath();
curve.forEach(({ lam, L }, i) => {
if (i === 0) ctx.moveTo(toX(lam), toY(L));
else ctx.lineTo(toX(lam), toY(L));
});
ctx.stroke();
// Subgradient history path
if (history && history.length > 1) {
ctx.strokeStyle = '#457b9d'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 2]);
ctx.beginPath();
history.forEach(({ lam, L }, i) => {
if (i === 0) ctx.moveTo(toX(lam), toY(L));
else ctx.lineTo(toX(lam), toY(L));
});
ctx.stroke();
ctx.setLineDash([]);
}
// Current lambda dot
const { L } = lagrangian(currentLam);
ctx.fillStyle = '#e63946';
ctx.beginPath();
ctx.arc(toX(currentLam), toY(L), 5, 0, 2 * Math.PI);
ctx.fill();
}
let history = [];
let running = false;
function update() {
const lam = parseFloat(document.getElementById('lam').value);
document.getElementById('lam-val').textContent = lam.toFixed(2);
const { L, xA, xB, violation } = lagrangian(lam);
document.getElementById('dual').textContent = L.toFixed(2);
document.getElementById('best').textContent = bestL.toFixed(2);
document.getElementById('gap').textContent = (TRUE_OPT - Math.max(L, 0)).toFixed(2);
const machA = xA === 0 ? 'Machine 1 (cost 5)' : 'Machine 2 (cost 9)';
const machB = xB === 0 ? 'Machine 1 (cost 7)' : 'Machine 2 (cost 3)';
const viol = violation === 0 ? 'coupling satisfied (different machines)' : fmt('coupling VIOLATED (same machine, violation=%violation%)', { violation: violation });
document.getElementById('detail').innerHTML =
fmt('At λ=%lam%: Job A → %machA%, Job B → %machB%<br>%viol%. Relaxed cost = %L%.', { lam: lam.toFixed(2), machA: machA, machB: machB, viol: viol, L: L.toFixed(2) });
drawChart(lam, history);
}
async function runSubgradient() {
if (running) return;
running = true;
history = [];
let lam = 0;
const T = 40;
for (let t = 0; t < T; t++) {
const { L, violation } = lagrangian(lam);
history.push({ lam, L });
const stepSize = 2.0 / (t + 1);
lam = Math.max(0, Math.min(LAM_MAX, lam + stepSize * violation));
document.getElementById('lam').value = lam;
update();
await new Promise(r => setTimeout(r, 60));
}
running = false;
}
document.getElementById('lam').addEventListener('input', () => { history = []; update(); });
document.getElementById('run-btn').addEventListener('click', runSubgradient);
document.getElementById('reset-btn').addEventListener('click', () => {
history = []; running = false;
document.getElementById('lam').value = 0;
update();
});
update();
<p class="hint">
Two jobs compete for two machines. A <b>coupling constraint</b> says their start times must differ by ≥ 1. We relax it by charging penalty <b>λ</b> per unit of violation. The relaxed cost is always ≤ the true optimum — a valid lower bound.
</p>
<div class="controls">
<label>λ = <span id="lam-val">0.00</span>
<input type="range" id="lam" min="0" max="6" step="0.05" value="0">
</label>
<button id="run-btn" type="button">Run subgradient</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="metrics">
<div class="metric"><span class="label">Dual bound L(λ)</span><span id="dual" class="val">—</span></div>
<div class="metric"><span class="label">Best bound L*</span><span id="best" class="val">—</span></div>
<div class="metric"><span class="label">True optimum</span><span id="true-opt" class="val">8</span></div>
<div class="metric"><span class="label">Duality gap</span><span id="gap" class="val">—</span></div>
</div>
<canvas id="chart" width="480" height="180"></canvas>
<div id="detail" class="detail"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; align-items: center; gap: .7rem; flex-wrap: wrap; margin-bottom: .8rem; }
label { display: flex; align-items: center; gap: .4rem; font-size: .93rem; font-weight: 600; }
input[type=range] { width: 160px; cursor: pointer; }
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; }
.metrics { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
.metric { background: #eef2f6; border-radius: 8px; padding: .4rem .7rem; min-width: 110px; }
.metric .label { display: block; font-size: .75rem; color: #666; margin-bottom: .1rem; }
.metric .val { font: 700 1.05rem ui-monospace, monospace; color: #1d3557; }
#chart { display: block; width: 100%; max-width: 480px; border-radius: 8px;
background: #f7f9fb; border: 1px solid #dde3ea; }
.detail { font-size: .85rem; color: #444; margin-top: .5rem; min-height: 2.5em; line-height: 1.5; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Problem: assign Job A and Job B each to one of two machines.
// Job A on machine 1 costs 5, on machine 2 costs 9.
// Job B on machine 1 costs 7, on machine 2 costs 3.
// Coupling constraint: |startA - startB| >= 1 (startA in {0,1}, startB in {0,1})
// For simplicity we encode: xA in {0,1} (0=machine1, 1=machine2),
// xB in {0,1} (0=machine1, 1=machine2).
// Constraint: xA != xB (each job to a DIFFERENT machine: xA + xB = 1)
// encoded as coupling g(x) = (xA + xB - 1)^2 (= 0 iff exactly one is 1)
// True problem: min costA[xA] + costB[xB] s.t. xA + xB = 1
// Feasible solutions: (xA=0,xB=1)->5+3=8 or (xA=1,xB=0)->9+7=16
// True optimum = 8.
//
// Lagrangian relaxation: L(λ) = min over (xA,xB) in {0,1}^2 of
// costA[xA] + costB[xB] + λ*(xA + xB - 1)
// This decouples into two independent 1-variable minimizations.
const costA = [5, 9]; // costA[0]=machine1, costA[1]=machine2
const costB = [7, 3]; // costB[0]=machine1, costB[1]=machine2
const TRUE_OPT = 8; // xA=0,xB=1 -> 5+3=8
function lagrangian(lam) {
// Decoupled: choose xA to minimize costA[xA] + lam*xA
const fA0 = costA[0] + lam * 0;
const fA1 = costA[1] + lam * 1;
const xA = fA0 <= fA1 ? 0 : 1;
const valA = Math.min(fA0, fA1);
// Choose xB to minimize costB[xB] + lam*xB
const fB0 = costB[0] + lam * 0;
const fB1 = costB[1] + lam * 1;
const xB = fB0 <= fB1 ? 0 : 1;
const valB = Math.min(fB0, fB1);
const L = valA + valB - lam; // subtract lam (the -lambda constant term)
const violation = xA + xB - 1; // coupling constraint residual
return { L, xA, xB, violation };
}
// Pre-compute the dual curve for the chart
const STEPS = 120;
const LAM_MAX = 6;
const curve = [];
let bestL = -Infinity, bestLam = 0;
for (let i = 0; i <= STEPS; i++) {
const lam = (i / STEPS) * LAM_MAX;
const { L } = lagrangian(lam);
curve.push({ lam, L });
if (L > bestL) { bestL = L; bestLam = lam; }
}
// Canvas chart
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 18, right: 18, bottom: 28, left: 44 };
const minL = Math.min(...curve.map(p => p.L)) - 0.5;
const maxL = Math.max(...curve.map(p => p.L)) + 0.5;
function toX(lam) { return PAD.left + (lam / LAM_MAX) * (W - PAD.left - PAD.right); }
function toY(L) { return PAD.top + (1 - (L - minL) / (maxL - minL)) * (H - PAD.top - PAD.bottom); }
function drawChart(currentLam, history) {
ctx.clearRect(0, 0, W, H);
// Axes
ctx.strokeStyle = '#cdd9e3'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, H - PAD.bottom);
ctx.lineTo(W - PAD.right, H - PAD.bottom);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#888'; ctx.font = '11px system-ui'; ctx.textAlign = 'right';
const yTicks = 4;
for (let i = 0; i <= yTicks; i++) {
const val = minL + (i / yTicks) * (maxL - minL);
const y = toY(val);
ctx.fillText(val.toFixed(1), PAD.left - 4, y + 4);
}
ctx.textAlign = 'center';
for (let lam = 0; lam <= LAM_MAX; lam += 2) {
ctx.fillText(lam.toFixed(0), toX(lam), H - PAD.bottom + 14);
}
ctx.fillText('λ', W - PAD.right + 8, H - PAD.bottom + 4);
ctx.fillText('L(λ)', PAD.left - 32, PAD.top - 6);
// True optimum line
ctx.strokeStyle = '#e63946'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.moveTo(PAD.left, toY(TRUE_OPT)); ctx.lineTo(W - PAD.right, toY(TRUE_OPT));
ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#e63946'; ctx.font = '10px system-ui'; ctx.textAlign = 'left';
ctx.fillText(fmt('true opt=%TRUE_OPT%', { TRUE_OPT: TRUE_OPT }), PAD.left + 4, toY(TRUE_OPT) - 3);
// Dual curve
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2;
ctx.beginPath();
curve.forEach(({ lam, L }, i) => {
if (i === 0) ctx.moveTo(toX(lam), toY(L));
else ctx.lineTo(toX(lam), toY(L));
});
ctx.stroke();
// Subgradient history path
if (history && history.length > 1) {
ctx.strokeStyle = '#457b9d'; ctx.lineWidth = 1.5; ctx.setLineDash([3, 2]);
ctx.beginPath();
history.forEach(({ lam, L }, i) => {
if (i === 0) ctx.moveTo(toX(lam), toY(L));
else ctx.lineTo(toX(lam), toY(L));
});
ctx.stroke();
ctx.setLineDash([]);
}
// Current lambda dot
const { L } = lagrangian(currentLam);
ctx.fillStyle = '#e63946';
ctx.beginPath();
ctx.arc(toX(currentLam), toY(L), 5, 0, 2 * Math.PI);
ctx.fill();
}
let history = [];
let running = false;
function update() {
const lam = parseFloat(document.getElementById('lam').value);
document.getElementById('lam-val').textContent = lam.toFixed(2);
const { L, xA, xB, violation } = lagrangian(lam);
document.getElementById('dual').textContent = L.toFixed(2);
document.getElementById('best').textContent = bestL.toFixed(2);
document.getElementById('gap').textContent = (TRUE_OPT - Math.max(L, 0)).toFixed(2);
const machA = xA === 0 ? 'Machine 1 (cost 5)' : 'Machine 2 (cost 9)';
const machB = xB === 0 ? 'Machine 1 (cost 7)' : 'Machine 2 (cost 3)';
const viol = violation === 0 ? 'coupling satisfied (different machines)' : fmt('coupling VIOLATED (same machine, violation=%violation%)', { violation: violation });
document.getElementById('detail').innerHTML =
fmt('At λ=%lam%: Job A → %machA%, Job B → %machB%<br>%viol%. Relaxed cost = %L%.', { lam: lam.toFixed(2), machA: machA, machB: machB, viol: viol, L: L.toFixed(2) });
drawChart(lam, history);
}
async function runSubgradient() {
if (running) return;
running = true;
history = [];
let lam = 0;
const T = 40;
for (let t = 0; t < T; t++) {
const { L, violation } = lagrangian(lam);
history.push({ lam, L });
const stepSize = 2.0 / (t + 1);
lam = Math.max(0, Math.min(LAM_MAX, lam + stepSize * violation));
document.getElementById('lam').value = lam;
update();
await new Promise(r => setTimeout(r, 60));
}
running = false;
}
document.getElementById('lam').addEventListener('input', () => { history = []; update(); });
document.getElementById('run-btn').addEventListener('click', runSubgradient);
document.getElementById('reset-btn').addEventListener('click', () => {
history = []; running = false;
document.getElementById('lam').value = 0;
update();
});
update();
Comments
Loading comments...