-
Notifications
You must be signed in to change notification settings - Fork 0
Advanced Features
This page covers the optional features that sit on top of the core optimisation loop: dynamic bandwidth narrowing, crash recovery via checkpoints, evaluation caching, CSV logging, callback functions, and early stopping. All of these are configured through keyword arguments to .optimize() and have sensible defaults, so you can ignore them until you need them.
The pitch adjustment step in Harmony Search perturbs a continuous value by a small amount — the bandwidth. harmonix decays this bandwidth exponentially from bw_max at iteration 0 to bw_min at the final iteration, following:
bw(t) = bw_max × exp(−ln(bw_max / bw_min) × t / T)
where t is the current iteration and T is max_iter. Early iterations explore broadly; late iterations converge precisely. Only Continuous variables are affected — Discrete, Integer, and Categorical variables always step by their natural unit regardless of bandwidth.
| Parameter | Default | Meaning |
|---|---|---|
bw_max |
0.05 |
Initial bandwidth as a fraction of domain width. 0.05 = 5% of hi − lo. |
bw_min |
0.001 |
Final bandwidth. Must be ≤ bw_max and > 0. |
result = Minimization(space, objective).optimize(
max_iter = 10_000,
bw_max = 0.10, # start wide — 10% of domain
bw_min = 0.0005, # finish tight — 0.05% of domain
)
- Set
bw_max == bw_minto disable decay and use a constant bandwidth (original HS behaviour). - For large, rugged landscapes increase
bw_maxto 0.10–0.20 so early iterations can escape local optima. - For fine-grained convergence on smooth problems decrease
bw_minto 0.0001 or lower. - Both values must be strictly positive. Passing zero raises a
ValueError.
Long runs can be interrupted by crashes, time limits, or manual stops. harmonix saves the full harmony memory to a JSON file at regular intervals so a run can be continued exactly where it left off.
| Parameter | Default | Meaning |
|---|---|---|
checkpoint_path |
None |
Path to the JSON checkpoint file. Set this to enable checkpointing. |
checkpoint_every |
500 |
Save a checkpoint every this many iterations. |
resume |
"auto" |
One of "auto", "new", or "resume" — see below. |
| Mode | Behaviour | When to use |
|---|---|---|
"auto" |
Resumes if the checkpoint file exists and is non-empty; starts fresh otherwise. | Safe default for most situations — run the same script repeatedly without thinking about it. |
"new" |
Always starts fresh. Overwrites any existing checkpoint file. | When you want to guarantee a clean run regardless of previous state. |
"resume" |
Always resumes. Raises FileNotFoundError if the checkpoint does not exist. |
When continuation is mandatory and a missing checkpoint should be an error, not a silent restart. |
result = Minimization(space, objective).optimize(
max_iter = 50_000,
checkpoint_path = "run.json",
checkpoint_every = 1000,
resume = "auto",
)
The initial harmony memory is saved immediately at startup — before the first improvisation step. Even a run interrupted in the first few seconds can be resumed cleanly. Each subsequent checkpoint overwrites the previous one; only the latest state is kept.
The checkpoint file stores the complete harmony memory (all harmonies, fitness values, and penalties) and the last completed iteration index. It does not store the random state, so a resumed run will follow a different random trajectory — but from the same starting memory.
For MultiObjective, the Pareto archive is also serialised and restored alongside the harmony memory.
When use_cache=True, harmonix wraps the objective function in an LRU cache. Identical harmonies — same variable values, same order — are never evaluated twice. The cached result is returned immediately.
| Parameter | Default | Meaning |
|---|---|---|
use_cache |
False |
Enable the evaluation cache. |
cache_maxsize |
4096 |
Maximum number of entries. Oldest entries are evicted when full (LRU policy). |
result = Minimization(space, objective).optimize(
max_iter = 30_000,
use_cache = True,
cache_maxsize = 8192,
)
# Inspect cache performance after the run
print(optimizer._cache.stats())
# EvaluationCache: 4821 hits / 30000 total (16.1% hit rate) size=4096/8192
- Expensive objectives — FEM simulations, CFD calls, database queries. Even a 5% hit rate can save significant wall time.
- Late-stage convergence — when the harmony memory has largely converged, the same few harmonies are repeatedly considered. Hit rates of 30–50% are common in this phase.
- Discrete-heavy spaces — variables with small finite domains produce repeated harmonies more often than purely continuous spaces.
- Purely continuous spaces with wide domains — the probability of sampling exactly the same float twice is negligible. The cache overhead (hashing the harmony dict on every call) costs more than it saves.
- Very cheap objectives — if evaluation takes microseconds, the cache adds more overhead than it removes.
harmonix can write three separate CSV log files during a run. All are readable directly in Excel or with pandas.read_csv(). Log file paths are derived automatically from checkpoint_path unless you specify them explicitly.
| Flag | Auto path | Columns | Written |
|---|---|---|---|
log_init=True |
run_init.csv |
harmony_index, all variable names, fitness, penalty, feasible
|
Once, after memory initialisation |
log_history=True |
run_history.csv |
iteration, best_fitness, best_penalty, feasible, all variable names prefixed best_
|
Every history_every iterations |
log_evaluations=True |
run_evals.csv |
wall_time_s, iteration, all variable names, fitness, penalty, feasible
|
Every evaluation |
result = Minimization(space, objective).optimize(
max_iter = 10_000,
checkpoint_path = "run.json", # log paths derived from this
log_init = True, # → run_init.csv
log_history = True, # → run_history.csv
log_evaluations = True, # → run_evals.csv
history_every = 50, # write history every 50 iterations
)
You can also specify log paths explicitly:
result = optimizer.optimize(
log_history = True,
history_log_path = "results/convergence.csv",
history_every = 10,
)
-
log_initis useful for debugging — inspect the starting population to verify your space is sampling the right region. -
log_historyis the most useful for post-run analysis. Plotbest_fitnessvsiterationto see the convergence curve. Sethistory_everyto a larger value (e.g. 100) to keep file size manageable on long runs. -
log_evaluationsrecords every harmony evaluated, not just the best. On a 30,000-iteration run this produces a 30,000-row file. Use it for detailed statistical analysis, not routine monitoring.
A callback function is called at the end of every iteration and receives the current iteration index and a partial OptimizationResult (or ParetoResult for multi-objective runs). You can use it to monitor progress, log to an external system, or stop the run early.
def my_callback(iteration, partial_result):
print(f"iter {iteration:>6d} | best = {partial_result.best_fitness:.6g}")
result = Minimization(space, objective).optimize(
max_iter = 10_000,
callback = my_callback,
)
Raise StopIteration inside the callback to terminate the run cleanly. The optimiser catches it and returns the best result found so far.
def early_stop(iteration, partial_result):
if partial_result.best_fitness < 1e-6:
raise StopIteration # target reached — stop now
if partial_result.best_penalty > 0 and iteration > 5000:
raise StopIteration # still infeasible after 5000 iters — give up
result = Minimization(space, objective).optimize(
max_iter = 50_000,
callback = early_stop,
)
print(f"Stopped at iteration {result.iterations}")
from tqdm import tqdm
bar = tqdm(total=10_000)
def update_bar(iteration, partial_result):
bar.set_postfix(fitness=f"{partial_result.best_fitness:.4g}")
bar.update(1)
result = Minimization(space, objective).optimize(
max_iter = 10_000,
callback = update_bar,
)
bar.close()
- Do not modify the
partial_resultobject — it is a live view of the optimiser state. - Avoid slow operations (file I/O, network calls) unless necessary — the callback is on the critical path of every iteration.
- Do not raise any exception other than
StopIterationfor early stopping. Other exceptions will propagate and abort the run without returning a result.
Set verbose=True to print the best fitness and penalty to stdout at every iteration. Useful for quick interactive monitoring without writing a callback.
result = Minimization(space, objective).optimize(
max_iter = 1000,
verbose = True,
)
# [HS] iter 1 | fitness = 18.4321 | penalty = 0.0000
# [HS] iter 2 | fitness = 15.7842 | penalty = 0.0000
# ...
For production runs or Jupyter notebooks, prefer a callback with tqdm or a custom logger over verbose=True — the per-iteration print adds measurable overhead on fast objectives.
All features compose freely. A typical long production run might look like:
from harmonix import Minimization
def on_iteration(it, res):
if res.best_fitness < TARGET:
raise StopIteration
result = Minimization(space, objective).optimize(
max_iter = 100_000,
hmcr = 0.90,
par = 0.40,
bw_max = 0.08,
bw_min = 0.0005,
resume = "auto",
checkpoint_path = "long_run.json",
checkpoint_every = 2000,
use_cache = True,
cache_maxsize = 8192,
log_history = True,
history_every = 100,
callback = on_iteration,
)
-
Optimizer Parameters — complete reference for
hmcr,par,memory_size, and all other core parameters - Algorithm Background — how bandwidth narrowing fits into the broader HS algorithm
- Variable Types — which variable types respond to bandwidth and which do not