The landscape below is a simple 1-D function with a clear global minimum. Each dot is a candidate solution. Press Run to watch the DE loop: mutation, crossover, and selection — generation after generation — until the population collapses onto the optimum.
<!-- DE demo: controls + canvas + buttons -->
<div class="controls">
<label>F <input id="slF" type="range" min="0.1" max="1.5" step="0.05" value="0.8"> <span id="valF">0.8</span></label>
<label>CR <input id="slCR" type="range" min="0.0" max="1.0" step="0.05" value="0.9"> <span id="valCR">0.9</span></label>
</div>
<canvas id="cv" width="560" height="220" aria-label="Differential evolution population converging on the objective function"></canvas>
<div class="info">
<span id="gen">Generation: 0</span>
<span id="best">Best: —</span>
</div>
<div class="btns">
<button id="btnRun" type="button">Run</button>
<button id="btnStep" type="button">Step</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<p class="hint">Red dots are candidates; the green dot marks the current best. <b>F</b> scales the mutation step; <b>CR</b> controls crossover. Each generation: pick 3 random peers, compute a + F·(b−c), mix with crossover, keep the winner.</p>
/* Layout: controls on top, canvas, info row, buttons, hint */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: transparent; }
.controls { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
canvas { display: block; width: 100%; max-width: 560px; border-radius: 8px;
border: 1px solid #cdd9e3; background: #f7f9fb; }
.info { display: flex; gap: 1.5rem; font-size: .9rem; margin: .4rem 0; color: #444; }
.hint { font-size: .85rem; color: #555; margin: .5rem 0 0; line-height: 1.5; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Differential Evolution — 1-D demo */
/* Objective: multimodal 1-D function with a clear global minimum. */
/* Combines slow quadratic bowl + fast sinusoids for a rugged landscape. */
function objective(x) {
return 0.3 * Math.sin(2.5 * x) + 0.5 * Math.sin(0.8 * x) +
0.15 * Math.cos(4 * x) + 0.05 * (x * x) / 25;
}
const X_MIN = -6, X_MAX = 6;
const POP_SIZE = 14;
/* DE parameters: F=scale factor, CR=crossover rate */
let F = 0.8, CR = 0.9;
let pop = [], gen = 0, running = false, raf = null;
let bestX = null, bestY = Infinity;
/* Canvas geometry */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const PAD = { top: 16, bottom: 40, left: 10, right: 10 };
/* Pre-compute the curve for drawing */
const CURVE_PTS = 400;
const curveX = [], curveY = [];
let yMin = Infinity, yMax = -Infinity;
for (let i = 0; i < CURVE_PTS; i++) {
const x = X_MIN + (X_MAX - X_MIN) * i / (CURVE_PTS - 1);
const y = objective(x);
curveX.push(x); curveY.push(y);
if (y < yMin) yMin = y;
if (y > yMax) yMax = y;
}
/* Add vertical margin so dots don't touch the border */
const ySpan = yMax - yMin;
yMin -= 0.08 * ySpan; yMax += 0.08 * ySpan;
function toCanvas(x, y) {
const px = PAD.left + (x - X_MIN) / (X_MAX - X_MIN) * (W - PAD.left - PAD.right);
const py = PAD.top + (1 - (y - yMin) / (yMax - yMin)) * (H - PAD.top - PAD.bottom);
return [px, py];
}
function initPop() {
/* Random uniform initialisation over [X_MIN, X_MAX] */
pop = Array.from({ length: POP_SIZE }, () => {
const x = X_MIN + Math.random() * (X_MAX - X_MIN);
return { x, y: objective(x) };
});
gen = 0; bestX = null; bestY = Infinity;
for (const p of pop) { if (p.y < bestY) { bestY = p.y; bestX = p.x; } }
updateInfo();
}
function deStep() {
/* One DE generation: for each individual, mutate + crossover + select */
const newPop = [];
for (let i = 0; i < POP_SIZE; i++) {
/* Pick three distinct peers a, b, c different from i */
let a, b, c;
do { a = Math.floor(Math.random() * POP_SIZE); } while (a === i);
do { b = Math.floor(Math.random() * POP_SIZE); } while (b === i || b === a);
do { c = Math.floor(Math.random() * POP_SIZE); } while (c === i || c === a || c === b);
/* Mutation: donor + F * (b - c) */
let mutant = pop[a].x + F * (pop[b].x - pop[c].x);
/* Clamp mutant to the valid search range */
mutant = Math.max(X_MIN, Math.min(X_MAX, mutant));
/* Crossover: accept mutant coordinate with probability CR */
const trial = Math.random() < CR ? mutant : pop[i].x;
const trialY = objective(trial);
/* Selection: keep trial only if it is at least as good */
if (trialY <= pop[i].y) {
newPop.push({ x: trial, y: trialY });
} else {
newPop.push(pop[i]);
}
}
pop = newPop;
gen++;
bestY = Infinity;
for (const p of pop) { if (p.y < bestY) { bestY = p.y; bestX = p.x; } }
updateInfo();
}
function updateInfo() {
document.getElementById('gen').textContent = 'Generation: ' + gen;
document.getElementById('best').textContent =
bestX !== null
? i18nFmt('Best: x=%x% f=%y%', { x: bestX.toFixed(3), y: bestY.toFixed(4) })
: 'Best: —';
}
function draw() {
ctx.clearRect(0, 0, W, H);
/* Draw the objective function curve */
ctx.beginPath();
for (let i = 0; i < CURVE_PTS; i++) {
const [px, py] = toCanvas(curveX[i], curveY[i]);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.strokeStyle = '#aac4de';
ctx.lineWidth = 2;
ctx.stroke();
/* Draw the x-axis reference line */
const [, axisY] = toCanvas(0, yMin);
ctx.beginPath();
ctx.moveTo(PAD.left, axisY);
ctx.lineTo(W - PAD.right, axisY);
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 1;
ctx.stroke();
/* Draw all population members as red dots */
for (const p of pop) {
const [px, py] = toCanvas(p.x, p.y);
ctx.beginPath();
ctx.arc(px, py, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
/* Highlight the current best in green */
if (bestX !== null) {
const [bx, by] = toCanvas(bestX, bestY);
ctx.beginPath();
ctx.arc(bx, by, 7.5, 0, 2 * Math.PI);
ctx.fillStyle = '#0a7d33';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
/* Draw x-axis tick labels */
ctx.fillStyle = '#666';
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'center';
for (let x = -6; x <= 6; x += 2) {
const [px, py] = toCanvas(x, yMin);
ctx.fillText(x, px, py + 14);
}
}
function loop() {
if (!running) return;
deStep(); draw();
if (gen >= 200) { running = false; setButtons(); return; }
raf = requestAnimationFrame(loop);
}
function setButtons() {
document.getElementById('btnRun').disabled = running;
document.getElementById('btnStep').disabled = running || gen >= 200;
}
document.getElementById('btnRun').addEventListener('click', () => {
running = true; setButtons(); loop();
});
document.getElementById('btnStep').addEventListener('click', () => {
deStep(); draw(); setButtons();
});
document.getElementById('btnReset').addEventListener('click', () => {
running = false; cancelAnimationFrame(raf); initPop(); draw(); setButtons();
});
document.getElementById('slF').addEventListener('input', function () {
F = +this.value; document.getElementById('valF').textContent = F.toFixed(2);
});
document.getElementById('slCR').addEventListener('input', function () {
CR = +this.value; document.getElementById('valCR').textContent = CR.toFixed(2);
});
initPop(); draw(); setButtons();
<!-- DE demo: controls + canvas + buttons -->
<div class="controls">
<label>F <input id="slF" type="range" min="0.1" max="1.5" step="0.05" value="0.8"> <span id="valF">0.8</span></label>
<label>CR <input id="slCR" type="range" min="0.0" max="1.0" step="0.05" value="0.9"> <span id="valCR">0.9</span></label>
</div>
<canvas id="cv" width="560" height="220" aria-label="Differential evolution population converging on the objective function"></canvas>
<div class="info">
<span id="gen">Generation: 0</span>
<span id="best">Best: —</span>
</div>
<div class="btns">
<button id="btnRun" type="button">Run</button>
<button id="btnStep" type="button">Step</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<p class="hint">Red dots are candidates; the green dot marks the current best. <b>F</b> scales the mutation step; <b>CR</b> controls crossover. Each generation: pick 3 random peers, compute a + F·(b−c), mix with crossover, keep the winner.</p>
/* Layout: controls on top, canvas, info row, buttons, hint */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: transparent; }
.controls { display: flex; gap: 1.2rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .4rem; }
canvas { display: block; width: 100%; max-width: 560px; border-radius: 8px;
border: 1px solid #cdd9e3; background: #f7f9fb; }
.info { display: flex; gap: 1.5rem; font-size: .9rem; margin: .4rem 0; color: #444; }
.hint { font-size: .85rem; color: #555; margin: .5rem 0 0; line-height: 1.5; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Differential Evolution — 1-D demo */
/* Objective: multimodal 1-D function with a clear global minimum. */
/* Combines slow quadratic bowl + fast sinusoids for a rugged landscape. */
function objective(x) {
return 0.3 * Math.sin(2.5 * x) + 0.5 * Math.sin(0.8 * x) +
0.15 * Math.cos(4 * x) + 0.05 * (x * x) / 25;
}
const X_MIN = -6, X_MAX = 6;
const POP_SIZE = 14;
/* DE parameters: F=scale factor, CR=crossover rate */
let F = 0.8, CR = 0.9;
let pop = [], gen = 0, running = false, raf = null;
let bestX = null, bestY = Infinity;
/* Canvas geometry */
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const W = cv.width, H = cv.height;
const PAD = { top: 16, bottom: 40, left: 10, right: 10 };
/* Pre-compute the curve for drawing */
const CURVE_PTS = 400;
const curveX = [], curveY = [];
let yMin = Infinity, yMax = -Infinity;
for (let i = 0; i < CURVE_PTS; i++) {
const x = X_MIN + (X_MAX - X_MIN) * i / (CURVE_PTS - 1);
const y = objective(x);
curveX.push(x); curveY.push(y);
if (y < yMin) yMin = y;
if (y > yMax) yMax = y;
}
/* Add vertical margin so dots don't touch the border */
const ySpan = yMax - yMin;
yMin -= 0.08 * ySpan; yMax += 0.08 * ySpan;
function toCanvas(x, y) {
const px = PAD.left + (x - X_MIN) / (X_MAX - X_MIN) * (W - PAD.left - PAD.right);
const py = PAD.top + (1 - (y - yMin) / (yMax - yMin)) * (H - PAD.top - PAD.bottom);
return [px, py];
}
function initPop() {
/* Random uniform initialisation over [X_MIN, X_MAX] */
pop = Array.from({ length: POP_SIZE }, () => {
const x = X_MIN + Math.random() * (X_MAX - X_MIN);
return { x, y: objective(x) };
});
gen = 0; bestX = null; bestY = Infinity;
for (const p of pop) { if (p.y < bestY) { bestY = p.y; bestX = p.x; } }
updateInfo();
}
function deStep() {
/* One DE generation: for each individual, mutate + crossover + select */
const newPop = [];
for (let i = 0; i < POP_SIZE; i++) {
/* Pick three distinct peers a, b, c different from i */
let a, b, c;
do { a = Math.floor(Math.random() * POP_SIZE); } while (a === i);
do { b = Math.floor(Math.random() * POP_SIZE); } while (b === i || b === a);
do { c = Math.floor(Math.random() * POP_SIZE); } while (c === i || c === a || c === b);
/* Mutation: donor + F * (b - c) */
let mutant = pop[a].x + F * (pop[b].x - pop[c].x);
/* Clamp mutant to the valid search range */
mutant = Math.max(X_MIN, Math.min(X_MAX, mutant));
/* Crossover: accept mutant coordinate with probability CR */
const trial = Math.random() < CR ? mutant : pop[i].x;
const trialY = objective(trial);
/* Selection: keep trial only if it is at least as good */
if (trialY <= pop[i].y) {
newPop.push({ x: trial, y: trialY });
} else {
newPop.push(pop[i]);
}
}
pop = newPop;
gen++;
bestY = Infinity;
for (const p of pop) { if (p.y < bestY) { bestY = p.y; bestX = p.x; } }
updateInfo();
}
function updateInfo() {
document.getElementById('gen').textContent = 'Generation: ' + gen;
document.getElementById('best').textContent =
bestX !== null
? i18nFmt('Best: x=%x% f=%y%', { x: bestX.toFixed(3), y: bestY.toFixed(4) })
: 'Best: —';
}
function draw() {
ctx.clearRect(0, 0, W, H);
/* Draw the objective function curve */
ctx.beginPath();
for (let i = 0; i < CURVE_PTS; i++) {
const [px, py] = toCanvas(curveX[i], curveY[i]);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.strokeStyle = '#aac4de';
ctx.lineWidth = 2;
ctx.stroke();
/* Draw the x-axis reference line */
const [, axisY] = toCanvas(0, yMin);
ctx.beginPath();
ctx.moveTo(PAD.left, axisY);
ctx.lineTo(W - PAD.right, axisY);
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 1;
ctx.stroke();
/* Draw all population members as red dots */
for (const p of pop) {
const [px, py] = toCanvas(p.x, p.y);
ctx.beginPath();
ctx.arc(px, py, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
}
/* Highlight the current best in green */
if (bestX !== null) {
const [bx, by] = toCanvas(bestX, bestY);
ctx.beginPath();
ctx.arc(bx, by, 7.5, 0, 2 * Math.PI);
ctx.fillStyle = '#0a7d33';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
/* Draw x-axis tick labels */
ctx.fillStyle = '#666';
ctx.font = '11px system-ui, sans-serif';
ctx.textAlign = 'center';
for (let x = -6; x <= 6; x += 2) {
const [px, py] = toCanvas(x, yMin);
ctx.fillText(x, px, py + 14);
}
}
function loop() {
if (!running) return;
deStep(); draw();
if (gen >= 200) { running = false; setButtons(); return; }
raf = requestAnimationFrame(loop);
}
function setButtons() {
document.getElementById('btnRun').disabled = running;
document.getElementById('btnStep').disabled = running || gen >= 200;
}
document.getElementById('btnRun').addEventListener('click', () => {
running = true; setButtons(); loop();
});
document.getElementById('btnStep').addEventListener('click', () => {
deStep(); draw(); setButtons();
});
document.getElementById('btnReset').addEventListener('click', () => {
running = false; cancelAnimationFrame(raf); initPop(); draw(); setButtons();
});
document.getElementById('slF').addEventListener('input', function () {
F = +this.value; document.getElementById('valF').textContent = F.toFixed(2);
});
document.getElementById('slCR').addEventListener('input', function () {
CR = +this.value; document.getElementById('valCR').textContent = CR.toFixed(2);
});
initPop(); draw(); setButtons();
Notice how the spread of dots shrinks over time. Early generations explore widely; later ones exploit the neighborhood of the best region. The scaling factor F controls the step size, and crossover rate CR controls how aggressively each coordinate is replaced. Tune them and observe the trade-off between exploration and exploitation.
Comments
Loading comments...