Below is a 3-coordinate simplex (an equilateral triangle). A linear objective f(x)=c⋅x is minimized over the simplex — the optimum is always at a vertex. Both algorithms start at the center (31,31,31) and take steps toward the same minimum.
<!-- Two-panel demo: simplex trajectory + objective value plot -->
<div class="controls">
<label>Step size η: <input id="eta" type="range" min="0.05" max="1.0" step="0.05" value="0.3"> <span id="eta-val">0.3</span></label>
<label>Gradient direction:
<select id="dir">
<option value="0">Push toward vertex x₀</option>
<option value="1">Push toward vertex x₁</option>
<option value="2">Push toward vertex x₂</option>
</select>
</label>
</div>
<div class="panels">
<canvas id="simplex" width="200" height="180" title="Simplex trajectory"></canvas>
<canvas id="objplot" width="200" height="180" title="Objective value over steps"></canvas>
</div>
<div class="legend">
<span class="dot eucl"></span>Euclidean projected GD
<span class="dot mirr"></span>Mirror descent (entropy)
</div>
<div id="status" class="status"></div>
<div class="btns">
<button id="btn-step">Step</button>
<button id="btn-run">Run</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
/* Styles for the mirror descent demo panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin-bottom: .4rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 90px; }
.panels { display: flex; gap: 8px; flex-wrap: wrap; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f7fafc; }
.legend { display: flex; align-items: center; gap: 1rem; font-size: .82rem; margin: .35rem 0; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; margin-right: 3px; }
.dot.eucl { background: #e63946; }
.dot.mirr { background: #1d6fa5; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.4em; margin: .25rem 0; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Mirror descent vs Euclidean projected gradient descent on the 3-simplex.
// Feasible set: probability simplex {x: x_i >= 0, sum(x) = 1}.
// Euclidean GD step: x_{t+1} = proj(x_t - eta * g)
// Mirror descent (entropy map) step: x_{t+1,i} = x_{t,i} * exp(-eta * g_i) / Z
const cs = document.getElementById('simplex');
const cp = document.getElementById('objplot');
const ctxS = cs.getContext('2d');
const ctxP = cp.getContext('2d');
const WS = cs.width, HS = cs.height;
const WP = cp.width, HP = cp.height;
// Canvas pixel coordinates of the three simplex vertices
const V = [
[WS/2, 10], // top vertex: x0=1
[10, HS-10], // bottom-left vertex: x1=1
[WS-10, HS-10] // bottom-right vertex: x2=1
];
// Barycentric to canvas-pixel conversion
function toPx(x) {
return [
x[0]*V[0][0] + x[1]*V[1][0] + x[2]*V[2][0],
x[0]*V[0][1] + x[1]*V[1][1] + x[2]*V[2][1]
];
}
// Linear objective gradients — one per target vertex
const GRADS = [
[-1, 0.5, 0.5], // pushes toward vertex x0=1
[ 0.5, -1, 0.5], // pushes toward vertex x1=1
[ 0.5, 0.5, -1] // pushes toward vertex x2=1
];
// Evaluate the linear objective f(x) = g . x
function objective(pt, g) { return g[0]*pt[0] + g[1]*pt[1] + g[2]*pt[2]; }
// Project onto the simplex using the O(n log n) sort algorithm
function projSimplex(v) {
const u = [...v].sort((a, b) => b - a);
let cssv = 0, rho = 0;
for (let j = 0; j < 3; j++) {
cssv += u[j];
if (u[j] - (cssv - 1) / (j + 1) > 0) rho = j;
}
const theta = (u.slice(0, rho+1).reduce((a,b) => a+b, 0) - 1) / (rho + 1);
return v.map(vi => Math.max(vi - theta, 0));
}
// Shared state: current points, paths, and objective histories
let eucl, mirr, path_e, path_m, obj_e, obj_m, running, animId, stepCount;
const MAX_STEPS = 60;
function getEta() { return parseFloat(document.getElementById('eta').value); }
function getGrad() { return GRADS[parseInt(document.getElementById('dir').value)]; }
function reset() {
if (animId) { cancelAnimationFrame(animId); animId = null; }
running = false;
stepCount = 0;
// Start from the uniform distribution (centroid of the simplex)
eucl = [1/3, 1/3, 1/3];
mirr = [1/3, 1/3, 1/3];
path_e = [[...eucl]];
path_m = [[...mirr]];
const g = getGrad();
obj_e = [objective(eucl, g)];
obj_m = [objective(mirr, g)];
document.getElementById('btn-run').textContent = I18N.btn_run;
setStatus('', '');
draw();
}
// Euclidean projected gradient descent update
function stepEuclidean(g, eta) {
const raw = eucl.map((xi, i) => xi - eta * g[i]);
eucl = projSimplex(raw);
path_e.push([...eucl]);
obj_e.push(objective(eucl, g));
}
// Mirror descent update: multiply by exp(-eta*g), renormalize
function stepMirror(g, eta) {
const raw = mirr.map((xi, i) => xi * Math.exp(-eta * g[i]));
const Z = raw.reduce((a, b) => a + b, 0);
mirr = raw.map(v => v / Z);
path_m.push([...mirr]);
obj_m.push(objective(mirr, g));
}
function drawSimplex() {
ctxS.clearRect(0, 0, WS, HS);
// Draw filled triangle representing the simplex
ctxS.beginPath();
ctxS.moveTo(V[0][0], V[0][1]);
ctxS.lineTo(V[1][0], V[1][1]);
ctxS.lineTo(V[2][0], V[2][1]);
ctxS.closePath();
ctxS.fillStyle = '#eef3f7';
ctxS.fill();
ctxS.strokeStyle = '#8a9aac';
ctxS.lineWidth = 1.3;
ctxS.stroke();
// Label the three extreme points
ctxS.font = '10px ui-monospace, monospace';
ctxS.fillStyle = '#3d5166';
ctxS.textAlign = 'center';
ctxS.fillText('x₀=1', V[0][0], V[0][1] - 3);
ctxS.fillText('x₁=1', V[1][0] - 2, V[1][1] + 11);
ctxS.fillText('x₂=1', V[2][0] + 2, V[2][1] + 11);
// Mark the shared starting point with a gray dot
const [sx, sy] = toPx([1/3,1/3,1/3]);
ctxS.beginPath(); ctxS.arc(sx, sy, 3, 0, 2*Math.PI);
ctxS.fillStyle = '#888'; ctxS.fill();
// Draw dashed trajectory lines for both algorithms
for (const [pts, color] of [[path_e, '#e63946'], [path_m, '#1d6fa5']]) {
if (pts.length < 2) continue;
ctxS.beginPath();
ctxS.strokeStyle = color; ctxS.lineWidth = 1.6; ctxS.setLineDash([3,3]);
const [x0, y0] = toPx(pts[0]); ctxS.moveTo(x0, y0);
for (let i = 1; i < pts.length; i++) { const [x,y] = toPx(pts[i]); ctxS.lineTo(x,y); }
ctxS.stroke(); ctxS.setLineDash([]);
}
// Draw current position dots for both algorithms
for (const [pt, color] of [[eucl, '#e63946'], [mirr, '#1d6fa5']]) {
const [px, py] = toPx(pt);
ctxS.beginPath(); ctxS.arc(px, py, 4, 0, 2*Math.PI);
ctxS.fillStyle = color; ctxS.fill();
}
}
function drawObjPlot() {
ctxP.clearRect(0, 0, WP, HP);
const PAD = { l: 26, r: 8, t: 14, b: 22 };
const pw = WP - PAD.l - PAD.r, ph = HP - PAD.t - PAD.b;
// Draw plot axes for the objective value chart
ctxP.strokeStyle = '#8a9aac'; ctxP.lineWidth = 1;
ctxP.beginPath();
ctxP.moveTo(PAD.l, PAD.t); ctxP.lineTo(PAD.l, PAD.t + ph);
ctxP.lineTo(PAD.l + pw, PAD.t + ph);
ctxP.stroke();
ctxP.font = '9px system-ui'; ctxP.fillStyle = '#3d5166'; ctxP.textAlign = 'center';
ctxP.fillText(I18N.axis_x, PAD.l + pw/2, HP - 3);
ctxP.save(); ctxP.translate(9, PAD.t + ph/2); ctxP.rotate(-Math.PI/2);
ctxP.fillText(I18N.axis_y, 0, 0); ctxP.restore();
const allVals = [...obj_e, ...obj_m];
const minV = Math.min(...allVals), maxV = Math.max(...allVals);
const range = maxV - minV || 1;
const T = Math.max(obj_e.length, obj_m.length);
const toXY = (t, v) => [
PAD.l + (t / Math.max(T - 1, 1)) * pw,
PAD.t + (1 - (v - minV) / range) * ph
];
// Plot objective value over steps for both algorithms
for (const [obj, color] of [[obj_e, '#e63946'], [obj_m, '#1d6fa5']]) {
if (obj.length < 2) continue;
ctxP.beginPath(); ctxP.strokeStyle = color; ctxP.lineWidth = 1.6;
ctxP.setLineDash([]);
const [x0, y0] = toXY(0, obj[0]); ctxP.moveTo(x0, y0);
for (let i = 1; i < obj.length; i++) {
const [x, y] = toXY(i, obj[i]); ctxP.lineTo(x, y);
}
ctxP.stroke();
}
ctxP.font = '9px system-ui'; ctxP.fillStyle = '#555'; ctxP.textAlign = 'right';
ctxP.fillText(minV.toFixed(2), PAD.l - 2, PAD.t + ph);
ctxP.fillText(maxV.toFixed(2), PAD.l - 2, PAD.t + 6);
}
function draw() { drawSimplex(); drawObjPlot(); }
function step() {
if (stepCount >= MAX_STEPS) return;
const g = getGrad(), eta = getEta();
stepEuclidean(g, eta);
stepMirror(g, eta);
stepCount++;
draw();
const last_e = obj_e[obj_e.length - 1];
const last_m = obj_m[obj_m.length - 1];
setStatus(
(I18N.msg_step||'')
.replace('{n}', stepCount)
.replace('{e}', last_e.toFixed(3))
.replace('{m}', last_m.toFixed(3)),
'ok'
);
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
let I18N = {};
function animate() {
if (stepCount < MAX_STEPS) {
step();
animId = requestAnimationFrame(() => setTimeout(animate, 120));
} else {
running = false;
document.getElementById('btn-run').textContent = I18N.btn_run;
}
}
document.getElementById('eta').addEventListener('input', function() {
document.getElementById('eta-val').textContent = this.value;
reset();
});
document.getElementById('btn-step').addEventListener('click', () => { if (!running) step(); });
document.getElementById('btn-run').addEventListener('click', function() {
if (running) {
running = false;
if (animId) { cancelAnimationFrame(animId); animId = null; }
this.textContent = I18N.btn_run;
} else {
running = true;
this.textContent = I18N.btn_pause;
animate();
}
});
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('dir').addEventListener('change', reset);
// Receive translations from the parent kipu frame
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'kipu-i18n') {
I18N = e.data.translations;
reset();
}
});
// Fallback if message event never fires
setTimeout(() => { if (!Object.keys(I18N).length) { I18N = window.__KIPU_I18N_FALLBACK__ || {}; reset(); } }, 200);
<!-- Two-panel demo: simplex trajectory + objective value plot -->
<div class="controls">
<label>Step size η: <input id="eta" type="range" min="0.05" max="1.0" step="0.05" value="0.3"> <span id="eta-val">0.3</span></label>
<label>Gradient direction:
<select id="dir">
<option value="0">Push toward vertex x₀</option>
<option value="1">Push toward vertex x₁</option>
<option value="2">Push toward vertex x₂</option>
</select>
</label>
</div>
<div class="panels">
<canvas id="simplex" width="200" height="180" title="Simplex trajectory"></canvas>
<canvas id="objplot" width="200" height="180" title="Objective value over steps"></canvas>
</div>
<div class="legend">
<span class="dot eucl"></span>Euclidean projected GD
<span class="dot mirr"></span>Mirror descent (entropy)
</div>
<div id="status" class="status"></div>
<div class="btns">
<button id="btn-step">Step</button>
<button id="btn-run">Run</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
/* Styles for the mirror descent demo panels */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin-bottom: .4rem; font-size: .85rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 90px; }
.panels { display: flex; gap: 8px; flex-wrap: wrap; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f7fafc; }
.legend { display: flex; align-items: center; gap: 1rem; font-size: .82rem; margin: .35rem 0; }
.dot { display: inline-block; width: 11px; height: 11px; border-radius: 50%; margin-right: 3px; }
.dot.eucl { background: #e63946; }
.dot.mirr { background: #1d6fa5; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.4em; margin: .25rem 0; }
.status.ok { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
// Mirror descent vs Euclidean projected gradient descent on the 3-simplex.
// Feasible set: probability simplex {x: x_i >= 0, sum(x) = 1}.
// Euclidean GD step: x_{t+1} = proj(x_t - eta * g)
// Mirror descent (entropy map) step: x_{t+1,i} = x_{t,i} * exp(-eta * g_i) / Z
const cs = document.getElementById('simplex');
const cp = document.getElementById('objplot');
const ctxS = cs.getContext('2d');
const ctxP = cp.getContext('2d');
const WS = cs.width, HS = cs.height;
const WP = cp.width, HP = cp.height;
// Canvas pixel coordinates of the three simplex vertices
const V = [
[WS/2, 10], // top vertex: x0=1
[10, HS-10], // bottom-left vertex: x1=1
[WS-10, HS-10] // bottom-right vertex: x2=1
];
// Barycentric to canvas-pixel conversion
function toPx(x) {
return [
x[0]*V[0][0] + x[1]*V[1][0] + x[2]*V[2][0],
x[0]*V[0][1] + x[1]*V[1][1] + x[2]*V[2][1]
];
}
// Linear objective gradients — one per target vertex
const GRADS = [
[-1, 0.5, 0.5], // pushes toward vertex x0=1
[ 0.5, -1, 0.5], // pushes toward vertex x1=1
[ 0.5, 0.5, -1] // pushes toward vertex x2=1
];
// Evaluate the linear objective f(x) = g . x
function objective(pt, g) { return g[0]*pt[0] + g[1]*pt[1] + g[2]*pt[2]; }
// Project onto the simplex using the O(n log n) sort algorithm
function projSimplex(v) {
const u = [...v].sort((a, b) => b - a);
let cssv = 0, rho = 0;
for (let j = 0; j < 3; j++) {
cssv += u[j];
if (u[j] - (cssv - 1) / (j + 1) > 0) rho = j;
}
const theta = (u.slice(0, rho+1).reduce((a,b) => a+b, 0) - 1) / (rho + 1);
return v.map(vi => Math.max(vi - theta, 0));
}
// Shared state: current points, paths, and objective histories
let eucl, mirr, path_e, path_m, obj_e, obj_m, running, animId, stepCount;
const MAX_STEPS = 60;
function getEta() { return parseFloat(document.getElementById('eta').value); }
function getGrad() { return GRADS[parseInt(document.getElementById('dir').value)]; }
function reset() {
if (animId) { cancelAnimationFrame(animId); animId = null; }
running = false;
stepCount = 0;
// Start from the uniform distribution (centroid of the simplex)
eucl = [1/3, 1/3, 1/3];
mirr = [1/3, 1/3, 1/3];
path_e = [[...eucl]];
path_m = [[...mirr]];
const g = getGrad();
obj_e = [objective(eucl, g)];
obj_m = [objective(mirr, g)];
document.getElementById('btn-run').textContent = I18N.btn_run;
setStatus('', '');
draw();
}
// Euclidean projected gradient descent update
function stepEuclidean(g, eta) {
const raw = eucl.map((xi, i) => xi - eta * g[i]);
eucl = projSimplex(raw);
path_e.push([...eucl]);
obj_e.push(objective(eucl, g));
}
// Mirror descent update: multiply by exp(-eta*g), renormalize
function stepMirror(g, eta) {
const raw = mirr.map((xi, i) => xi * Math.exp(-eta * g[i]));
const Z = raw.reduce((a, b) => a + b, 0);
mirr = raw.map(v => v / Z);
path_m.push([...mirr]);
obj_m.push(objective(mirr, g));
}
function drawSimplex() {
ctxS.clearRect(0, 0, WS, HS);
// Draw filled triangle representing the simplex
ctxS.beginPath();
ctxS.moveTo(V[0][0], V[0][1]);
ctxS.lineTo(V[1][0], V[1][1]);
ctxS.lineTo(V[2][0], V[2][1]);
ctxS.closePath();
ctxS.fillStyle = '#eef3f7';
ctxS.fill();
ctxS.strokeStyle = '#8a9aac';
ctxS.lineWidth = 1.3;
ctxS.stroke();
// Label the three extreme points
ctxS.font = '10px ui-monospace, monospace';
ctxS.fillStyle = '#3d5166';
ctxS.textAlign = 'center';
ctxS.fillText('x₀=1', V[0][0], V[0][1] - 3);
ctxS.fillText('x₁=1', V[1][0] - 2, V[1][1] + 11);
ctxS.fillText('x₂=1', V[2][0] + 2, V[2][1] + 11);
// Mark the shared starting point with a gray dot
const [sx, sy] = toPx([1/3,1/3,1/3]);
ctxS.beginPath(); ctxS.arc(sx, sy, 3, 0, 2*Math.PI);
ctxS.fillStyle = '#888'; ctxS.fill();
// Draw dashed trajectory lines for both algorithms
for (const [pts, color] of [[path_e, '#e63946'], [path_m, '#1d6fa5']]) {
if (pts.length < 2) continue;
ctxS.beginPath();
ctxS.strokeStyle = color; ctxS.lineWidth = 1.6; ctxS.setLineDash([3,3]);
const [x0, y0] = toPx(pts[0]); ctxS.moveTo(x0, y0);
for (let i = 1; i < pts.length; i++) { const [x,y] = toPx(pts[i]); ctxS.lineTo(x,y); }
ctxS.stroke(); ctxS.setLineDash([]);
}
// Draw current position dots for both algorithms
for (const [pt, color] of [[eucl, '#e63946'], [mirr, '#1d6fa5']]) {
const [px, py] = toPx(pt);
ctxS.beginPath(); ctxS.arc(px, py, 4, 0, 2*Math.PI);
ctxS.fillStyle = color; ctxS.fill();
}
}
function drawObjPlot() {
ctxP.clearRect(0, 0, WP, HP);
const PAD = { l: 26, r: 8, t: 14, b: 22 };
const pw = WP - PAD.l - PAD.r, ph = HP - PAD.t - PAD.b;
// Draw plot axes for the objective value chart
ctxP.strokeStyle = '#8a9aac'; ctxP.lineWidth = 1;
ctxP.beginPath();
ctxP.moveTo(PAD.l, PAD.t); ctxP.lineTo(PAD.l, PAD.t + ph);
ctxP.lineTo(PAD.l + pw, PAD.t + ph);
ctxP.stroke();
ctxP.font = '9px system-ui'; ctxP.fillStyle = '#3d5166'; ctxP.textAlign = 'center';
ctxP.fillText(I18N.axis_x, PAD.l + pw/2, HP - 3);
ctxP.save(); ctxP.translate(9, PAD.t + ph/2); ctxP.rotate(-Math.PI/2);
ctxP.fillText(I18N.axis_y, 0, 0); ctxP.restore();
const allVals = [...obj_e, ...obj_m];
const minV = Math.min(...allVals), maxV = Math.max(...allVals);
const range = maxV - minV || 1;
const T = Math.max(obj_e.length, obj_m.length);
const toXY = (t, v) => [
PAD.l + (t / Math.max(T - 1, 1)) * pw,
PAD.t + (1 - (v - minV) / range) * ph
];
// Plot objective value over steps for both algorithms
for (const [obj, color] of [[obj_e, '#e63946'], [obj_m, '#1d6fa5']]) {
if (obj.length < 2) continue;
ctxP.beginPath(); ctxP.strokeStyle = color; ctxP.lineWidth = 1.6;
ctxP.setLineDash([]);
const [x0, y0] = toXY(0, obj[0]); ctxP.moveTo(x0, y0);
for (let i = 1; i < obj.length; i++) {
const [x, y] = toXY(i, obj[i]); ctxP.lineTo(x, y);
}
ctxP.stroke();
}
ctxP.font = '9px system-ui'; ctxP.fillStyle = '#555'; ctxP.textAlign = 'right';
ctxP.fillText(minV.toFixed(2), PAD.l - 2, PAD.t + ph);
ctxP.fillText(maxV.toFixed(2), PAD.l - 2, PAD.t + 6);
}
function draw() { drawSimplex(); drawObjPlot(); }
function step() {
if (stepCount >= MAX_STEPS) return;
const g = getGrad(), eta = getEta();
stepEuclidean(g, eta);
stepMirror(g, eta);
stepCount++;
draw();
const last_e = obj_e[obj_e.length - 1];
const last_m = obj_m[obj_m.length - 1];
setStatus(
(I18N.msg_step||'')
.replace('{n}', stepCount)
.replace('{e}', last_e.toFixed(3))
.replace('{m}', last_m.toFixed(3)),
'ok'
);
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
let I18N = {};
function animate() {
if (stepCount < MAX_STEPS) {
step();
animId = requestAnimationFrame(() => setTimeout(animate, 120));
} else {
running = false;
document.getElementById('btn-run').textContent = I18N.btn_run;
}
}
document.getElementById('eta').addEventListener('input', function() {
document.getElementById('eta-val').textContent = this.value;
reset();
});
document.getElementById('btn-step').addEventListener('click', () => { if (!running) step(); });
document.getElementById('btn-run').addEventListener('click', function() {
if (running) {
running = false;
if (animId) { cancelAnimationFrame(animId); animId = null; }
this.textContent = I18N.btn_run;
} else {
running = true;
this.textContent = I18N.btn_pause;
animate();
}
});
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('dir').addEventListener('change', reset);
// Receive translations from the parent kipu frame
window.addEventListener('message', function(e) {
if (e.data && e.data.type === 'kipu-i18n') {
I18N = e.data.translations;
reset();
}
});
// Fallback if message event never fires
setTimeout(() => { if (!Object.keys(I18N).length) { I18N = window.__KIPU_I18N_FALLBACK__ || {}; reset(); } }, 200);
Comments
Loading comments...