Imagine scheduling production when daily demand d is uncertain — modeled as a normal random variable. You must choose a production quantity x≥0 to maximize revenue x, subject to the chance constraint: the probability that demand covers production must be at least 1−ε.
Pr(d≥x)≥1−ε
With d∼N(μ,σ2), this becomes x≤μ−z1−εσ where z1−ε=Φ−1(1−ε)>0 is the standard normal quantile at the confidence level. The higher the confidence requirement, the more you must reduce x below μ.
<!-- Chance-constrained optimization demo — confidence slider controls the safety buffer -->
<div class="demo-wrap">
<div class="controls">
<label for="conf-slider">
Confidence level (1 - ε): <strong id="conf-val">90%</strong>
</label>
<input id="conf-slider" type="range" min="50" max="99" value="90" step="1"
aria-label="Confidence level slider">
</div>
<canvas id="chart" width="460" height="200" aria-label="Normal distribution with optimal production level and safety buffer"></canvas>
<div class="metrics">
<div class="metric">
<span class="metric-label">Optimal x*</span>
<span class="metric-val" id="val-x">—</span>
</div>
<div class="metric">
<span class="metric-label">Safety buffer</span>
<span class="metric-val" id="val-buf">—</span>
</div>
<div class="metric">
<span class="metric-label">Loss vs. mean</span>
<span class="metric-val" id="val-loss">—</span>
</div>
</div>
<p class="note" id="note-text">Drag the slider to change the required confidence level and observe how the safety buffer grows.</p>
</div>
/* layout and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.demo-wrap { max-width: 480px; margin: 0 auto; }
.controls { display: flex; flex-direction: column; gap: .3rem; margin-bottom: .6rem; }
label { font-size: .92rem; font-weight: 600; color: #1d3557; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
canvas { display: block; width: 100%; border-radius: 8px; background: #f4f6f8; border: 1px solid #dde3ea; }
/* metric cards */
.metrics { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
.metric { flex: 1; min-width: 120px; background: #eef2f6; border-radius: 8px;
padding: .45rem .65rem; border: 1px solid #cdd9e3; }
.metric-label { display: block; font-size: .76rem; color: #445; margin-bottom: .15rem; }
.metric-val { display: block; font-size: 1.1rem; font-weight: 700; color: #1d3557; }
.note { font-size: .85rem; color: #556; margin-top: .5rem; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Demand distribution: mean MU, std dev SIGMA
const MU = 100;
const SIGMA = 15;
// Normal distribution inverse CDF (quantile function)
// Rational approximation (Beasley-Springer-Moro algorithm)
function normInvCDF(p) {
if (p <= 0) return -Infinity;
if (p >= 1) return Infinity;
const a = [0, -3.969683028665376e1, 2.209460984245205e2,
-2.759285104469687e2, 1.383577518672690e2,
-3.066479806614716e1, 2.506628277459239];
const b = [0, -5.447609879822406e1, 1.615858368580409e2,
-1.556989798598866e2, 6.680131188771972e1, -1.328068155288572e1];
const c = [0, -7.784894002430293e-3, -3.223964580411365e-1,
-2.400758277161838, -2.549732539343734,
4.374664141464968, 2.938163982698783];
const d = [0, 7.784695709041462e-3, 3.224671290700398e-1,
2.445134137142996, 3.754408661907416];
const pLow = 0.02425;
let q;
if (p < pLow) {
const t = Math.sqrt(-2 * Math.log(p));
q = (((((c[1]*t+c[2])*t+c[3])*t+c[4])*t+c[5])*t+c[6]) /
((((d[1]*t+d[2])*t+d[3])*t+d[4])*t+1);
} else if (p <= 1 - pLow) {
const u = p - 0.5;
const r = u * u;
q = u * (((((a[1]*r+a[2])*r+a[3])*r+a[4])*r+a[5])*r+a[6]) /
(((((b[1]*r+b[2])*r+b[3])*r+b[4])*r+b[5])*r+1);
} else {
const t = Math.sqrt(-2 * Math.log(1 - p));
q = -(((((c[1]*t+c[2])*t+c[3])*t+c[4])*t+c[5])*t+c[6]) /
((((d[1]*t+d[2])*t+d[3])*t+d[4])*t+1);
}
return q;
}
// Normal probability density function centered at MU
function normPDF(x) {
return Math.exp(-0.5 * ((x - MU) / SIGMA) ** 2) / (SIGMA * Math.sqrt(2 * Math.PI));
}
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Main draw function — redraws chart for a given confidence level
function draw(conf) {
ctx.clearRect(0, 0, W, H);
const eps = 1 - conf / 100;
// compute optimal x via chance constraint formula
const z = normInvCDF(conf / 100);
const xOpt = MU - z * SIGMA;
const buffer = MU - xOpt;
const loss = ((MU - xOpt) / MU * 100);
// map probability domain to canvas coordinates
const xMin = MU - 4 * SIGMA;
const xMax = MU + 4 * SIGMA;
const toCanvasX = (v) => (v - xMin) / (xMax - xMin) * (W - 40) + 20;
const maxPDF = normPDF(MU);
const toCanvasY = (p) => H - 30 - (p / (maxPDF * 1.1)) * (H - 50);
// shade the rejected tail (probability mass below x*)
ctx.beginPath();
ctx.moveTo(toCanvasX(xMin), toCanvasY(0));
for (let v = xMin; v <= xOpt + 0.1; v += 0.5) {
ctx.lineTo(toCanvasX(v), toCanvasY(normPDF(v)));
}
ctx.lineTo(toCanvasX(xOpt), toCanvasY(0));
ctx.closePath();
ctx.fillStyle = 'rgba(230, 57, 70, 0.18)';
ctx.fill();
// shade the accepted region (probability mass above x*)
ctx.beginPath();
ctx.moveTo(toCanvasX(xOpt), toCanvasY(0));
for (let v = xOpt; v <= xMax; v += 0.5) {
ctx.lineTo(toCanvasX(v), toCanvasY(normPDF(v)));
}
ctx.lineTo(toCanvasX(xMax), toCanvasY(0));
ctx.closePath();
ctx.fillStyle = 'rgba(10, 125, 51, 0.13)';
ctx.fill();
// draw the bell curve
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
for (let v = xMin; v <= xMax; v += 0.5) {
const cx = toCanvasX(v), cy = toCanvasY(normPDF(v));
v === xMin ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
}
ctx.stroke();
// dashed line at the mean mu
const muX = toCanvasX(MU);
ctx.beginPath();
ctx.setLineDash([5, 4]);
ctx.strokeStyle = '#888';
ctx.lineWidth = 1.2;
ctx.moveTo(muX, 10);
ctx.lineTo(muX, H - 30);
ctx.stroke();
ctx.setLineDash([]);
// solid red line at the optimal x*
const optX = toCanvasX(xOpt);
ctx.beginPath();
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2;
ctx.moveTo(optX, 10);
ctx.lineTo(optX, H - 30);
ctx.stroke();
// double-headed arrow showing the safety buffer width
const arrowY = H - 20;
ctx.beginPath();
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 1.5;
ctx.moveTo(optX, arrowY);
ctx.lineTo(muX, arrowY);
ctx.stroke();
ctx.fillStyle = '#c92f3c';
ctx.beginPath(); ctx.moveTo(optX - 5, arrowY - 3); ctx.lineTo(optX, arrowY); ctx.lineTo(optX - 5, arrowY + 3); ctx.fill();
ctx.beginPath(); ctx.moveTo(muX + 5, arrowY - 3); ctx.lineTo(muX, arrowY); ctx.lineTo(muX + 5, arrowY + 3); ctx.fill();
// axis labels for mu and x*
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#444';
ctx.textAlign = 'center';
ctx.fillText('μ=' + MU, muX, H - 4);
ctx.fillStyle = '#c92f3c';
ctx.fillText('x*=' + xOpt.toFixed(1), optX, H - 4);
// update the metric cards below the chart
document.getElementById('val-x').textContent = xOpt.toFixed(1);
document.getElementById('val-buf').textContent = buffer.toFixed(1);
document.getElementById('val-loss').textContent = loss.toFixed(1) + '%';
document.getElementById('conf-val').textContent = conf + '%';
// update the explanatory note based on current confidence
const noteEl = document.getElementById('note-text');
if (conf <= 55) {
noteEl.textContent = 'At low confidence the constraint barely binds — x* is close to the mean and the safety buffer is small.';
} else if (conf >= 97) {
noteEl.textContent = i18nFmt('At very high confidence the safety buffer is %buf% units — a large fraction of demand is sacrificed for near-certainty.', { buf: buffer.toFixed(1) });
} else {
noteEl.textContent = i18nFmt('At %conf% % confidence the chance constraint forces a safety buffer of %buf% units below the mean demand.', { conf: conf, buf: buffer.toFixed(1) });
}
}
const slider = document.getElementById('conf-slider');
slider.addEventListener('input', () => draw(Number(slider.value)));
draw(90);
<!-- Chance-constrained optimization demo — confidence slider controls the safety buffer -->
<div class="demo-wrap">
<div class="controls">
<label for="conf-slider">
Confidence level (1 - ε): <strong id="conf-val">90%</strong>
</label>
<input id="conf-slider" type="range" min="50" max="99" value="90" step="1"
aria-label="Confidence level slider">
</div>
<canvas id="chart" width="460" height="200" aria-label="Normal distribution with optimal production level and safety buffer"></canvas>
<div class="metrics">
<div class="metric">
<span class="metric-label">Optimal x*</span>
<span class="metric-val" id="val-x">—</span>
</div>
<div class="metric">
<span class="metric-label">Safety buffer</span>
<span class="metric-val" id="val-buf">—</span>
</div>
<div class="metric">
<span class="metric-label">Loss vs. mean</span>
<span class="metric-val" id="val-loss">—</span>
</div>
</div>
<p class="note" id="note-text">Drag the slider to change the required confidence level and observe how the safety buffer grows.</p>
</div>
/* layout and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.demo-wrap { max-width: 480px; margin: 0 auto; }
.controls { display: flex; flex-direction: column; gap: .3rem; margin-bottom: .6rem; }
label { font-size: .92rem; font-weight: 600; color: #1d3557; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
canvas { display: block; width: 100%; border-radius: 8px; background: #f4f6f8; border: 1px solid #dde3ea; }
/* metric cards */
.metrics { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
.metric { flex: 1; min-width: 120px; background: #eef2f6; border-radius: 8px;
padding: .45rem .65rem; border: 1px solid #cdd9e3; }
.metric-label { display: block; font-size: .76rem; color: #445; margin-bottom: .15rem; }
.metric-val { display: block; font-size: 1.1rem; font-weight: 700; color: #1d3557; }
.note { font-size: .85rem; color: #556; margin-top: .5rem; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Demand distribution: mean MU, std dev SIGMA
const MU = 100;
const SIGMA = 15;
// Normal distribution inverse CDF (quantile function)
// Rational approximation (Beasley-Springer-Moro algorithm)
function normInvCDF(p) {
if (p <= 0) return -Infinity;
if (p >= 1) return Infinity;
const a = [0, -3.969683028665376e1, 2.209460984245205e2,
-2.759285104469687e2, 1.383577518672690e2,
-3.066479806614716e1, 2.506628277459239];
const b = [0, -5.447609879822406e1, 1.615858368580409e2,
-1.556989798598866e2, 6.680131188771972e1, -1.328068155288572e1];
const c = [0, -7.784894002430293e-3, -3.223964580411365e-1,
-2.400758277161838, -2.549732539343734,
4.374664141464968, 2.938163982698783];
const d = [0, 7.784695709041462e-3, 3.224671290700398e-1,
2.445134137142996, 3.754408661907416];
const pLow = 0.02425;
let q;
if (p < pLow) {
const t = Math.sqrt(-2 * Math.log(p));
q = (((((c[1]*t+c[2])*t+c[3])*t+c[4])*t+c[5])*t+c[6]) /
((((d[1]*t+d[2])*t+d[3])*t+d[4])*t+1);
} else if (p <= 1 - pLow) {
const u = p - 0.5;
const r = u * u;
q = u * (((((a[1]*r+a[2])*r+a[3])*r+a[4])*r+a[5])*r+a[6]) /
(((((b[1]*r+b[2])*r+b[3])*r+b[4])*r+b[5])*r+1);
} else {
const t = Math.sqrt(-2 * Math.log(1 - p));
q = -(((((c[1]*t+c[2])*t+c[3])*t+c[4])*t+c[5])*t+c[6]) /
((((d[1]*t+d[2])*t+d[3])*t+d[4])*t+1);
}
return q;
}
// Normal probability density function centered at MU
function normPDF(x) {
return Math.exp(-0.5 * ((x - MU) / SIGMA) ** 2) / (SIGMA * Math.sqrt(2 * Math.PI));
}
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Main draw function — redraws chart for a given confidence level
function draw(conf) {
ctx.clearRect(0, 0, W, H);
const eps = 1 - conf / 100;
// compute optimal x via chance constraint formula
const z = normInvCDF(conf / 100);
const xOpt = MU - z * SIGMA;
const buffer = MU - xOpt;
const loss = ((MU - xOpt) / MU * 100);
// map probability domain to canvas coordinates
const xMin = MU - 4 * SIGMA;
const xMax = MU + 4 * SIGMA;
const toCanvasX = (v) => (v - xMin) / (xMax - xMin) * (W - 40) + 20;
const maxPDF = normPDF(MU);
const toCanvasY = (p) => H - 30 - (p / (maxPDF * 1.1)) * (H - 50);
// shade the rejected tail (probability mass below x*)
ctx.beginPath();
ctx.moveTo(toCanvasX(xMin), toCanvasY(0));
for (let v = xMin; v <= xOpt + 0.1; v += 0.5) {
ctx.lineTo(toCanvasX(v), toCanvasY(normPDF(v)));
}
ctx.lineTo(toCanvasX(xOpt), toCanvasY(0));
ctx.closePath();
ctx.fillStyle = 'rgba(230, 57, 70, 0.18)';
ctx.fill();
// shade the accepted region (probability mass above x*)
ctx.beginPath();
ctx.moveTo(toCanvasX(xOpt), toCanvasY(0));
for (let v = xOpt; v <= xMax; v += 0.5) {
ctx.lineTo(toCanvasX(v), toCanvasY(normPDF(v)));
}
ctx.lineTo(toCanvasX(xMax), toCanvasY(0));
ctx.closePath();
ctx.fillStyle = 'rgba(10, 125, 51, 0.13)';
ctx.fill();
// draw the bell curve
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
for (let v = xMin; v <= xMax; v += 0.5) {
const cx = toCanvasX(v), cy = toCanvasY(normPDF(v));
v === xMin ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
}
ctx.stroke();
// dashed line at the mean mu
const muX = toCanvasX(MU);
ctx.beginPath();
ctx.setLineDash([5, 4]);
ctx.strokeStyle = '#888';
ctx.lineWidth = 1.2;
ctx.moveTo(muX, 10);
ctx.lineTo(muX, H - 30);
ctx.stroke();
ctx.setLineDash([]);
// solid red line at the optimal x*
const optX = toCanvasX(xOpt);
ctx.beginPath();
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2;
ctx.moveTo(optX, 10);
ctx.lineTo(optX, H - 30);
ctx.stroke();
// double-headed arrow showing the safety buffer width
const arrowY = H - 20;
ctx.beginPath();
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 1.5;
ctx.moveTo(optX, arrowY);
ctx.lineTo(muX, arrowY);
ctx.stroke();
ctx.fillStyle = '#c92f3c';
ctx.beginPath(); ctx.moveTo(optX - 5, arrowY - 3); ctx.lineTo(optX, arrowY); ctx.lineTo(optX - 5, arrowY + 3); ctx.fill();
ctx.beginPath(); ctx.moveTo(muX + 5, arrowY - 3); ctx.lineTo(muX, arrowY); ctx.lineTo(muX + 5, arrowY + 3); ctx.fill();
// axis labels for mu and x*
ctx.font = '11px system-ui, sans-serif';
ctx.fillStyle = '#444';
ctx.textAlign = 'center';
ctx.fillText('μ=' + MU, muX, H - 4);
ctx.fillStyle = '#c92f3c';
ctx.fillText('x*=' + xOpt.toFixed(1), optX, H - 4);
// update the metric cards below the chart
document.getElementById('val-x').textContent = xOpt.toFixed(1);
document.getElementById('val-buf').textContent = buffer.toFixed(1);
document.getElementById('val-loss').textContent = loss.toFixed(1) + '%';
document.getElementById('conf-val').textContent = conf + '%';
// update the explanatory note based on current confidence
const noteEl = document.getElementById('note-text');
if (conf <= 55) {
noteEl.textContent = 'At low confidence the constraint barely binds — x* is close to the mean and the safety buffer is small.';
} else if (conf >= 97) {
noteEl.textContent = i18nFmt('At very high confidence the safety buffer is %buf% units — a large fraction of demand is sacrificed for near-certainty.', { buf: buffer.toFixed(1) });
} else {
noteEl.textContent = i18nFmt('At %conf% % confidence the chance constraint forces a safety buffer of %buf% units below the mean demand.', { conf: conf, buf: buffer.toFixed(1) });
}
}
const slider = document.getElementById('conf-slider');
slider.addEventListener('input', () => draw(Number(slider.value)));
draw(90);
Move the slider and watch the safety buffer (the gap between μ and the allowed x) grow as confidence rises. At 50 % confidence the optimal x equals the mean; at 99 % it is nearly two standard deviations below it.
Comments
Loading comments...