<!-- PSO demo container — canvas for swarm visualization, status bar, control buttons -->
<div class="hint" id="hint">20 particles explore a bumpy landscape. White dots = current positions; gold = each particle's personal best; red = swarm's global best. Use <b>Step</b> or <b>Run</b> to watch convergence.</div>
<canvas id="canvas" width="360" height="260"></canvas>
<div class="status" id="status">Scatter the swarm and press Run or Step.</div>
<div class="btns">
<button id="btn-run" type="button">Run</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Compact layout for the PSO simulation widget */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .5rem; line-height: 1.4; }
canvas { display: block; border-radius: 8px; width: 100%; max-width: 360px; }
.status { font-size: .95rem; font-weight: 600; margin: .45rem 0; min-height: 1.4em; color: #1d3557; }
.status.done { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Particle Swarm Optimization core loop.
// Each particle blends inertia, personal best pull, and global best pull to update velocity and position.
const W = 360, H = 260;
const N_PARTICLES = 20;
const MAX_ITER = 80;
// PSO hyperparameters: inertia weight (omega), cognitive coefficient (c1), social coefficient (c2)
const OMEGA = 0.5, C1 = 1.5, C2 = 1.5;
// 2-D landscape with three peaks plus ripple noise — simulates a multimodal objective function
// nx and ny are normalized coordinates in [0,1]; returns quality value (higher is better)
function landscape(nx, ny) {
const x = nx * 6 - 3, y = ny * 6 - 3;
const peak1 = Math.exp(-(x * x + y * y) / 2);
const peak2 = 0.8 * Math.exp(-((x - 2) * (x - 2) + (y - 1) * (y - 1)) / 1.5);
const peak3 = 0.6 * Math.exp(-((x + 2) * (x + 2) + (y + 2) * (y + 2)) / 1.0);
const ripple = 0.05 * Math.cos(x * 4) * Math.cos(y * 4);
return peak1 + peak2 + peak3 + ripple;
}
// Pre-compute landscape grid for canvas background rendering
const grid = [];
const GX = 80, GY = 60;
let globalMax = -Infinity;
for (let gy = 0; gy < GY; gy++) {
for (let gx = 0; gx < GX; gx++) {
const v = landscape(gx / (GX - 1), gy / (GY - 1));
grid.push(v);
if (v > globalMax) globalMax = v;
}
}
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let particles = [], gBest = null, gBestVal = -Infinity, iter = 0, running = false, animId = null;
function randRange(lo, hi) { return lo + Math.random() * (hi - lo); }
function initSwarm() {
// Scatter N_PARTICLES uniformly; seed personal and global bests
particles = [];
gBest = null;
gBestVal = -Infinity;
iter = 0;
for (let i = 0; i < N_PARTICLES; i++) {
const x = Math.random(), y = Math.random();
const val = landscape(x, y);
const p = { x, y, vx: randRange(-0.05, 0.05), vy: randRange(-0.05, 0.05),
px: x, py: y, pVal: val };
if (val > gBestVal) { gBestVal = val; gBest = { x, y }; }
particles.push(p);
}
}
function stepSwarm() {
// One PSO iteration — update velocities and positions for all particles
if (iter >= MAX_ITER) return false;
iter++;
for (const p of particles) {
const r1 = Math.random(), r2 = Math.random();
p.vx = OMEGA * p.vx + C1 * r1 * (p.px - p.x) + C2 * r2 * (gBest.x - p.x);
p.vy = OMEGA * p.vy + C1 * r1 * (p.py - p.y) + C2 * r2 * (gBest.y - p.y);
// Clamp velocity to avoid particles flying out of bounds in one step
const maxV = 0.08;
if (p.vx > maxV) p.vx = maxV; if (p.vx < -maxV) p.vx = -maxV;
if (p.vy > maxV) p.vy = maxV; if (p.vy < -maxV) p.vy = -maxV;
p.x = Math.min(1, Math.max(0, p.x + p.vx));
p.y = Math.min(1, Math.max(0, p.y + p.vy));
const val = landscape(p.x, p.y);
if (val > p.pVal) { p.px = p.x; p.py = p.y; p.pVal = val; }
if (val > gBestVal) { gBestVal = val; gBest = { x: p.x, y: p.y }; }
}
return iter < MAX_ITER;
}
function drawBackground() {
// Render heatmap background from precomputed landscape values
const img = ctx.createImageData(W, H);
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const nx = px / (W - 1), ny = py / (H - 1);
const v = landscape(nx, ny);
const t = Math.max(0, Math.min(1, v / globalMax));
// Map quality value to a blue-green color gradient (dark=low, bright=high)
const r = Math.round(20 + t * 60);
const g = Math.round(40 + t * 130);
const b = Math.round(80 + t * 100);
const idx = (py * W + px) * 4;
img.data[idx] = r; img.data[idx + 1] = g; img.data[idx + 2] = b; img.data[idx + 3] = 255;
}
}
ctx.putImageData(img, 0, 0);
}
function drawParticles() {
// Draw all particles on top of the background heatmap
for (const p of particles) {
const cx = p.x * W, cy = p.y * H;
// Personal best marker (small gold dot)
ctx.beginPath(); ctx.arc(p.px * W, p.py * H, 2.5, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(255,200,50,0.6)'; ctx.fill();
// Current position (white dot with dark border)
ctx.beginPath(); ctx.arc(cx, cy, 4, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.fill();
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 1; ctx.stroke();
}
if (gBest) {
// Global best marker (larger red dot)
const gx = gBest.x * W, gy = gBest.y * H;
ctx.beginPath(); ctx.arc(gx, gy, 7, 0, 2 * Math.PI);
ctx.fillStyle = 'rgba(230,57,70,0.85)'; ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5; ctx.stroke();
}
}
function draw() {
drawBackground();
drawParticles();
}
function updateStatus() {
const el = document.getElementById('status');
if (iter >= MAX_ITER) {
el.textContent = i18nFmt('Done — %iter% iterations. Best quality: %q%%%', { iter: iter, q: (gBestVal / globalMax * 100).toFixed(1) });
el.className = 'status done';
} else {
el.textContent = i18nFmt('Iteration %iter% / %total%', { iter: iter, total: MAX_ITER });
el.className = 'status';
}
}
function reset() {
running = false;
if (animId) { cancelAnimationFrame(animId); animId = null; }
initSwarm();
draw();
document.getElementById('status').textContent = 'Scatter the swarm and press Run or Step.';
document.getElementById('status').className = 'status';
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-run').disabled = false;
document.getElementById('btn-step').disabled = false;
}
function doStep() {
const more = stepSwarm();
draw();
updateStatus();
if (!more) {
running = false;
document.getElementById('btn-run').disabled = true;
document.getElementById('btn-step').disabled = true;
}
}
function runLoop() {
if (!running) return;
doStep();
if (running && iter < MAX_ITER) animId = requestAnimationFrame(runLoop);
else running = false;
}
document.getElementById('btn-run').addEventListener('click', () => {
if (running) {
// Toggle pause — stop the animation loop without resetting the swarm
running = false;
if (animId) { cancelAnimationFrame(animId); animId = null; }
document.getElementById('btn-run').textContent = 'Run';
} else if (iter < MAX_ITER) {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
runLoop();
}
});
document.getElementById('btn-step').addEventListener('click', () => {
if (!running && iter < MAX_ITER) doStep();
});
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Comments
Loading comments...