<!-- Two-product robust optimization demo: nominal vs robust production plan -->
<div class="panel">
<div class="info-bar">
<span class="tag nominal-tag">Nominal Plan</span>
<span class="tag robust-tag">Robust Plan</span>
</div>
<p class="hint">Adjust the uncertainty radius δ, then click <b>Adversarial attack</b>. The adversary picks the demand inside the box [nominal−δ, nominal+δ] that hurts the nominal plan most. Watch the robust plan hold its guaranteed profit.</p>
<!-- Uncertainty radius slider -->
<div class="controls">
<label>
Uncertainty radius δ = <strong id="deltaVal">10</strong>
<input type="range" id="deltaSlider" min="0" max="30" value="10" step="1">
</label>
</div>
<!-- Side-by-side plan comparison -->
<div class="plans">
<div class="plan-box nominal-box">
<div class="plan-title">Nominal Plan</div>
<div class="plan-row"><span>Produce A</span><span id="nomA">—</span></div>
<div class="plan-row"><span>Produce B</span><span id="nomB">—</span></div>
<div class="plan-row profit-row"><span>Expected profit</span><span id="nomProfit">—</span></div>
</div>
<div class="plan-box robust-box">
<div class="plan-title">Robust Plan</div>
<div class="plan-row"><span>Produce A</span><span id="robA">—</span></div>
<div class="plan-row"><span>Produce B</span><span id="robB">—</span></div>
<div class="plan-row profit-row"><span>Guaranteed profit</span><span id="robProfit">—</span></div>
</div>
</div>
<!-- Adversarial demand attack button -->
<div class="attack-section">
<button id="attackBtn" type="button">Adversarial attack</button>
<button id="resetBtn" type="button" class="ghost">Hide result</button>
</div>
<div id="resultBox" class="result-box hidden">
<div class="result-row">
<span>Adversarial demand</span>
<span id="advDemand">—</span>
</div>
<div class="result-row">
<span class="nominal-label">Nominal profit under attack</span>
<span id="nomActual" class="nominal-value">—</span>
</div>
<div class="result-row">
<span class="robust-label">Robust profit under attack</span>
<span id="robActual" class="robust-value">—</span>
</div>
<div id="verdict" class="verdict"></div>
</div>
</div>
/* Base reset and panel layout */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.panel { padding: .8rem; max-width: 520px; }
.info-bar { display: flex; gap: .5rem; margin-bottom: .6rem; }
.tag { font-size: .78rem; font-weight: 700; padding: .2rem .55rem; border-radius: 12px; }
.nominal-tag { background: #dde9f7; color: #1a4a8a; }
.robust-tag { background: #d6f0e0; color: #145c30; }
.hint { font-size: .88rem; color: #444; margin-bottom: .8rem; line-height: 1.5; }
/* Slider control styling */
.controls { margin-bottom: .9rem; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1a4a8a; }
/* Side-by-side plan card styling */
.plans { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .8rem; }
.plan-box { border-radius: 10px; padding: .65rem .75rem; }
.nominal-box { background: #eef4fc; border: 1.5px solid #adc6e8; }
.robust-box { background: #eaf6ee; border: 1.5px solid #82c89a; }
.plan-title { font-weight: 700; font-size: .85rem; margin-bottom: .4rem; }
.plan-row { display: flex; justify-content: space-between; font-size: .88rem; padding: .15rem 0; border-bottom: 1px solid rgba(0,0,0,.06); }
.plan-row:last-child { border-bottom: none; }
.profit-row { font-weight: 600; margin-top: .2rem; }
/* Attack button and result box styling */
.attack-section { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 13px system-ui; padding: .42rem .85rem; border: 1.5px solid #1a4a8a;
background: #1a4a8a; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1a4a8a; }
/* Result box and verdict styling */
.result-box { background: #f7f7f9; border: 1.5px solid #ccc; border-radius: 10px;
padding: .7rem .85rem; font-size: .9rem; }
.result-box.hidden { display: none; }
.result-row { display: flex; justify-content: space-between; padding: .18rem 0;
border-bottom: 1px solid rgba(0,0,0,.06); }
.result-row:last-child { border-bottom: none; }
.nominal-label { color: #1a4a8a; font-weight: 600; }
.robust-label { color: #145c30; font-weight: 600; }
.nominal-value { color: #1a4a8a; font-weight: 700; }
.robust-value { color: #145c30; font-weight: 700; }
.verdict { margin-top: .5rem; font-weight: 700; font-size: .95rem; text-align: center; padding: .3rem; border-radius: 6px; }
.verdict.robust-wins { background: #d6f0e0; color: #145c30; }
.verdict.nominal-wins { background: #dde9f7; color: #1a4a8a; }
// Robust optimization demo: compare nominal vs robust production planning.
// Model: factory produces qA units of A and qB units of B before demand (dA, dB) is revealed.
// Profit = price * min(q, d) - holding_cost * max(0, q - d). Total capacity: qA + qB <= CAP.
const PROFIT_A = 5; // profit per unit of A sold
const PROFIT_B = 4; // profit per unit of B sold
const HOLD_A = 1; // holding cost per unsold unit of A
const HOLD_B = 1; // holding cost per unsold unit of B
const CAP = 100; // total production capacity
const NOM_DA = 60; // nominal (expected) demand for A
const NOM_DB = 50; // nominal (expected) demand for B
// Compute realized profit given production quantities and actual demand.
function profit(qA, qB, dA, dB) {
const soldA = Math.min(qA, dA);
const soldB = Math.min(qB, dB);
const holdA = Math.max(0, qA - dA);
const holdB = Math.max(0, qB - dB);
return PROFIT_A * soldA + PROFIT_B * soldB - HOLD_A * holdA - HOLD_B * holdB;
}
// Nominal plan: maximize profit assuming demand equals its nominal value.
// 2D grid search over all feasible (qA, qB) pairs with qA+qB<=CAP, step 5.
function solveNominal(dA, dB) {
let best = -Infinity, bestQA = 0, bestQB = 0;
for (let qa = 0; qa <= CAP; qa += 5) {
for (let qb = 0; qa + qb <= CAP; qb += 5) {
const p = profit(qa, qb, dA, dB);
if (p > best) { best = p; bestQA = qa; bestQB = qb; }
}
}
return { qA: bestQA, qB: bestQB, p: best };
}
// Robust plan: maximize the worst-case profit over the uncertainty box.
// For each (qA,qB) the worst-case demand is at a corner of the box; check all 4 corners.
function solveRobust(nomDA, nomDB, delta) {
let best = -Infinity, bestQA = 0, bestQB = 0;
const dAlo = Math.max(0, nomDA - delta), dAhi = nomDA + delta;
const dBlo = Math.max(0, nomDB - delta), dBhi = nomDB + delta;
for (let qa = 0; qa <= CAP; qa += 5) {
for (let qb = 0; qa + qb <= CAP; qb += 5) {
// worst-case over box corners: the adversary minimizes profit for this production plan
let worstP = Infinity;
for (const da of [dAlo, dAhi]) {
for (const db of [dBlo, dBhi]) {
const p = profit(qa, qb, da, db);
if (p < worstP) worstP = p;
}
}
if (worstP > best) { best = worstP; bestQA = qa; bestQB = qb; }
}
}
return { qA: bestQA, qB: bestQB, p: best };
}
// Adversary: finds the demand in the box that minimizes the nominal plan's profit.
// Enumerates all integer demand pairs in [nomD-delta, nomD+delta] x [nomD-delta, nomD+delta].
function adversarialDemand(nomPlan, nomDA, nomDB, delta) {
let worst = Infinity, wdA = nomDA, wdB = nomDB;
for (let da = Math.max(0, nomDA - delta); da <= nomDA + delta; da += 1) {
for (let db = Math.max(0, nomDB - delta); db <= nomDB + delta; db += 1) {
const p = profit(nomPlan.qA, nomPlan.qB, da, db);
if (p < worst) { worst = p; wdA = da; wdB = db; }
}
}
return { dA: wdA, dB: wdB };
}
let nomPlan = null, robPlan = null, delta = 10;
function updatePlans() {
delta = parseInt(document.getElementById('deltaSlider').value);
document.getElementById('deltaVal').textContent = delta;
nomPlan = solveNominal(NOM_DA, NOM_DB);
robPlan = solveRobust(NOM_DA, NOM_DB, delta);
document.getElementById('nomA').textContent = nomPlan.qA;
document.getElementById('nomB').textContent = nomPlan.qB;
document.getElementById('nomProfit').textContent = nomPlan.p.toFixed(0);
document.getElementById('robA').textContent = robPlan.qA;
document.getElementById('robB').textContent = robPlan.qB;
document.getElementById('robProfit').textContent = robPlan.p.toFixed(0);
document.getElementById('resultBox').classList.add('hidden');
}
document.getElementById('attackBtn').addEventListener('click', () => {
if (!nomPlan || !robPlan) return;
const adv = adversarialDemand(nomPlan, NOM_DA, NOM_DB, delta);
const pNom = profit(nomPlan.qA, nomPlan.qB, adv.dA, adv.dB);
const pRob = profit(robPlan.qA, robPlan.qB, adv.dA, adv.dB);
document.getElementById('advDemand').textContent = `A=${adv.dA}, B=${adv.dB}`;
document.getElementById('nomActual').textContent = pNom.toFixed(0);
document.getElementById('robActual').textContent = pRob.toFixed(0);
const verdict = document.getElementById('verdict');
if (pRob >= pNom) {
verdict.textContent = 'Robust plan wins — the guarantee held!';
verdict.className = 'verdict robust-wins';
} else {
verdict.textContent = 'Nominal plan still ahead (δ too small to matter here).';
verdict.className = 'verdict nominal-wins';
}
document.getElementById('resultBox').classList.remove('hidden');
console.log('Adversarial attack: demand =', `A=${adv.dA}, B=${adv.dB}`, '| nom='+pNom.toFixed(0), 'rob='+pRob.toFixed(0));
});
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('resultBox').classList.add('hidden');
});
document.getElementById('deltaSlider').addEventListener('input', updatePlans);
updatePlans();
<!-- Two-product robust optimization demo: nominal vs robust production plan -->
<div class="panel">
<div class="info-bar">
<span class="tag nominal-tag">Nominal Plan</span>
<span class="tag robust-tag">Robust Plan</span>
</div>
<p class="hint">Adjust the uncertainty radius δ, then click <b>Adversarial attack</b>. The adversary picks the demand inside the box [nominal−δ, nominal+δ] that hurts the nominal plan most. Watch the robust plan hold its guaranteed profit.</p>
<!-- Uncertainty radius slider -->
<div class="controls">
<label>
Uncertainty radius δ = <strong id="deltaVal">10</strong>
<input type="range" id="deltaSlider" min="0" max="30" value="10" step="1">
</label>
</div>
<!-- Side-by-side plan comparison -->
<div class="plans">
<div class="plan-box nominal-box">
<div class="plan-title">Nominal Plan</div>
<div class="plan-row"><span>Produce A</span><span id="nomA">—</span></div>
<div class="plan-row"><span>Produce B</span><span id="nomB">—</span></div>
<div class="plan-row profit-row"><span>Expected profit</span><span id="nomProfit">—</span></div>
</div>
<div class="plan-box robust-box">
<div class="plan-title">Robust Plan</div>
<div class="plan-row"><span>Produce A</span><span id="robA">—</span></div>
<div class="plan-row"><span>Produce B</span><span id="robB">—</span></div>
<div class="plan-row profit-row"><span>Guaranteed profit</span><span id="robProfit">—</span></div>
</div>
</div>
<!-- Adversarial demand attack button -->
<div class="attack-section">
<button id="attackBtn" type="button">Adversarial attack</button>
<button id="resetBtn" type="button" class="ghost">Hide result</button>
</div>
<div id="resultBox" class="result-box hidden">
<div class="result-row">
<span>Adversarial demand</span>
<span id="advDemand">—</span>
</div>
<div class="result-row">
<span class="nominal-label">Nominal profit under attack</span>
<span id="nomActual" class="nominal-value">—</span>
</div>
<div class="result-row">
<span class="robust-label">Robust profit under attack</span>
<span id="robActual" class="robust-value">—</span>
</div>
<div id="verdict" class="verdict"></div>
</div>
</div>
/* Base reset and panel layout */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.panel { padding: .8rem; max-width: 520px; }
.info-bar { display: flex; gap: .5rem; margin-bottom: .6rem; }
.tag { font-size: .78rem; font-weight: 700; padding: .2rem .55rem; border-radius: 12px; }
.nominal-tag { background: #dde9f7; color: #1a4a8a; }
.robust-tag { background: #d6f0e0; color: #145c30; }
.hint { font-size: .88rem; color: #444; margin-bottom: .8rem; line-height: 1.5; }
/* Slider control styling */
.controls { margin-bottom: .9rem; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
input[type=range] { flex: 1; min-width: 120px; accent-color: #1a4a8a; }
/* Side-by-side plan card styling */
.plans { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .8rem; }
.plan-box { border-radius: 10px; padding: .65rem .75rem; }
.nominal-box { background: #eef4fc; border: 1.5px solid #adc6e8; }
.robust-box { background: #eaf6ee; border: 1.5px solid #82c89a; }
.plan-title { font-weight: 700; font-size: .85rem; margin-bottom: .4rem; }
.plan-row { display: flex; justify-content: space-between; font-size: .88rem; padding: .15rem 0; border-bottom: 1px solid rgba(0,0,0,.06); }
.plan-row:last-child { border-bottom: none; }
.profit-row { font-weight: 600; margin-top: .2rem; }
/* Attack button and result box styling */
.attack-section { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
button { font: 600 13px system-ui; padding: .42rem .85rem; border: 1.5px solid #1a4a8a;
background: #1a4a8a; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1a4a8a; }
/* Result box and verdict styling */
.result-box { background: #f7f7f9; border: 1.5px solid #ccc; border-radius: 10px;
padding: .7rem .85rem; font-size: .9rem; }
.result-box.hidden { display: none; }
.result-row { display: flex; justify-content: space-between; padding: .18rem 0;
border-bottom: 1px solid rgba(0,0,0,.06); }
.result-row:last-child { border-bottom: none; }
.nominal-label { color: #1a4a8a; font-weight: 600; }
.robust-label { color: #145c30; font-weight: 600; }
.nominal-value { color: #1a4a8a; font-weight: 700; }
.robust-value { color: #145c30; font-weight: 700; }
.verdict { margin-top: .5rem; font-weight: 700; font-size: .95rem; text-align: center; padding: .3rem; border-radius: 6px; }
.verdict.robust-wins { background: #d6f0e0; color: #145c30; }
.verdict.nominal-wins { background: #dde9f7; color: #1a4a8a; }
// Robust optimization demo: compare nominal vs robust production planning.
// Model: factory produces qA units of A and qB units of B before demand (dA, dB) is revealed.
// Profit = price * min(q, d) - holding_cost * max(0, q - d). Total capacity: qA + qB <= CAP.
const PROFIT_A = 5; // profit per unit of A sold
const PROFIT_B = 4; // profit per unit of B sold
const HOLD_A = 1; // holding cost per unsold unit of A
const HOLD_B = 1; // holding cost per unsold unit of B
const CAP = 100; // total production capacity
const NOM_DA = 60; // nominal (expected) demand for A
const NOM_DB = 50; // nominal (expected) demand for B
// Compute realized profit given production quantities and actual demand.
function profit(qA, qB, dA, dB) {
const soldA = Math.min(qA, dA);
const soldB = Math.min(qB, dB);
const holdA = Math.max(0, qA - dA);
const holdB = Math.max(0, qB - dB);
return PROFIT_A * soldA + PROFIT_B * soldB - HOLD_A * holdA - HOLD_B * holdB;
}
// Nominal plan: maximize profit assuming demand equals its nominal value.
// 2D grid search over all feasible (qA, qB) pairs with qA+qB<=CAP, step 5.
function solveNominal(dA, dB) {
let best = -Infinity, bestQA = 0, bestQB = 0;
for (let qa = 0; qa <= CAP; qa += 5) {
for (let qb = 0; qa + qb <= CAP; qb += 5) {
const p = profit(qa, qb, dA, dB);
if (p > best) { best = p; bestQA = qa; bestQB = qb; }
}
}
return { qA: bestQA, qB: bestQB, p: best };
}
// Robust plan: maximize the worst-case profit over the uncertainty box.
// For each (qA,qB) the worst-case demand is at a corner of the box; check all 4 corners.
function solveRobust(nomDA, nomDB, delta) {
let best = -Infinity, bestQA = 0, bestQB = 0;
const dAlo = Math.max(0, nomDA - delta), dAhi = nomDA + delta;
const dBlo = Math.max(0, nomDB - delta), dBhi = nomDB + delta;
for (let qa = 0; qa <= CAP; qa += 5) {
for (let qb = 0; qa + qb <= CAP; qb += 5) {
// worst-case over box corners: the adversary minimizes profit for this production plan
let worstP = Infinity;
for (const da of [dAlo, dAhi]) {
for (const db of [dBlo, dBhi]) {
const p = profit(qa, qb, da, db);
if (p < worstP) worstP = p;
}
}
if (worstP > best) { best = worstP; bestQA = qa; bestQB = qb; }
}
}
return { qA: bestQA, qB: bestQB, p: best };
}
// Adversary: finds the demand in the box that minimizes the nominal plan's profit.
// Enumerates all integer demand pairs in [nomD-delta, nomD+delta] x [nomD-delta, nomD+delta].
function adversarialDemand(nomPlan, nomDA, nomDB, delta) {
let worst = Infinity, wdA = nomDA, wdB = nomDB;
for (let da = Math.max(0, nomDA - delta); da <= nomDA + delta; da += 1) {
for (let db = Math.max(0, nomDB - delta); db <= nomDB + delta; db += 1) {
const p = profit(nomPlan.qA, nomPlan.qB, da, db);
if (p < worst) { worst = p; wdA = da; wdB = db; }
}
}
return { dA: wdA, dB: wdB };
}
let nomPlan = null, robPlan = null, delta = 10;
function updatePlans() {
delta = parseInt(document.getElementById('deltaSlider').value);
document.getElementById('deltaVal').textContent = delta;
nomPlan = solveNominal(NOM_DA, NOM_DB);
robPlan = solveRobust(NOM_DA, NOM_DB, delta);
document.getElementById('nomA').textContent = nomPlan.qA;
document.getElementById('nomB').textContent = nomPlan.qB;
document.getElementById('nomProfit').textContent = nomPlan.p.toFixed(0);
document.getElementById('robA').textContent = robPlan.qA;
document.getElementById('robB').textContent = robPlan.qB;
document.getElementById('robProfit').textContent = robPlan.p.toFixed(0);
document.getElementById('resultBox').classList.add('hidden');
}
document.getElementById('attackBtn').addEventListener('click', () => {
if (!nomPlan || !robPlan) return;
const adv = adversarialDemand(nomPlan, NOM_DA, NOM_DB, delta);
const pNom = profit(nomPlan.qA, nomPlan.qB, adv.dA, adv.dB);
const pRob = profit(robPlan.qA, robPlan.qB, adv.dA, adv.dB);
document.getElementById('advDemand').textContent = `A=${adv.dA}, B=${adv.dB}`;
document.getElementById('nomActual').textContent = pNom.toFixed(0);
document.getElementById('robActual').textContent = pRob.toFixed(0);
const verdict = document.getElementById('verdict');
if (pRob >= pNom) {
verdict.textContent = 'Robust plan wins — the guarantee held!';
verdict.className = 'verdict robust-wins';
} else {
verdict.textContent = 'Nominal plan still ahead (δ too small to matter here).';
verdict.className = 'verdict nominal-wins';
}
document.getElementById('resultBox').classList.remove('hidden');
console.log('Adversarial attack: demand =', `A=${adv.dA}, B=${adv.dB}`, '| nom='+pNom.toFixed(0), 'rob='+pRob.toFixed(0));
});
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('resultBox').classList.add('hidden');
});
document.getElementById('deltaSlider').addEventListener('input', updatePlans);
updatePlans();
The robust plan sacrifices a sliver of expected profit to buy insurance. When the adversary strikes, that insurance pays off — the robust solution keeps a guaranteed minimum profit the nominal plan cannot match.
Comments
Loading comments...