The demo below runs Harmony Search on a simple one-dimensional function with several local minima — exactly the kind of landscape that trips up hill-climbing methods. The horizontal axis shows the search space; the dots in the Harmony Memory panel are the current best candidates.
<p class="hint">Harmony Search keeps a small <b>memory</b> of good solutions and improves by picking from memory, nudging a value slightly (pitch adjustment), or jumping to a random point. Watch the red dots cluster near the minimum.</p>
<div class="panels">
<div class="chart-panel">
<div class="panel-label">Function f(x)</div>
<canvas id="chart" width="320" height="160"></canvas>
</div>
<div class="mem-panel">
<div class="panel-label">Harmony Memory (<span id="hms-val">5</span>)</div>
<canvas id="mem" width="140" height="160"></canvas>
</div>
</div>
<div class="controls">
<label>Memory rate (p_HMC): <input type="range" id="hmc" min="0" max="100" value="80"> <span id="hmc-val">0.80</span></label>
<label>Pitch adjust (p_PA): <input type="range" id="pa" min="0" max="100" value="30"> <span id="pa-val">0.30</span></label>
</div>
<div class="status" id="status">Press Step or Run to start.</div>
<div class="btns">
<button id="step-btn" type="button">Step</button>
<button id="run-btn" type="button">Run</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panels { display: flex; gap: 8px; margin-bottom: .5rem; }
.chart-panel { flex: 1; }
.mem-panel { width: 148px; flex-shrink: 0; }
.panel-label { font-size: .75rem; font-weight: 600; color: #555; margin-bottom: 2px; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 6px; background: #f5f8fa; }
.controls { display: flex; flex-direction: column; gap: .25rem; margin-bottom: .4rem; }
.controls label { font-size: .82rem; color: #333; display: flex; align-items: center; gap: .4rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
.controls span { font-weight: 700; width: 2.8rem; text-align: right; }
.status { font-size: .95rem; font-weight: 600; margin: .35rem 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.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: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Objective function with multiple local minima — the kind HS handles well
// f(x) = sin(x) + 0.5*sin(3x) + 0.2*sin(7x) — multiple local minima in [0,10]
const X_MIN = 0, X_MAX = 10;
const f = x => Math.sin(x) + 0.5 * Math.sin(3 * x) + 0.2 * Math.sin(7 * x);
// harmony memory size: fixed at 5 for this demo
const HMS = 5;
let memory = [];
let iteration = 0;
let running = false;
let runTimer = null;
let lastCandidate = null;
let lastAction = '';
// Sync slider display
const hmcSlider = document.getElementById('hmc');
const paSlider = document.getElementById('pa');
const hmcVal = document.getElementById('hmc-val');
const paVal = document.getElementById('pa-val');
hmcSlider.addEventListener('input', () => { hmcVal.textContent = (hmcSlider.value / 100).toFixed(2); });
paSlider.addEventListener('input', () => { paVal.textContent = (paSlider.value / 100).toFixed(2); });
const BW = 0.4; // pitch adjustment bandwidth
function pHMC() { return hmcSlider.value / 100; }
function pPA() { return paSlider.value / 100; }
// Initialize memory with HMS random solutions
function initMemory() {
memory = [];
for (let i = 0; i < HMS; i++) {
const x = X_MIN + Math.random() * (X_MAX - X_MIN);
memory.push({ x, score: f(x) });
}
}
// Find index of worst (highest-scoring) solution in memory
function worstIndex() {
let wi = 0;
for (let i = 1; i < memory.length; i++) if (memory[i].score > memory[wi].score) wi = i;
return wi;
}
// Find best (lowest-scoring) solution in memory
function bestEntry() {
return memory.reduce((b, m) => m.score < b.score ? m : b, memory[0]);
}
// Generate one new candidate via the three HS rules
function improvise() {
let x, action;
if (Math.random() < pHMC()) {
// Pick a value from a random memory slot
const src = memory[Math.floor(Math.random() * memory.length)];
x = src.x;
if (Math.random() < pPA()) {
// Pitch adjust: perturb within bandwidth, clamp to domain
x = x + (Math.random() * 2 - 1) * BW;
x = Math.max(X_MIN, Math.min(X_MAX, x));
action = 'pitch';
} else {
action = 'memory';
}
} else {
// Random new value (exploration)
x = X_MIN + Math.random() * (X_MAX - X_MIN);
action = 'random';
}
return { x, score: f(x), action };
}
// One iteration: improvise, compare, replace worst if better
function step() {
const candidate = improvise();
lastCandidate = candidate;
lastAction = candidate.action;
const wi = worstIndex();
if (candidate.score < memory[wi].score) {
memory[wi] = { x: candidate.x, score: candidate.score };
}
iteration++;
const best = bestEntry();
let actionLabel;
if (candidate.action === 'pitch') actionLabel = 'pitch-adjusted';
else if (candidate.action === 'memory') actionLabel = 'from memory';
else actionLabel = 'random';
const msg = i18nFmt('Iter %n% — %a% — best f = %best%', { n: iteration, a: actionLabel, best: best.score.toFixed(4) });
setStatus(msg, 'info');
draw();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
// Rendering helpers
function draw() {
drawChart();
drawMem();
}
function drawChart() {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw objective function curve
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
for (let px = 0; px < W; px++) {
const x = X_MIN + (px / W) * (X_MAX - X_MIN);
const y = f(x);
const py = H / 2 - (y / 2) * (H * 0.38);
if (px === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
// Overlay current memory solutions as red dots
for (const m of memory) {
const px = ((m.x - X_MIN) / (X_MAX - X_MIN)) * W;
const py = H / 2 - (m.score / 2) * (H * 0.38);
ctx.beginPath();
ctx.arc(px, py, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
}
// Highlight last candidate with an orange ring
if (lastCandidate) {
const px = ((lastCandidate.x - X_MIN) / (X_MAX - X_MIN)) * W;
const py = H / 2 - (lastCandidate.score / 2) * (H * 0.38);
ctx.beginPath();
ctx.arc(px, py, 5.5, 0, 2 * Math.PI);
ctx.strokeStyle = '#f4a261';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function drawMem() {
const canvas = document.getElementById('mem');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const rowH = H / HMS;
// Render harmony memory as color-coded rows (blue=low, red=high)
const sorted = [...memory].sort((a, b) => a.score - b.score);
sorted.forEach((m, i) => {
const y = i * rowH;
const heat = (m.score - (-1.7)) / (1.7 - (-1.7));
const r = Math.round(30 + heat * 200);
const g = Math.round(120 - heat * 80);
const b2 = Math.round(200 - heat * 170);
ctx.fillStyle = `rgb(${r},${g},${b2})`;
ctx.fillRect(2, y + 2, W - 4, rowH - 4);
ctx.fillStyle = '#fff';
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(m.x.toFixed(3), 6, y + rowH / 2 + 4);
ctx.fillText(m.score.toFixed(3), W / 2, y + rowH / 2 + 4);
});
}
// Button event listeners
document.getElementById('step-btn').addEventListener('click', () => { if (!running) step(); });
document.getElementById('run-btn').addEventListener('click', () => {
if (running) {
running = false;
clearInterval(runTimer);
document.getElementById('run-btn').textContent = 'Run';
document.getElementById('step-btn').disabled = false;
} else {
running = true;
document.getElementById('run-btn').textContent = 'Pause';
document.getElementById('step-btn').disabled = true;
runTimer = setInterval(() => { if (iteration < 500) step(); else { running = false; clearInterval(runTimer); document.getElementById('run-btn').textContent = 'Run'; document.getElementById('step-btn').disabled = false; } }, 80);
}
});
document.getElementById('reset-btn').addEventListener('click', () => {
running = false;
clearInterval(runTimer);
document.getElementById('run-btn').textContent = 'Run';
document.getElementById('step-btn').disabled = false;
iteration = 0;
lastCandidate = null;
lastAction = '';
initMemory();
setStatus('Press Step or Run to start.', '');
draw();
});
// Bootstrap: seed memory and draw initial state
initMemory();
draw();
<p class="hint">Harmony Search keeps a small <b>memory</b> of good solutions and improves by picking from memory, nudging a value slightly (pitch adjustment), or jumping to a random point. Watch the red dots cluster near the minimum.</p>
<div class="panels">
<div class="chart-panel">
<div class="panel-label">Function f(x)</div>
<canvas id="chart" width="320" height="160"></canvas>
</div>
<div class="mem-panel">
<div class="panel-label">Harmony Memory (<span id="hms-val">5</span>)</div>
<canvas id="mem" width="140" height="160"></canvas>
</div>
</div>
<div class="controls">
<label>Memory rate (p_HMC): <input type="range" id="hmc" min="0" max="100" value="80"> <span id="hmc-val">0.80</span></label>
<label>Pitch adjust (p_PA): <input type="range" id="pa" min="0" max="100" value="30"> <span id="pa-val">0.30</span></label>
</div>
<div class="status" id="status">Press Step or Run to start.</div>
<div class="btns">
<button id="step-btn" type="button">Step</button>
<button id="run-btn" type="button">Run</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.4; }
.panels { display: flex; gap: 8px; margin-bottom: .5rem; }
.chart-panel { flex: 1; }
.mem-panel { width: 148px; flex-shrink: 0; }
.panel-label { font-size: .75rem; font-weight: 600; color: #555; margin-bottom: 2px; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 6px; background: #f5f8fa; }
.controls { display: flex; flex-direction: column; gap: .25rem; margin-bottom: .4rem; }
.controls label { font-size: .82rem; color: #333; display: flex; align-items: center; gap: .4rem; }
input[type=range] { flex: 1; accent-color: #1d3557; }
.controls span { font-weight: 700; width: 2.8rem; text-align: right; }
.status { font-size: .95rem; font-weight: 600; margin: .35rem 0; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.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: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Objective function with multiple local minima — the kind HS handles well
// f(x) = sin(x) + 0.5*sin(3x) + 0.2*sin(7x) — multiple local minima in [0,10]
const X_MIN = 0, X_MAX = 10;
const f = x => Math.sin(x) + 0.5 * Math.sin(3 * x) + 0.2 * Math.sin(7 * x);
// harmony memory size: fixed at 5 for this demo
const HMS = 5;
let memory = [];
let iteration = 0;
let running = false;
let runTimer = null;
let lastCandidate = null;
let lastAction = '';
// Sync slider display
const hmcSlider = document.getElementById('hmc');
const paSlider = document.getElementById('pa');
const hmcVal = document.getElementById('hmc-val');
const paVal = document.getElementById('pa-val');
hmcSlider.addEventListener('input', () => { hmcVal.textContent = (hmcSlider.value / 100).toFixed(2); });
paSlider.addEventListener('input', () => { paVal.textContent = (paSlider.value / 100).toFixed(2); });
const BW = 0.4; // pitch adjustment bandwidth
function pHMC() { return hmcSlider.value / 100; }
function pPA() { return paSlider.value / 100; }
// Initialize memory with HMS random solutions
function initMemory() {
memory = [];
for (let i = 0; i < HMS; i++) {
const x = X_MIN + Math.random() * (X_MAX - X_MIN);
memory.push({ x, score: f(x) });
}
}
// Find index of worst (highest-scoring) solution in memory
function worstIndex() {
let wi = 0;
for (let i = 1; i < memory.length; i++) if (memory[i].score > memory[wi].score) wi = i;
return wi;
}
// Find best (lowest-scoring) solution in memory
function bestEntry() {
return memory.reduce((b, m) => m.score < b.score ? m : b, memory[0]);
}
// Generate one new candidate via the three HS rules
function improvise() {
let x, action;
if (Math.random() < pHMC()) {
// Pick a value from a random memory slot
const src = memory[Math.floor(Math.random() * memory.length)];
x = src.x;
if (Math.random() < pPA()) {
// Pitch adjust: perturb within bandwidth, clamp to domain
x = x + (Math.random() * 2 - 1) * BW;
x = Math.max(X_MIN, Math.min(X_MAX, x));
action = 'pitch';
} else {
action = 'memory';
}
} else {
// Random new value (exploration)
x = X_MIN + Math.random() * (X_MAX - X_MIN);
action = 'random';
}
return { x, score: f(x), action };
}
// One iteration: improvise, compare, replace worst if better
function step() {
const candidate = improvise();
lastCandidate = candidate;
lastAction = candidate.action;
const wi = worstIndex();
if (candidate.score < memory[wi].score) {
memory[wi] = { x: candidate.x, score: candidate.score };
}
iteration++;
const best = bestEntry();
let actionLabel;
if (candidate.action === 'pitch') actionLabel = 'pitch-adjusted';
else if (candidate.action === 'memory') actionLabel = 'from memory';
else actionLabel = 'random';
const msg = i18nFmt('Iter %n% — %a% — best f = %best%', { n: iteration, a: actionLabel, best: best.score.toFixed(4) });
setStatus(msg, 'info');
draw();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
// Rendering helpers
function draw() {
drawChart();
drawMem();
}
function drawChart() {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw objective function curve
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
for (let px = 0; px < W; px++) {
const x = X_MIN + (px / W) * (X_MAX - X_MIN);
const y = f(x);
const py = H / 2 - (y / 2) * (H * 0.38);
if (px === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
// Overlay current memory solutions as red dots
for (const m of memory) {
const px = ((m.x - X_MIN) / (X_MAX - X_MIN)) * W;
const py = H / 2 - (m.score / 2) * (H * 0.38);
ctx.beginPath();
ctx.arc(px, py, 4, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946';
ctx.fill();
}
// Highlight last candidate with an orange ring
if (lastCandidate) {
const px = ((lastCandidate.x - X_MIN) / (X_MAX - X_MIN)) * W;
const py = H / 2 - (lastCandidate.score / 2) * (H * 0.38);
ctx.beginPath();
ctx.arc(px, py, 5.5, 0, 2 * Math.PI);
ctx.strokeStyle = '#f4a261';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function drawMem() {
const canvas = document.getElementById('mem');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const rowH = H / HMS;
// Render harmony memory as color-coded rows (blue=low, red=high)
const sorted = [...memory].sort((a, b) => a.score - b.score);
sorted.forEach((m, i) => {
const y = i * rowH;
const heat = (m.score - (-1.7)) / (1.7 - (-1.7));
const r = Math.round(30 + heat * 200);
const g = Math.round(120 - heat * 80);
const b2 = Math.round(200 - heat * 170);
ctx.fillStyle = `rgb(${r},${g},${b2})`;
ctx.fillRect(2, y + 2, W - 4, rowH - 4);
ctx.fillStyle = '#fff';
ctx.font = '11px ui-monospace, monospace';
ctx.fillText(m.x.toFixed(3), 6, y + rowH / 2 + 4);
ctx.fillText(m.score.toFixed(3), W / 2, y + rowH / 2 + 4);
});
}
// Button event listeners
document.getElementById('step-btn').addEventListener('click', () => { if (!running) step(); });
document.getElementById('run-btn').addEventListener('click', () => {
if (running) {
running = false;
clearInterval(runTimer);
document.getElementById('run-btn').textContent = 'Run';
document.getElementById('step-btn').disabled = false;
} else {
running = true;
document.getElementById('run-btn').textContent = 'Pause';
document.getElementById('step-btn').disabled = true;
runTimer = setInterval(() => { if (iteration < 500) step(); else { running = false; clearInterval(runTimer); document.getElementById('run-btn').textContent = 'Run'; document.getElementById('step-btn').disabled = false; } }, 80);
}
});
document.getElementById('reset-btn').addEventListener('click', () => {
running = false;
clearInterval(runTimer);
document.getElementById('run-btn').textContent = 'Run';
document.getElementById('step-btn').disabled = false;
iteration = 0;
lastCandidate = null;
lastAction = '';
initMemory();
setStatus('Press Step or Run to start.', '');
draw();
});
// Bootstrap: seed memory and draw initial state
initMemory();
draw();
Comments
Loading comments...