The demo below simulates one decomposition cycle. A master problem holds a restricted set of columns and asks each subproblem for an improving column by checking reduced costs. Click Run one iteration to see the subproblem price out a new column and add it, or click Run to optimality to keep going until no profitable column remains.
<!-- Dantzig-Wolfe column generation demo — master problem panel and subproblem panel -->
<div class="dw-wrap">
<div class="panel master-panel">
<h3>Master Problem</h3>
<div class="info-line">Objective: <span id="master-obj">—</span></div>
<div class="info-line">Duals: <span id="master-duals">—</span></div>
<div class="col-list-label">Active columns:</div>
<div id="col-list" class="col-list"></div>
</div>
<div class="arrow-col">
<div class="arrow-block">
<div class="arrow-label">duals →</div>
<div class="arrow">→</div>
</div>
<div class="arrow-block">
<div class="arrow-label">← new column</div>
<div class="arrow rev">←</div>
</div>
</div>
<div class="panel sub-panel">
<h3>Subproblem (Pricing)</h3>
<div class="info-line">Best reduced cost: <span id="sub-rc">—</span></div>
<div class="info-line">New column: <span id="sub-col">—</span></div>
<div class="status-bar" id="status-bar">Press ’Run one iteration’ to begin column generation.</div>
</div>
</div>
<div class="btns">
<button id="btn-step" type="button">Run one iteration</button>
<button id="btn-run" type="button">Run to optimality</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="iter-line" id="iter-line">Iteration: <span id="iter-count">0</span></div>
/* Layout for the two-panel master/subproblem visualization */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; margin: 0; padding: 4px; }
.dw-wrap { display: flex; align-items: stretch; gap: 0; margin-bottom: .6rem; }
.panel { flex: 1; border: 1.5px solid #cdd9e3; border-radius: 10px; padding: .65rem .8rem; background: #f5f8fb; min-width: 0; }
.panel h3 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
.info-line { font-size: .82rem; color: #444; margin-bottom: .25rem; }
.info-line span { font-weight: 700; color: #1d3557; }
.col-list-label { font-size: .78rem; color: #666; margin-top: .35rem; margin-bottom: .2rem; }
.col-list { display: flex; flex-wrap: wrap; gap: .25rem; min-height: 1.6rem; }
.col-tag { font-size: .75rem; background: #dde8f0; border: 1px solid #b8cfe0; border-radius: 5px;
padding: .15rem .35rem; font-family: ui-monospace, monospace; transition: background .3s; }
.col-tag.new { background: #c3e8d0; border-color: #5cb37b; }
.arrow-col { display: flex; flex-direction: column; justify-content: center; align-items: center;
gap: .6rem; padding: 0 .4rem; }
.arrow-block { text-align: center; }
.arrow-label { font-size: .68rem; color: #888; margin-bottom: .05rem; }
.arrow { font-size: 1.3rem; color: #1d3557; }
.arrow.rev { color: #0a7d33; }
.status-bar { margin-top: .4rem; font-size: .82rem; font-weight: 600; min-height: 2.2rem;
border-radius: 7px; padding: .3rem .5rem; background: #e8eef3; color: #333; }
.status-bar.ok { background: #d4f0df; color: #0a7d33; }
.status-bar.done { background: #fff3cd; color: #856404; }
.status-bar.bad { background: #fde8e8; color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.iter-line { font-size: .8rem; color: #666; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Dantzig-Wolfe column generation — simplified two-block LP illustration
// Model: two independent blocks, each a small LP, linked by two capacity constraints.
// The master prices extreme points; subproblems find the most negative reduced cost.
// Master problem (Dantzig-Wolfe reformulation):
// minimize c^T x subject to Ax = b, x >= 0 (convex combination of block extreme points)
// Two blocks, each with four extreme points:
// Block 1 extreme points (x1,x2): (0,0),(4,0),(3,2),(0,3)
// Block 2 extreme points (x3,x4): (0,0),(5,0),(4,2),(0,4)
// Linking constraints: x1+x3 <= 6, x2+x4 <= 5
// Objective: minimize -(3*x1+2*x2) - (2*x3+4*x4) [i.e. maximize revenue]
// Extreme points of each block’s feasible region (LP vertices)
const BLOCK1 = [
[0,0], [4,0], [3,2], [0,3]
];
const BLOCK2 = [
[0,0], [5,0], [4,2], [0,4]
];
// Objective coefficients for each block
const C1 = [3,2]; // block 1 revenue per unit of (x1,x2)
const C2 = [2,4]; // block 2 revenue per unit of (x3,x4)
// Right-hand side of linking constraints
const B = [6, 5]; // capacity for resource 1 and resource 2
// Algorithm state
let columns = []; // columns added to the master so far (as human-readable labels)
let duals = [0, 0]; // dual prices for the two linking constraints, start at zero
let masterObj = 0;
let iteration = 0;
let done = false;
function dot(a, b) { return a.reduce((s,v,i)=>s+v*b[i], 0); }
// Compute reduced cost of a candidate column for each block
// reduced cost of a block point p using dual vector pi:
// rc = -(c^T p) + pi^T A_block p
// where A_block selects the linking-constraint coefficients for this block
function reducedCost1(p) {
// Block 1: columns are [x1,x2], linking rows are [x1, x2] with RHS [6,5]
return -(C1[0]*p[0] + C1[1]*p[1]) + duals[0]*p[0] + duals[1]*p[1];
}
function reducedCost2(p) {
// Block 2: columns are [x3,x4], linking rows are [x3, x4] with RHS [6,5]
return -(C2[0]*p[0] + C2[1]*p[1]) + duals[0]*p[0] + duals[1]*p[1];
}
// Simplified master LP solver (subgradient dual update for illustration)
// We simulate a simplified master: we track selected extreme points and solve
// the master LP by greedy dual update (for illustration).
function solveMaster() {
// Greedy: pick best extreme point per block under current duals
// Pick the best column from each block by objective contribution minus dual penalty.
// For illustration we use a simple greedy: find point in each block maximizing
// net objective (c - dual*A) contribution, then compute the resulting obj and duals.
let best1 = null, best1Score = -Infinity;
for (const p of BLOCK1) {
const score = C1[0]*p[0]+C1[1]*p[1] - duals[0]*p[0] - duals[1]*p[1];
if (score > best1Score) { best1Score = score; best1 = p; }
}
let best2 = null, best2Score = -Infinity;
for (const p of BLOCK2) {
const score = C2[0]*p[0]+C2[1]*p[1] - duals[0]*p[0] - duals[1]*p[1];
if (score > best2Score) { best2Score = score; best2 = p; }
}
// Subgradient dual update — move duals toward feasibility of linking constraints
// Update duals: dual[i] = max(0, prev + step*(usage - rhs)) — simple subgradient step
const stepSize = 0.4 / (iteration + 1);
const usage0 = (best1 ? best1[0] : 0) + (best2 ? best2[0] : 0);
const usage1 = (best1 ? best1[1] : 0) + (best2 ? best2[1] : 0);
duals[0] = Math.max(0, duals[0] + stepSize*(usage0 - B[0]));
duals[1] = Math.max(0, duals[1] + stepSize*(usage1 - B[1]));
// Compute primal objective value from selected extreme points
masterObj = (best1 ? C1[0]*best1[0]+C1[1]*best1[1] : 0)
+ (best2 ? C2[0]*best2[0]+C2[1]*best2[1] : 0);
return { best1, best2 };
}
// Pricing step: find the extreme point with most negative reduced cost in each block
function priceSubproblems() {
let minRc1 = Infinity, bestP1 = null;
for (const p of BLOCK1) {
const rc = reducedCost1(p);
if (rc < minRc1) { minRc1 = rc; bestP1 = p; }
}
let minRc2 = Infinity, bestP2 = null;
for (const p of BLOCK2) {
const rc = reducedCost2(p);
if (rc < minRc2) { minRc2 = rc; bestP2 = p; }
}
return { minRc1, bestP1, minRc2, bestP2 };
}
function fmt(n) { return n.toFixed(3); }
function colTag(label, isNew) {
const el = document.createElement('span');
el.className = 'col-tag' + (isNew ? ' new' : '');
el.textContent = label;
return el;
}
function render(newCols) {
document.getElementById('master-obj').textContent = fmt(masterObj);
document.getElementById('master-duals').textContent =
'π₁=' + fmt(duals[0]) + ', π₂=' + fmt(duals[1]);
const list = document.getElementById('col-list');
list.innerHTML = '';
columns.forEach((c, i) => {
const isNew = newCols && i >= columns.length - newCols;
list.appendChild(colTag(c, isNew));
});
document.getElementById('iter-count').textContent = iteration;
}
function setStatus(msg, cls) {
const el = document.getElementById('status-bar');
el.textContent = msg;
el.className = 'status-bar ' + (cls||'');
}
function step() {
if (done) return;
iteration++;
// Step 1: price subproblems with current duals to find best extreme points
// Step 1: price subproblems with current duals to find best extreme points
const { minRc1, bestP1, minRc2, bestP2 } = priceSubproblems();
const overallMinRc = Math.min(minRc1, minRc2);
document.getElementById('sub-rc').textContent = fmt(overallMinRc);
// Step 2: add any genuinely new columns with negative reduced cost
// Step 2: add any genuinely new columns with negative reduced cost
let addedCount = 0;
let newColLabel = '';
if (minRc1 < -0.001 && bestP1) {
const lbl = 'B1(' + bestP1[0] + ',' + bestP1[1] + ')';
if (!columns.includes(lbl)) { columns.push(lbl); addedCount++; newColLabel = lbl; }
}
if (minRc2 < -0.001 && bestP2) {
const lbl = 'B2(' + bestP2[0] + ',' + bestP2[1] + ')';
if (!columns.includes(lbl)) { columns.push(lbl); addedCount++; newColLabel += (newColLabel?' & ':'') + lbl; }
}
document.getElementById('sub-col').textContent = newColLabel || '—';
// Optimality: no new column has negative reduced cost (all best extreme points already in master)
// Optimality: no new column has negative reduced cost (all best extreme points already in master)
if (addedCount === 0) {
done = true;
// Run a final master solve to update displayed objective
solveMaster();
render(0);
setStatus('Optimality reached — no column has negative reduced cost.', 'done');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
return;
}
// Step 3: re-solve master with the new columns (updates masterObj and duals)
// Step 3: re-solve master with the new columns (updates masterObj and duals)
solveMaster();
render(addedCount);
setStatus(i18nFmt('Added column %newColLabel% to the master.', { newColLabel: newColLabel }), 'ok');
}
function runAll() {
// Safety limit to avoid infinite loop in edge cases
if (done) return;
const MAX = 20;
while (!done && iteration < MAX) step();
}
function reset() {
columns = [];
duals = [0, 0];
masterObj = 0;
iteration = 0;
done = false;
document.getElementById('master-obj').textContent = '—';
document.getElementById('master-duals').textContent = '—';
document.getElementById('sub-rc').textContent = '—';
document.getElementById('sub-col').textContent = '—';
document.getElementById('col-list').innerHTML = '';
document.getElementById('iter-count').textContent = '0';
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
setStatus('Press ’Run one iteration’ to begin column generation.', '');
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', runAll);
document.getElementById('btn-reset').addEventListener('click', reset);
<!-- Dantzig-Wolfe column generation demo — master problem panel and subproblem panel -->
<div class="dw-wrap">
<div class="panel master-panel">
<h3>Master Problem</h3>
<div class="info-line">Objective: <span id="master-obj">—</span></div>
<div class="info-line">Duals: <span id="master-duals">—</span></div>
<div class="col-list-label">Active columns:</div>
<div id="col-list" class="col-list"></div>
</div>
<div class="arrow-col">
<div class="arrow-block">
<div class="arrow-label">duals →</div>
<div class="arrow">→</div>
</div>
<div class="arrow-block">
<div class="arrow-label">← new column</div>
<div class="arrow rev">←</div>
</div>
</div>
<div class="panel sub-panel">
<h3>Subproblem (Pricing)</h3>
<div class="info-line">Best reduced cost: <span id="sub-rc">—</span></div>
<div class="info-line">New column: <span id="sub-col">—</span></div>
<div class="status-bar" id="status-bar">Press ’Run one iteration’ to begin column generation.</div>
</div>
</div>
<div class="btns">
<button id="btn-step" type="button">Run one iteration</button>
<button id="btn-run" type="button">Run to optimality</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="iter-line" id="iter-line">Iteration: <span id="iter-count">0</span></div>
/* Layout for the two-panel master/subproblem visualization */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; font-size: 14px; color: #222; margin: 0; padding: 4px; }
.dw-wrap { display: flex; align-items: stretch; gap: 0; margin-bottom: .6rem; }
.panel { flex: 1; border: 1.5px solid #cdd9e3; border-radius: 10px; padding: .65rem .8rem; background: #f5f8fb; min-width: 0; }
.panel h3 { margin: 0 0 .4rem; font-size: .9rem; color: #1d3557; }
.info-line { font-size: .82rem; color: #444; margin-bottom: .25rem; }
.info-line span { font-weight: 700; color: #1d3557; }
.col-list-label { font-size: .78rem; color: #666; margin-top: .35rem; margin-bottom: .2rem; }
.col-list { display: flex; flex-wrap: wrap; gap: .25rem; min-height: 1.6rem; }
.col-tag { font-size: .75rem; background: #dde8f0; border: 1px solid #b8cfe0; border-radius: 5px;
padding: .15rem .35rem; font-family: ui-monospace, monospace; transition: background .3s; }
.col-tag.new { background: #c3e8d0; border-color: #5cb37b; }
.arrow-col { display: flex; flex-direction: column; justify-content: center; align-items: center;
gap: .6rem; padding: 0 .4rem; }
.arrow-block { text-align: center; }
.arrow-label { font-size: .68rem; color: #888; margin-bottom: .05rem; }
.arrow { font-size: 1.3rem; color: #1d3557; }
.arrow.rev { color: #0a7d33; }
.status-bar { margin-top: .4rem; font-size: .82rem; font-weight: 600; min-height: 2.2rem;
border-radius: 7px; padding: .3rem .5rem; background: #e8eef3; color: #333; }
.status-bar.ok { background: #d4f0df; color: #0a7d33; }
.status-bar.done { background: #fff3cd; color: #856404; }
.status-bar.bad { background: #fde8e8; color: #c92f3c; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.iter-line { font-size: .8rem; color: #666; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Dantzig-Wolfe column generation — simplified two-block LP illustration
// Model: two independent blocks, each a small LP, linked by two capacity constraints.
// The master prices extreme points; subproblems find the most negative reduced cost.
// Master problem (Dantzig-Wolfe reformulation):
// minimize c^T x subject to Ax = b, x >= 0 (convex combination of block extreme points)
// Two blocks, each with four extreme points:
// Block 1 extreme points (x1,x2): (0,0),(4,0),(3,2),(0,3)
// Block 2 extreme points (x3,x4): (0,0),(5,0),(4,2),(0,4)
// Linking constraints: x1+x3 <= 6, x2+x4 <= 5
// Objective: minimize -(3*x1+2*x2) - (2*x3+4*x4) [i.e. maximize revenue]
// Extreme points of each block’s feasible region (LP vertices)
const BLOCK1 = [
[0,0], [4,0], [3,2], [0,3]
];
const BLOCK2 = [
[0,0], [5,0], [4,2], [0,4]
];
// Objective coefficients for each block
const C1 = [3,2]; // block 1 revenue per unit of (x1,x2)
const C2 = [2,4]; // block 2 revenue per unit of (x3,x4)
// Right-hand side of linking constraints
const B = [6, 5]; // capacity for resource 1 and resource 2
// Algorithm state
let columns = []; // columns added to the master so far (as human-readable labels)
let duals = [0, 0]; // dual prices for the two linking constraints, start at zero
let masterObj = 0;
let iteration = 0;
let done = false;
function dot(a, b) { return a.reduce((s,v,i)=>s+v*b[i], 0); }
// Compute reduced cost of a candidate column for each block
// reduced cost of a block point p using dual vector pi:
// rc = -(c^T p) + pi^T A_block p
// where A_block selects the linking-constraint coefficients for this block
function reducedCost1(p) {
// Block 1: columns are [x1,x2], linking rows are [x1, x2] with RHS [6,5]
return -(C1[0]*p[0] + C1[1]*p[1]) + duals[0]*p[0] + duals[1]*p[1];
}
function reducedCost2(p) {
// Block 2: columns are [x3,x4], linking rows are [x3, x4] with RHS [6,5]
return -(C2[0]*p[0] + C2[1]*p[1]) + duals[0]*p[0] + duals[1]*p[1];
}
// Simplified master LP solver (subgradient dual update for illustration)
// We simulate a simplified master: we track selected extreme points and solve
// the master LP by greedy dual update (for illustration).
function solveMaster() {
// Greedy: pick best extreme point per block under current duals
// Pick the best column from each block by objective contribution minus dual penalty.
// For illustration we use a simple greedy: find point in each block maximizing
// net objective (c - dual*A) contribution, then compute the resulting obj and duals.
let best1 = null, best1Score = -Infinity;
for (const p of BLOCK1) {
const score = C1[0]*p[0]+C1[1]*p[1] - duals[0]*p[0] - duals[1]*p[1];
if (score > best1Score) { best1Score = score; best1 = p; }
}
let best2 = null, best2Score = -Infinity;
for (const p of BLOCK2) {
const score = C2[0]*p[0]+C2[1]*p[1] - duals[0]*p[0] - duals[1]*p[1];
if (score > best2Score) { best2Score = score; best2 = p; }
}
// Subgradient dual update — move duals toward feasibility of linking constraints
// Update duals: dual[i] = max(0, prev + step*(usage - rhs)) — simple subgradient step
const stepSize = 0.4 / (iteration + 1);
const usage0 = (best1 ? best1[0] : 0) + (best2 ? best2[0] : 0);
const usage1 = (best1 ? best1[1] : 0) + (best2 ? best2[1] : 0);
duals[0] = Math.max(0, duals[0] + stepSize*(usage0 - B[0]));
duals[1] = Math.max(0, duals[1] + stepSize*(usage1 - B[1]));
// Compute primal objective value from selected extreme points
masterObj = (best1 ? C1[0]*best1[0]+C1[1]*best1[1] : 0)
+ (best2 ? C2[0]*best2[0]+C2[1]*best2[1] : 0);
return { best1, best2 };
}
// Pricing step: find the extreme point with most negative reduced cost in each block
function priceSubproblems() {
let minRc1 = Infinity, bestP1 = null;
for (const p of BLOCK1) {
const rc = reducedCost1(p);
if (rc < minRc1) { minRc1 = rc; bestP1 = p; }
}
let minRc2 = Infinity, bestP2 = null;
for (const p of BLOCK2) {
const rc = reducedCost2(p);
if (rc < minRc2) { minRc2 = rc; bestP2 = p; }
}
return { minRc1, bestP1, minRc2, bestP2 };
}
function fmt(n) { return n.toFixed(3); }
function colTag(label, isNew) {
const el = document.createElement('span');
el.className = 'col-tag' + (isNew ? ' new' : '');
el.textContent = label;
return el;
}
function render(newCols) {
document.getElementById('master-obj').textContent = fmt(masterObj);
document.getElementById('master-duals').textContent =
'π₁=' + fmt(duals[0]) + ', π₂=' + fmt(duals[1]);
const list = document.getElementById('col-list');
list.innerHTML = '';
columns.forEach((c, i) => {
const isNew = newCols && i >= columns.length - newCols;
list.appendChild(colTag(c, isNew));
});
document.getElementById('iter-count').textContent = iteration;
}
function setStatus(msg, cls) {
const el = document.getElementById('status-bar');
el.textContent = msg;
el.className = 'status-bar ' + (cls||'');
}
function step() {
if (done) return;
iteration++;
// Step 1: price subproblems with current duals to find best extreme points
// Step 1: price subproblems with current duals to find best extreme points
const { minRc1, bestP1, minRc2, bestP2 } = priceSubproblems();
const overallMinRc = Math.min(minRc1, minRc2);
document.getElementById('sub-rc').textContent = fmt(overallMinRc);
// Step 2: add any genuinely new columns with negative reduced cost
// Step 2: add any genuinely new columns with negative reduced cost
let addedCount = 0;
let newColLabel = '';
if (minRc1 < -0.001 && bestP1) {
const lbl = 'B1(' + bestP1[0] + ',' + bestP1[1] + ')';
if (!columns.includes(lbl)) { columns.push(lbl); addedCount++; newColLabel = lbl; }
}
if (minRc2 < -0.001 && bestP2) {
const lbl = 'B2(' + bestP2[0] + ',' + bestP2[1] + ')';
if (!columns.includes(lbl)) { columns.push(lbl); addedCount++; newColLabel += (newColLabel?' & ':'') + lbl; }
}
document.getElementById('sub-col').textContent = newColLabel || '—';
// Optimality: no new column has negative reduced cost (all best extreme points already in master)
// Optimality: no new column has negative reduced cost (all best extreme points already in master)
if (addedCount === 0) {
done = true;
// Run a final master solve to update displayed objective
solveMaster();
render(0);
setStatus('Optimality reached — no column has negative reduced cost.', 'done');
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-run').disabled = true;
return;
}
// Step 3: re-solve master with the new columns (updates masterObj and duals)
// Step 3: re-solve master with the new columns (updates masterObj and duals)
solveMaster();
render(addedCount);
setStatus(i18nFmt('Added column %newColLabel% to the master.', { newColLabel: newColLabel }), 'ok');
}
function runAll() {
// Safety limit to avoid infinite loop in edge cases
if (done) return;
const MAX = 20;
while (!done && iteration < MAX) step();
}
function reset() {
columns = [];
duals = [0, 0];
masterObj = 0;
iteration = 0;
done = false;
document.getElementById('master-obj').textContent = '—';
document.getElementById('master-duals').textContent = '—';
document.getElementById('sub-rc').textContent = '—';
document.getElementById('sub-col').textContent = '—';
document.getElementById('col-list').innerHTML = '';
document.getElementById('iter-count').textContent = '0';
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-run').disabled = false;
setStatus('Press ’Run one iteration’ to begin column generation.', '');
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', runAll);
document.getElementById('btn-reset').addEventListener('click', reset);
Notice the pattern: each iteration the master gets a tighter LP bound, and the subproblem's best reduced cost creeps toward zero. When it reaches zero (or becomes positive), you have proven the current master solution is LP-optimal — without ever listing all possible columns.
Comments
Loading comments...