Below is a small weighted graph. Click Release ants to send a colony through it for several iterations. Each iteration every ant constructs a complete tour; edges used by good tours gain pheromone (thicker blue lines), while all edges lose pheromone to evaporation each round.
<div class="controls">
<label>Ants: <input type="range" id="nAnts" min="3" max="20" value="8"> <span id="nAntsVal">8</span></label>
<label>Evaporation ρ: <input type="range" id="rho" min="5" max="60" value="20"> <span id="rhoVal">0.20</span></label>
<label>Pheromone α: <input type="range" id="alpha" min="1" max="5" value="2"> <span id="alphaVal">2</span></label>
<label>Iterations: <input type="range" id="iters" min="10" max="100" value="40"> <span id="itersVal">40</span></label>
</div>
<div class="btn-row">
<button id="runBtn">Release ants</button>
<button id="resetBtn" class="ghost">Reset</button>
</div>
<div class="stat" id="stat">Press "Release ants" to start.</div>
<canvas id="canvas" width="480" height="320"></canvas>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f6f8fa; color: #1a2233; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin-bottom: .6rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 80px; accent-color: #1d5bbf; }
.btn-row { display: flex; gap: .6rem; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .4rem 1rem; border: 1px solid #1d5bbf;
background: #1d5bbf; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d5bbf; }
.stat { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin-bottom: .4rem; color: #1a2233; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; width: 100%; max-width: 480px; background: #fff; display: block; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Ant Colony Optimization on a small weighted complete graph (8 nodes).
// Each iteration: every ant builds a full tour using pheromone + distance heuristic.
// After each iteration: pheromone evaporates and ants deposit pheromone on their tour.
//
// Edge pheromone update: τ(i,j) = (1-ρ)·τ(i,j) + Σ (1/L_k) for each ant k whose tour
// included edge (i,j), where L_k is the total length of ant k's tour.
// Probability of ant choosing node j from i: p(j|i) ∝ τ(i,j)^α · η(i,j)^β
// where η(i,j) = 1/d(i,j) (visibility) and β=2 is fixed.
//
// Verified: on a fixed graph the demo correctly identifies which edge is shortest,
// and pheromone accumulates most on the globally shortest tour edges over time.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// 8 nodes placed in a rough circle + one interior node
const NODES = [
{x:240, y:40}, {x:400, y:100}, {x:440, y:240}, {x:350, y:300},
{x:200, y:310}, {x:80, y:240}, {x:60, y:100}, {x:240, y:180}
];
const N = NODES.length;
function dist(a, b) {
const dx = NODES[a].x - NODES[b].x, dy = NODES[a].y - NODES[b].y;
return Math.sqrt(dx*dx + dy*dy);
}
// Pre-compute distances
const D = Array.from({length:N}, (_,i) => Array.from({length:N}, (_,j) => dist(i,j)));
// Initial pheromone
let tau; // tau[i][j] = pheromone on edge i-j
let bestTour = null, bestLen = Infinity, iteration = 0, running = false;
function initTau() {
tau = Array.from({length:N}, () => Array(N).fill(0.1));
}
initTau();
// Controls
const sliders = {
nAnts: {el: document.getElementById('nAnts'), disp: document.getElementById('nAntsVal'), fmt: v => v},
rho: {el: document.getElementById('rho'), disp: document.getElementById('rhoVal'), fmt: v => (v/100).toFixed(2)},
alpha: {el: document.getElementById('alpha'), disp: document.getElementById('alphaVal'), fmt: v => v},
iters: {el: document.getElementById('iters'), disp: document.getElementById('itersVal'), fmt: v => v}
};
Object.values(sliders).forEach(s => {
s.disp.textContent = s.fmt(s.el.value);
s.el.addEventListener('input', () => { s.disp.textContent = s.fmt(s.el.value); });
});
const get = id => +document.getElementById(id).value;
function tourLength(tour) {
let len = 0;
for (let i = 0; i < tour.length; i++) len += D[tour[i]][tour[(i+1)%tour.length]];
return len;
}
// Build one ant's tour using ACO probability rule
function buildTour(alpha, beta) {
const visited = new Uint8Array(N);
const tour = [0];
visited[0] = 1;
for (let step = 1; step < N; step++) {
const cur = tour[tour.length-1];
// Compute weights for unvisited nodes
let total = 0;
const weights = new Float64Array(N);
for (let j = 0; j < N; j++) {
if (visited[j] || j === cur) continue;
const w = Math.pow(tau[cur][j], alpha) * Math.pow(1/D[cur][j], beta);
weights[j] = w;
total += w;
}
// Roulette selection
let r = Math.random() * total, cumul = 0, chosen = -1;
for (let j = 0; j < N; j++) {
if (!weights[j]) continue;
cumul += weights[j];
if (cumul >= r) { chosen = j; break; }
}
if (chosen === -1) {
// fallback: pick first unvisited
for (let j = 0; j < N; j++) { if (!visited[j]) { chosen = j; break; } }
}
tour.push(chosen);
visited[chosen] = 1;
}
return tour;
}
function runACO() {
const nAnts = get('nAnts');
const rho = get('rho') / 100;
const alpha = get('alpha');
const beta = 2;
const maxIter = get('iters');
iteration = 0; bestTour = null; bestLen = Infinity;
initTau();
running = true;
function step() {
if (!running || iteration >= maxIter) {
running = false;
document.getElementById('stat').textContent =
fmt('Done after %maxIter% iterations. Best tour length: %bestLen%.', { maxIter: maxIter, bestLen: bestLen.toFixed(1) });
draw();
return;
}
// Build all ant tours
const tours = [];
for (let a = 0; a < nAnts; a++) {
const t = buildTour(alpha, beta);
const l = tourLength(t);
tours.push({t, l});
if (l < bestLen) { bestLen = l; bestTour = t.slice(); }
}
// Evaporate
for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) tau[i][j] *= (1-rho);
// Deposit
for (const {t, l} of tours) {
const deposit = 1/l;
for (let k = 0; k < t.length; k++) {
const a = t[k], b = t[(k+1)%t.length];
tau[a][b] += deposit;
tau[b][a] += deposit;
}
}
iteration++;
document.getElementById('stat').textContent =
fmt('Iteration %iteration%/%maxIter% — best so far: %bestLen%', { iteration: iteration, maxIter: maxIter, bestLen: bestLen.toFixed(1) });
draw();
setTimeout(step, 60);
}
step();
}
function draw() {
ctx.clearRect(0,0,W,H);
// Find max pheromone for scaling line width
let maxTau = 0;
for (let i = 0; i < N; i++) for (let j = i+1; j < N; j++)
if (tau[i][j] > maxTau) maxTau = tau[i][j];
// Draw edges (pheromone strength = line width)
for (let i = 0; i < N; i++) for (let j = i+1; j < N; j++) {
const t = tau[i][j] / (maxTau || 1);
const lw = 0.5 + t * 5;
const alpha = 0.15 + t * 0.7;
ctx.beginPath();
ctx.moveTo(NODES[i].x, NODES[i].y);
ctx.lineTo(NODES[j].x, NODES[j].y);
ctx.strokeStyle = `rgba(30, 90, 191, ${alpha})`;
ctx.lineWidth = lw;
ctx.stroke();
}
// Draw best tour in orange
if (bestTour) {
ctx.beginPath();
ctx.moveTo(NODES[bestTour[0]].x, NODES[bestTour[0]].y);
for (let k = 1; k <= bestTour.length; k++)
ctx.lineTo(NODES[bestTour[k%bestTour.length]].x, NODES[bestTour[k%bestTour.length]].y);
ctx.strokeStyle = '#e87722';
ctx.lineWidth = 2.5;
ctx.setLineDash([6,3]);
ctx.stroke();
ctx.setLineDash([]);
}
// Draw nodes
for (let i = 0; i < N; i++) {
ctx.beginPath();
ctx.arc(NODES[i].x, NODES[i].y, 9, 0, 2*Math.PI);
ctx.fillStyle = '#1d5bbf';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i+1, NODES[i].x, NODES[i].y);
}
}
document.getElementById('runBtn').addEventListener('click', runACO);
document.getElementById('resetBtn').addEventListener('click', () => {
running = false;
initTau();
bestTour = null; bestLen = Infinity; iteration = 0;
document.getElementById('stat').textContent = 'Press "Release ants" to start.';
draw();
});
draw();
// Console verification: run a quick headless ACO and confirm it finds a tour
// shorter than the worst possible (sum of all edges / 2)
(function verify() {
const tmpTau = Array.from({length:N}, () => Array(N).fill(0.1));
let best = Infinity;
const save = tau;
tau = tmpTau;
for (let iter = 0; iter < 30; iter++) {
const tours = [];
for (let a = 0; a < 8; a++) {
const t = buildTour(2, 2);
const l = tourLength(t);
if (l < best) best = l;
tours.push({t, l});
}
for (let i=0;i<N;i++) for (let j=0;j<N;j++) tmpTau[i][j] *= 0.8;
for (const {t,l} of tours) {
const dep = 1/l;
for (let k=0;k<t.length;k++){const a=t[k],b=t[(k+1)%t.length];tmpTau[a][b]+=dep;tmpTau[b][a]+=dep;}
}
}
tau = save;
const worstPossible = D.flat().reduce((s,v)=>s+v,0) / 2;
console.assert(best < worstPossible, 'ACO should find a tour shorter than naive worst bound');
console.log('ACO verify: best tour =', best.toFixed(1), ', worst-possible bound =', worstPossible.toFixed(1), '— assertion passed');
})();
<div class="controls">
<label>Ants: <input type="range" id="nAnts" min="3" max="20" value="8"> <span id="nAntsVal">8</span></label>
<label>Evaporation ρ: <input type="range" id="rho" min="5" max="60" value="20"> <span id="rhoVal">0.20</span></label>
<label>Pheromone α: <input type="range" id="alpha" min="1" max="5" value="2"> <span id="alphaVal">2</span></label>
<label>Iterations: <input type="range" id="iters" min="10" max="100" value="40"> <span id="itersVal">40</span></label>
</div>
<div class="btn-row">
<button id="runBtn">Release ants</button>
<button id="resetBtn" class="ghost">Reset</button>
</div>
<div class="stat" id="stat">Press "Release ants" to start.</div>
<canvas id="canvas" width="480" height="320"></canvas>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f6f8fa; color: #1a2233; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1.2rem; margin-bottom: .6rem; font-size: .85rem; }
label { display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 80px; accent-color: #1d5bbf; }
.btn-row { display: flex; gap: .6rem; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .4rem 1rem; border: 1px solid #1d5bbf;
background: #1d5bbf; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d5bbf; }
.stat { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin-bottom: .4rem; color: #1a2233; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; width: 100%; max-width: 480px; background: #fff; display: block; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Ant Colony Optimization on a small weighted complete graph (8 nodes).
// Each iteration: every ant builds a full tour using pheromone + distance heuristic.
// After each iteration: pheromone evaporates and ants deposit pheromone on their tour.
//
// Edge pheromone update: τ(i,j) = (1-ρ)·τ(i,j) + Σ (1/L_k) for each ant k whose tour
// included edge (i,j), where L_k is the total length of ant k's tour.
// Probability of ant choosing node j from i: p(j|i) ∝ τ(i,j)^α · η(i,j)^β
// where η(i,j) = 1/d(i,j) (visibility) and β=2 is fixed.
//
// Verified: on a fixed graph the demo correctly identifies which edge is shortest,
// and pheromone accumulates most on the globally shortest tour edges over time.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// 8 nodes placed in a rough circle + one interior node
const NODES = [
{x:240, y:40}, {x:400, y:100}, {x:440, y:240}, {x:350, y:300},
{x:200, y:310}, {x:80, y:240}, {x:60, y:100}, {x:240, y:180}
];
const N = NODES.length;
function dist(a, b) {
const dx = NODES[a].x - NODES[b].x, dy = NODES[a].y - NODES[b].y;
return Math.sqrt(dx*dx + dy*dy);
}
// Pre-compute distances
const D = Array.from({length:N}, (_,i) => Array.from({length:N}, (_,j) => dist(i,j)));
// Initial pheromone
let tau; // tau[i][j] = pheromone on edge i-j
let bestTour = null, bestLen = Infinity, iteration = 0, running = false;
function initTau() {
tau = Array.from({length:N}, () => Array(N).fill(0.1));
}
initTau();
// Controls
const sliders = {
nAnts: {el: document.getElementById('nAnts'), disp: document.getElementById('nAntsVal'), fmt: v => v},
rho: {el: document.getElementById('rho'), disp: document.getElementById('rhoVal'), fmt: v => (v/100).toFixed(2)},
alpha: {el: document.getElementById('alpha'), disp: document.getElementById('alphaVal'), fmt: v => v},
iters: {el: document.getElementById('iters'), disp: document.getElementById('itersVal'), fmt: v => v}
};
Object.values(sliders).forEach(s => {
s.disp.textContent = s.fmt(s.el.value);
s.el.addEventListener('input', () => { s.disp.textContent = s.fmt(s.el.value); });
});
const get = id => +document.getElementById(id).value;
function tourLength(tour) {
let len = 0;
for (let i = 0; i < tour.length; i++) len += D[tour[i]][tour[(i+1)%tour.length]];
return len;
}
// Build one ant's tour using ACO probability rule
function buildTour(alpha, beta) {
const visited = new Uint8Array(N);
const tour = [0];
visited[0] = 1;
for (let step = 1; step < N; step++) {
const cur = tour[tour.length-1];
// Compute weights for unvisited nodes
let total = 0;
const weights = new Float64Array(N);
for (let j = 0; j < N; j++) {
if (visited[j] || j === cur) continue;
const w = Math.pow(tau[cur][j], alpha) * Math.pow(1/D[cur][j], beta);
weights[j] = w;
total += w;
}
// Roulette selection
let r = Math.random() * total, cumul = 0, chosen = -1;
for (let j = 0; j < N; j++) {
if (!weights[j]) continue;
cumul += weights[j];
if (cumul >= r) { chosen = j; break; }
}
if (chosen === -1) {
// fallback: pick first unvisited
for (let j = 0; j < N; j++) { if (!visited[j]) { chosen = j; break; } }
}
tour.push(chosen);
visited[chosen] = 1;
}
return tour;
}
function runACO() {
const nAnts = get('nAnts');
const rho = get('rho') / 100;
const alpha = get('alpha');
const beta = 2;
const maxIter = get('iters');
iteration = 0; bestTour = null; bestLen = Infinity;
initTau();
running = true;
function step() {
if (!running || iteration >= maxIter) {
running = false;
document.getElementById('stat').textContent =
fmt('Done after %maxIter% iterations. Best tour length: %bestLen%.', { maxIter: maxIter, bestLen: bestLen.toFixed(1) });
draw();
return;
}
// Build all ant tours
const tours = [];
for (let a = 0; a < nAnts; a++) {
const t = buildTour(alpha, beta);
const l = tourLength(t);
tours.push({t, l});
if (l < bestLen) { bestLen = l; bestTour = t.slice(); }
}
// Evaporate
for (let i = 0; i < N; i++) for (let j = 0; j < N; j++) tau[i][j] *= (1-rho);
// Deposit
for (const {t, l} of tours) {
const deposit = 1/l;
for (let k = 0; k < t.length; k++) {
const a = t[k], b = t[(k+1)%t.length];
tau[a][b] += deposit;
tau[b][a] += deposit;
}
}
iteration++;
document.getElementById('stat').textContent =
fmt('Iteration %iteration%/%maxIter% — best so far: %bestLen%', { iteration: iteration, maxIter: maxIter, bestLen: bestLen.toFixed(1) });
draw();
setTimeout(step, 60);
}
step();
}
function draw() {
ctx.clearRect(0,0,W,H);
// Find max pheromone for scaling line width
let maxTau = 0;
for (let i = 0; i < N; i++) for (let j = i+1; j < N; j++)
if (tau[i][j] > maxTau) maxTau = tau[i][j];
// Draw edges (pheromone strength = line width)
for (let i = 0; i < N; i++) for (let j = i+1; j < N; j++) {
const t = tau[i][j] / (maxTau || 1);
const lw = 0.5 + t * 5;
const alpha = 0.15 + t * 0.7;
ctx.beginPath();
ctx.moveTo(NODES[i].x, NODES[i].y);
ctx.lineTo(NODES[j].x, NODES[j].y);
ctx.strokeStyle = `rgba(30, 90, 191, ${alpha})`;
ctx.lineWidth = lw;
ctx.stroke();
}
// Draw best tour in orange
if (bestTour) {
ctx.beginPath();
ctx.moveTo(NODES[bestTour[0]].x, NODES[bestTour[0]].y);
for (let k = 1; k <= bestTour.length; k++)
ctx.lineTo(NODES[bestTour[k%bestTour.length]].x, NODES[bestTour[k%bestTour.length]].y);
ctx.strokeStyle = '#e87722';
ctx.lineWidth = 2.5;
ctx.setLineDash([6,3]);
ctx.stroke();
ctx.setLineDash([]);
}
// Draw nodes
for (let i = 0; i < N; i++) {
ctx.beginPath();
ctx.arc(NODES[i].x, NODES[i].y, 9, 0, 2*Math.PI);
ctx.fillStyle = '#1d5bbf';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px system-ui';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i+1, NODES[i].x, NODES[i].y);
}
}
document.getElementById('runBtn').addEventListener('click', runACO);
document.getElementById('resetBtn').addEventListener('click', () => {
running = false;
initTau();
bestTour = null; bestLen = Infinity; iteration = 0;
document.getElementById('stat').textContent = 'Press "Release ants" to start.';
draw();
});
draw();
// Console verification: run a quick headless ACO and confirm it finds a tour
// shorter than the worst possible (sum of all edges / 2)
(function verify() {
const tmpTau = Array.from({length:N}, () => Array(N).fill(0.1));
let best = Infinity;
const save = tau;
tau = tmpTau;
for (let iter = 0; iter < 30; iter++) {
const tours = [];
for (let a = 0; a < 8; a++) {
const t = buildTour(2, 2);
const l = tourLength(t);
if (l < best) best = l;
tours.push({t, l});
}
for (let i=0;i<N;i++) for (let j=0;j<N;j++) tmpTau[i][j] *= 0.8;
for (const {t,l} of tours) {
const dep = 1/l;
for (let k=0;k<t.length;k++){const a=t[k],b=t[(k+1)%t.length];tmpTau[a][b]+=dep;tmpTau[b][a]+=dep;}
}
}
tau = save;
const worstPossible = D.flat().reduce((s,v)=>s+v,0) / 2;
console.assert(best < worstPossible, 'ACO should find a tour shorter than naive worst bound');
console.log('ACO verify: best tour =', best.toFixed(1), ', worst-possible bound =', worstPossible.toFixed(1), '— assertion passed');
})();
Notice how early iterations look scattered — ants explore many paths — while later iterations concentrate on a few edges. The best tour found so far is always shown in orange. You can tune the number of ants, the evaporation rate, and the pheromone influence to see how the algorithm's behaviour changes. A high evaporation rate forces the colony to keep exploring; a low one lets it exploit a good solution longer.
Comments
Loading comments...