Below are four islands, each holding a small population of bit-strings trying to maximize the number of 1-bits (a simple fitness function). Each island evolves on its own — selection, crossover, mutation — generation by generation. Watch how quickly they diverge, then click Migrate to send each island's best individual to the next island and see the elites spread.
<!-- Island-model GA demo — four independent populations with ring migration -->
<div class="hint-bar">Four island populations evolve independently. Each bar shows one individual's fitness (ones in its bit-string). Click <b>Step</b> to advance one generation, <b>Migrate</b> to send each island's best to its neighbor, or <b>Run</b> for continuous evolution.</div>
<div id="islands-grid"></div>
<div id="status" class="status"></div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-migrate" type="button">Migrate</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Reset and base styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint-bar { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#islands-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: .5rem;
}
/* Island card styles — highlighted when it just received a migrant */
.island {
border: 2px solid #c8d6e3;
border-radius: 8px;
padding: 6px 8px;
background: #f4f8fb;
}
.island.migrated { border-color: #2196a8; background: #e8f7f9; }
.island-title {
font-size: .78rem;
font-weight: 700;
color: #1d3557;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
}
.island-fitness { color: #0a7d33; }
/* Horizontal fitness bar for each individual */
.bars { display: flex; flex-direction: column; gap: 2px; }
.bar-row { display: flex; align-items: center; gap: 4px; }
.bar-bg { flex: 1; height: 8px; background: #dde5ec; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; background: #1d3557; border-radius: 4px; transition: width .2s; }
.bar-fill.best { background: #0a7d33; }
.bar-label { font-size: .65rem; color: #888; width: 24px; text-align: right; }
/* Status bar below the islands */
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; }
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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Island-model GA — several populations evolve in isolation, then periodically
// exchange migrants (ring topology). Each bar represents one individual's fitness.
const N_ISLANDS = 4;
const POP_SIZE = 6;
const GENOME_LEN = 8;
const MUTATION_RATE = 0.05;
// Fitness = number of 1-bits (ones) in the genome (onemax problem)
function fitness(bits) { return bits.reduce((s, b) => s + b, 0); }
function randomGenome() {
return Array.from({ length: GENOME_LEN }, () => Math.random() < 0.5 ? 1 : 0);
}
// Single-point crossover at a random cut point
function crossover(a, b) {
const pt = 1 + Math.floor(Math.random() * (GENOME_LEN - 1));
return [...a.slice(0, pt), ...b.slice(pt)];
}
function mutate(bits) {
return bits.map(b => Math.random() < MUTATION_RATE ? 1 - b : b);
}
// Tournament selection — pick the fitter of two random individuals
function tournamentSelect(pop) {
const a = pop[Math.floor(Math.random() * pop.length)];
const b = pop[Math.floor(Math.random() * pop.length)];
return fitness(a) >= fitness(b) ? a : b;
}
function evolveIsland(pop) {
// Produce next generation with elitism + crossover + mutation
const next = [];
const sorted = [...pop].sort((x, y) => fitness(y) - fitness(x));
next.push(sorted[0]); // keep best individual unchanged (elitism)
while (next.length < POP_SIZE) {
const p1 = tournamentSelect(pop);
const p2 = tournamentSelect(pop);
next.push(mutate(crossover(p1, p2)));
}
return next;
}
// Global state — islands, generation counter, migration tracking
let islands = [];
let generation = 0;
let migratedSet = new Set();
let running = false;
let runTimer = null;
function initIslands() {
islands = Array.from({ length: N_ISLANDS }, () =>
Array.from({ length: POP_SIZE }, () => randomGenome())
);
generation = 0;
migratedSet = new Set();
}
function bestOf(pop) {
return pop.reduce((best, g) => fitness(g) > fitness(best) ? g : best);
}
// Render all four island cards with fitness bars
function renderIslands() {
const grid = document.getElementById('islands-grid');
grid.innerHTML = '';
for (let i = 0; i < N_ISLANDS; i++) {
const pop = islands[i];
const best = bestOf(pop);
const bestFit = fitness(best);
const isMigrated = migratedSet.has(i);
const div = document.createElement('div');
div.className = 'island' + (isMigrated ? ' migrated' : '');
const title = document.createElement('div');
title.className = 'island-title';
title.innerHTML = `<span>Island ${i + 1}</span><span class="island-fitness">${bestFit}/${GENOME_LEN}</span>`;
div.appendChild(title);
const bars = document.createElement('div');
bars.className = 'bars';
const sorted = [...pop].sort((a, b) => fitness(b) - fitness(a));
sorted.forEach((g, rank) => {
const f = fitness(g);
const row = document.createElement('div');
row.className = 'bar-row';
const bg = document.createElement('div');
bg.className = 'bar-bg';
const fill = document.createElement('div');
fill.className = 'bar-fill' + (rank === 0 ? ' best' : '');
fill.style.width = (f / GENOME_LEN * 100) + '%';
bg.appendChild(fill);
const label = document.createElement('div');
label.className = 'bar-label';
label.textContent = f;
row.appendChild(bg);
row.appendChild(label);
bars.appendChild(row);
});
div.appendChild(bars);
grid.appendChild(div);
}
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function avgBestFitness() {
return islands.reduce((s, pop) => s + fitness(bestOf(pop)), 0) / N_ISLANDS;
}
function stepAll() {
islands = islands.map(pop => evolveIsland(pop));
generation++;
migratedSet = new Set();
renderIslands();
const avg = avgBestFitness().toFixed(1);
// Update status line after each generation step
setStatus(i18nFmt('Generation %gen% · avg best fitness: %avg%', { gen: generation, avg }), 'info');
}
// Ring migration — each island's best moves to the next island, replacing its worst
function migrate() {
const elites = islands.map(pop => [...bestOf(pop)]);
for (let i = 0; i < N_ISLANDS; i++) {
const target = (i + 1) % N_ISLANDS;
const sorted = [...islands[target]].sort((a, b) => fitness(a) - fitness(b));
sorted[0] = elites[i]; // replace the weakest individual with the incoming migrant
islands[target] = sorted;
migratedSet.add(target);
}
renderIslands();
setStatus('Elites migrated! Watch the weaker islands catch up.', 'ok');
}
function reset() {
stopRun();
initIslands();
renderIslands();
setStatus('Ready — click Step or Run to start.', '');
}
function startRun() {
if (running) return;
running = true;
document.getElementById('btn-run').textContent = 'Stop';
document.getElementById('btn-step').disabled = true;
runTimer = setInterval(() => {
stepAll();
const allPerfect = islands.every(pop => fitness(bestOf(pop)) === GENOME_LEN);
if (allPerfect) {
stopRun();
setStatus('All islands reached perfect fitness!', 'ok');
}
}, 250);
}
function stopRun() {
running = false;
clearInterval(runTimer);
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
document.getElementById('btn-step').addEventListener('click', stepAll);
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-migrate').addEventListener('click', migrate);
document.getElementById('btn-reset').addEventListener('click', reset);
initIslands();
renderIslands();
setStatus('Ready — click Step or Run to start.', '');
<!-- Island-model GA demo — four independent populations with ring migration -->
<div class="hint-bar">Four island populations evolve independently. Each bar shows one individual's fitness (ones in its bit-string). Click <b>Step</b> to advance one generation, <b>Migrate</b> to send each island's best to its neighbor, or <b>Run</b> for continuous evolution.</div>
<div id="islands-grid"></div>
<div id="status" class="status"></div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-migrate" type="button">Migrate</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Reset and base styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.hint-bar { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#islands-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: .5rem;
}
/* Island card styles — highlighted when it just received a migrant */
.island {
border: 2px solid #c8d6e3;
border-radius: 8px;
padding: 6px 8px;
background: #f4f8fb;
}
.island.migrated { border-color: #2196a8; background: #e8f7f9; }
.island-title {
font-size: .78rem;
font-weight: 700;
color: #1d3557;
margin-bottom: 4px;
display: flex;
justify-content: space-between;
}
.island-fitness { color: #0a7d33; }
/* Horizontal fitness bar for each individual */
.bars { display: flex; flex-direction: column; gap: 2px; }
.bar-row { display: flex; align-items: center; gap: 4px; }
.bar-bg { flex: 1; height: 8px; background: #dde5ec; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; background: #1d3557; border-radius: 4px; transition: width .2s; }
.bar-fill.best { background: #0a7d33; }
.bar-label { font-size: .65rem; color: #888; width: 24px; text-align: right; }
/* Status bar below the islands */
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin: .3rem 0; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; }
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; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Island-model GA — several populations evolve in isolation, then periodically
// exchange migrants (ring topology). Each bar represents one individual's fitness.
const N_ISLANDS = 4;
const POP_SIZE = 6;
const GENOME_LEN = 8;
const MUTATION_RATE = 0.05;
// Fitness = number of 1-bits (ones) in the genome (onemax problem)
function fitness(bits) { return bits.reduce((s, b) => s + b, 0); }
function randomGenome() {
return Array.from({ length: GENOME_LEN }, () => Math.random() < 0.5 ? 1 : 0);
}
// Single-point crossover at a random cut point
function crossover(a, b) {
const pt = 1 + Math.floor(Math.random() * (GENOME_LEN - 1));
return [...a.slice(0, pt), ...b.slice(pt)];
}
function mutate(bits) {
return bits.map(b => Math.random() < MUTATION_RATE ? 1 - b : b);
}
// Tournament selection — pick the fitter of two random individuals
function tournamentSelect(pop) {
const a = pop[Math.floor(Math.random() * pop.length)];
const b = pop[Math.floor(Math.random() * pop.length)];
return fitness(a) >= fitness(b) ? a : b;
}
function evolveIsland(pop) {
// Produce next generation with elitism + crossover + mutation
const next = [];
const sorted = [...pop].sort((x, y) => fitness(y) - fitness(x));
next.push(sorted[0]); // keep best individual unchanged (elitism)
while (next.length < POP_SIZE) {
const p1 = tournamentSelect(pop);
const p2 = tournamentSelect(pop);
next.push(mutate(crossover(p1, p2)));
}
return next;
}
// Global state — islands, generation counter, migration tracking
let islands = [];
let generation = 0;
let migratedSet = new Set();
let running = false;
let runTimer = null;
function initIslands() {
islands = Array.from({ length: N_ISLANDS }, () =>
Array.from({ length: POP_SIZE }, () => randomGenome())
);
generation = 0;
migratedSet = new Set();
}
function bestOf(pop) {
return pop.reduce((best, g) => fitness(g) > fitness(best) ? g : best);
}
// Render all four island cards with fitness bars
function renderIslands() {
const grid = document.getElementById('islands-grid');
grid.innerHTML = '';
for (let i = 0; i < N_ISLANDS; i++) {
const pop = islands[i];
const best = bestOf(pop);
const bestFit = fitness(best);
const isMigrated = migratedSet.has(i);
const div = document.createElement('div');
div.className = 'island' + (isMigrated ? ' migrated' : '');
const title = document.createElement('div');
title.className = 'island-title';
title.innerHTML = `<span>Island ${i + 1}</span><span class="island-fitness">${bestFit}/${GENOME_LEN}</span>`;
div.appendChild(title);
const bars = document.createElement('div');
bars.className = 'bars';
const sorted = [...pop].sort((a, b) => fitness(b) - fitness(a));
sorted.forEach((g, rank) => {
const f = fitness(g);
const row = document.createElement('div');
row.className = 'bar-row';
const bg = document.createElement('div');
bg.className = 'bar-bg';
const fill = document.createElement('div');
fill.className = 'bar-fill' + (rank === 0 ? ' best' : '');
fill.style.width = (f / GENOME_LEN * 100) + '%';
bg.appendChild(fill);
const label = document.createElement('div');
label.className = 'bar-label';
label.textContent = f;
row.appendChild(bg);
row.appendChild(label);
bars.appendChild(row);
});
div.appendChild(bars);
grid.appendChild(div);
}
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + (cls || '');
}
function avgBestFitness() {
return islands.reduce((s, pop) => s + fitness(bestOf(pop)), 0) / N_ISLANDS;
}
function stepAll() {
islands = islands.map(pop => evolveIsland(pop));
generation++;
migratedSet = new Set();
renderIslands();
const avg = avgBestFitness().toFixed(1);
// Update status line after each generation step
setStatus(i18nFmt('Generation %gen% · avg best fitness: %avg%', { gen: generation, avg }), 'info');
}
// Ring migration — each island's best moves to the next island, replacing its worst
function migrate() {
const elites = islands.map(pop => [...bestOf(pop)]);
for (let i = 0; i < N_ISLANDS; i++) {
const target = (i + 1) % N_ISLANDS;
const sorted = [...islands[target]].sort((a, b) => fitness(a) - fitness(b));
sorted[0] = elites[i]; // replace the weakest individual with the incoming migrant
islands[target] = sorted;
migratedSet.add(target);
}
renderIslands();
setStatus('Elites migrated! Watch the weaker islands catch up.', 'ok');
}
function reset() {
stopRun();
initIslands();
renderIslands();
setStatus('Ready — click Step or Run to start.', '');
}
function startRun() {
if (running) return;
running = true;
document.getElementById('btn-run').textContent = 'Stop';
document.getElementById('btn-step').disabled = true;
runTimer = setInterval(() => {
stepAll();
const allPerfect = islands.every(pop => fitness(bestOf(pop)) === GENOME_LEN);
if (allPerfect) {
stopRun();
setStatus('All islands reached perfect fitness!', 'ok');
}
}, 250);
}
function stopRun() {
running = false;
clearInterval(runTimer);
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
document.getElementById('btn-step').addEventListener('click', stepAll);
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-migrate').addEventListener('click', migrate);
document.getElementById('btn-reset').addEventListener('click', reset);
initIslands();
renderIslands();
setStatus('Ready — click Step or Run to start.', '');
Notice the pattern: islands that got lucky early race ahead; islands that fell behind catch up after migration delivers a good solution into their gene pool. That catching-up moment is the island model's core trick — isolated diversity followed by targeted knowledge transfer.
Comments
Loading comments...