Introduction

Finding the best solution to a hard problem is, at its core, a search through an enormous space of possibilities. Most brute-force methods just cannot afford to check every candidate. Harmony Search (HS), proposed by Zong Woo Geem in 2001, is a metaheuristic that borrows a different strategy: it listens to what already sounds good.

The algorithm keeps a small harmony memory — a collection of the best solutions found so far. At each step, like a jazz musician improvising, it constructs a new candidate by:

  1. Picking a value from memory with probability pHMCp_{HMC} (harmony memory consideration rate),
  2. Nudging that value slightly — a pitch adjustment — with probability pPAp_{PA} (pitch adjustment rate),
  3. Or trying a random new value with probability 1−pHMC1 - p_{HMC}.

If the new "harmony" scores better than the worst member of the memory, it replaces it. Over many iterations the memory fills with progressively better solutions, and the best one recorded is the answer.

The metaphor is remarkably direct: each decision variable is an instrument, each value is a note, and the objective function is the audience's applause. Good music stays in the repertoire; bad experiments are forgotten.

Watch the Memory Sharpen

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();
Result
Console

Press Step to advance one iteration at a time and observe how a new candidate is born (memory pick, pitch adjustment, or random). Press Run to let it converge automatically. Notice how the memory gradually clusters near the global minimum — each iteration discards the worst note and replaces it with a slightly better improvisation.

How It Works

Harmony Search sits in the broad family of population-based metaheuristics alongside simulated annealing, genetic algorithms, and non-convex optimization methods generally. Understanding where it fits requires being honest about what it promises.

The three knobs:

  • pHMCp_{HMC} (typically 0.7–0.95): how often to draw from memory vs. generate randomly. High values exploit; low values explore.
  • pPAp_{PA} (typically 0.1–0.5): how often to nudge a memory value. Controls local refinement.
  • bwb_w (bandwidth): how far a pitch adjustment can travel. Larger = rougher exploration.

What it guarantees — and what it does not:

  • With probability 1, any globally optimal solution will eventually enter the memory — but "eventually" can mean a very long time.
  • There is no polynomial-time convergence bound for non-convex problems; getting stuck near a local optimum is always possible.
  • Each iteration costs only O(HMS)O(HMS) evaluations (harmony memory size), making it cheap per step but potentially slow in total.

Why it sometimes wins anyway: unlike gradient methods, Harmony Search needs no derivative information. Unlike pure random search, the memory biases new candidates toward promising regions. That combination makes it a practical tool when the objective function is black-box, discontinuous, or mixed-integer — landscapes where calculus cannot reach.

Where It Matters

Harmony Search has been applied in a surprisingly wide range of fields since its 2001 debut — wherever the search space is large, the function is noisy or discontinuous, and exact methods are too expensive:

  • Structural engineering: minimizing the weight of trusses and frames subject to stress and deflection constraints. The variables (member sizes) are discrete and the constraints are nonlinear — a natural fit.
  • Water distribution networks: Geem himself applied HS to optimal pipe sizing, reducing cost while meeting pressure requirements throughout the network.
  • Scheduling: timetabling, job-shop problems, and vehicle routing benefit from population-based search when the constraint landscape is rough — see also scheduling.
  • Music and art generation: the metaphor comes full circle — HS has been used to generate melodies that satisfy harmonic rules, letting an algorithm compose simple tunes.
  • Hyperparameter tuning: the memory-plus-perturbation strategy applies naturally to tuning machine learning models, where gradient information is absent or expensive.

It is not a silver bullet: on smooth, convex problems a gradient method will win easily. But whenever the landscape is jagged and the budget is limited, improvising from a memory of what already works is a surprisingly effective strategy.

Conclusion

Harmony Search distills a universal insight into a simple loop: keep a memory of what works, improvise variations on it, and occasionally try something wild. The algorithm has no guarantee of finding the globally best answer in bounded time, but it earns its place wherever derivative-free, flexible search is needed.

Its musical metaphor is not mere decoration. Every real optimization problem involves the same tension — exploit what you know, or explore what you don't. Harmony Search makes that tension explicit, gives it three tunable knobs, and lets you watch the chord resolve.

For deeper dives into related ideas, explore non-convex optimization for the landscape that makes these heuristics necessary, or scheduling for a domain where Harmony Search regularly competes with exact solvers.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/harmony-search/Content licensed under CC BY-NC 4.0.