Skip to main content

Harmony Search optimisation with dependent variable spaces and engineering domain catalogues

Project description

harmonix

CI Python Tests License: MIT

Harmony Search optimisation with dependent variable spaces and engineering domain catalogues.

harmonix is a Python library for solving single- and multi-objective optimisation problems using the Harmony Search metaheuristic. Its key design principle is search-space first: instead of just minimising a function, you describe the domain of each variable precisely — including dependencies between variables, discrete grids, catalogue lookups, and domain-specific feasibility rules — and let the algorithm handle the rest.

from harmonix import DesignSpace, Continuous, Discrete, Minimization

space = DesignSpace()
space.add("h",  Continuous(0.30, 1.20))
space.add("bf", Continuous(lo=lambda ctx: ctx["h"] * 0.5,
                           hi=lambda ctx: ctx["h"] * 2.0))
space.add("n",  Discrete(4, 2, 20))

def objective(harmony):
    h, bf, n = harmony["h"], harmony["bf"], harmony["n"]
    cost    = 1.1 * h * bf + 0.04 * n
    penalty = max(0.0, h - 2 * bf)
    return cost, penalty

result = Minimization(space, objective).optimize(
    memory_size=20, hmcr=0.85, par=0.35, max_iter=5000
)
print(result)

Installation

pip install harmonix-opt

Requires Python 3.8+. No mandatory dependencies beyond the standard library.

For development:

pip install -r requirements-dev.txt
pip install -e .

Core concepts

Design variables

Every variable implements three methods that the algorithm calls internally:

Method Purpose
sample(ctx) Draw a random feasible value
filter(candidates, ctx) Keep only feasible values from harmony memory
neighbor(value, ctx) Return an adjacent feasible value (pitch adjustment)

The ctx argument is a dict of all variable values assigned earlier in the same harmony. This enables dependent bounds — the domain of a variable can depend on previously assigned variables.

Built-in variable types

Type Domain
Continuous(lo, hi) ℝ ∩ [lo, hi]
Discrete(lo, step, hi) {lo, lo+step, …, hi}
Integer(lo, hi) {lo, lo+1, …, hi}
Categorical(choices) finite label set

All bounds accept callables for dependent domains:

space.add("d",  Continuous(0.40, 1.20))
space.add("tw", Continuous(lo=lambda ctx: ctx["d"] / 50,
                           hi=lambda ctx: ctx["d"] / 10))

Domain-specific variable spaces

harmonix ships a catalogue of ready-made variable types for common engineering and mathematical domains:

from harmonix import ACIRebar, SteelSection, ConcreteGrade, PrimeVariable

# ACI 318 ductile bar arrangement — bounds depend on d and fc
space.add("rebar", ACIRebar(d_expr=lambda ctx: ctx["d"],
                            cc_expr=60.0,
                            fc=lambda ctx: ctx["grade"].fck_MPa,
                            fy=420.0))

# Standard steel I-section from built-in catalogue (IPE, HEA, HEB, W)
space.add("section", SteelSection(series=["IPE", "HEA"]))

# EN 206 concrete grade (C12/15 to C90/105)
space.add("concrete", ConcreteGrade(min_grade="C25/30", max_grade="C50/60"))

# Prime numbers
space.add("p", PrimeVariable(lo=2, hi=500))

Full catalogue:

Category Types
Mathematical NaturalNumber, WholeNumber, NegativeInt, NegativeReal, PositiveReal, PrimeVariable, PowerOfTwo, Fibonacci
Structural ACIRebar, ACIDoubleRebar, SteelSection, ConcreteGrade
Geotechnical SoilSPT
Seismic SeismicZoneTBDY

All types are also accessible via the plugin registry:

from harmonix import create_variable, list_variable_types
print(list_variable_types())
var = create_variable("aci_rebar", d_expr=0.55, cc_expr=40.0)

Custom variables

Subclass Variable for full control:

from harmonix import Variable, register_variable

@register_variable("my_type")
class MyVariable(Variable):
    def sample(self, ctx):              ...
    def filter(self, candidates, ctx):  ...
    def neighbor(self, value, ctx):     ...

Factory function for quick prototyping:

from harmonix import make_variable
import random

EvenVar = make_variable(
    sample   = lambda ctx: random.choice(range(2, 101, 2)),
    filter   = lambda cands, ctx: [c for c in cands if c % 2 == 0],
    neighbor = lambda val, ctx: val + random.choice([-2, 2]),
    name     = "even",
)
space.add("n", EvenVar())

Optimisers

Minimization

result = Minimization(space, objective).optimize(
    memory_size      = 20,       # Harmony Memory Size (HMS)
    hmcr             = 0.85,     # Harmony Memory Considering Rate
    par              = 0.35,     # Pitch Adjusting Rate
    max_iter         = 5000,
    bw_max           = 0.05,     # Initial bandwidth (5% of domain width)
    bw_min           = 0.001,    # Final bandwidth (exponential decay)
    resume           = "auto",   # "auto" | "new" | "resume"
    checkpoint_path  = "run.json",
    checkpoint_every = 500,
    use_cache        = False,    # Cache identical harmony evaluations
    cache_maxsize    = 4096,
    log_init         = False,    # Write initial memory to CSV
    log_history      = False,    # Write best-per-iteration to CSV
    log_evaluations  = False,    # Write every evaluated harmony to CSV
    history_every    = 1,
    verbose          = True,
    callback         = my_callback,
)
print(result.best_harmony)
print(result.best_fitness)

Maximization

Same interface — negates internally, reports original sign.

result = Maximization(space, objective).optimize(...)

MultiObjective

def objective(harmony):
    f1 = harmony["x"] ** 2
    f2 = (harmony["x"] - 2) ** 2
    return (f1, f2), 0.0    # tuple of objectives, penalty

result = MultiObjective(space, objective).optimize(
    max_iter     = 10_000,
    archive_size = 100,
)

for entry in result.front:
    print(entry.objectives, entry.harmony)

Callback and early stopping

def my_callback(iteration, partial_result):
    print(iteration, partial_result.best_fitness)
    if partial_result.best_fitness < 1e-4:
        raise StopIteration    # stops the loop cleanly

Advanced features

Dynamic bandwidth narrowing

The pitch adjustment step size decays exponentially from bw_max to bw_min over the run — wide exploration early, fine convergence late.

result = Minimization(space, objective).optimize(
    bw_max=0.10,   # 10% of domain width at iteration 0
    bw_min=0.001,  # 0.1% at final iteration
    max_iter=5000,
)

Set bw_max == bw_min for constant bandwidth (original HS behaviour). Discrete and categorical variables are unaffected by bandwidth.

Resume control

# "auto"   — continue if checkpoint exists, start fresh otherwise (safe default)
# "new"    — always start fresh, overwrite any existing checkpoint
# "resume" — always continue; raises FileNotFoundError if checkpoint missing

result = optimizer.optimize(
    max_iter        = 50_000,
    checkpoint_path = "run.json",
    resume          = "auto",
)

The initial harmony memory is saved immediately at startup — even a run interrupted in the first seconds can be resumed cleanly.

Evaluation cache

Identical harmonies are never re-evaluated when use_cache=True. Particularly valuable for expensive objectives (FEM, CFD, etc.).

result = optimizer.optimize(use_cache=True, cache_maxsize=4096)
print(optimizer._cache.stats())
# EvaluationCache: 412 hits / 1005 total (41.0% hit rate)  size=593/4096

CSV logging

result = optimizer.optimize(
    checkpoint_path  = "run.json",
    log_init         = True,    # → run_init.csv     (initial memory)
    log_history      = True,    # → run_history.csv  (best per iteration)
    log_evaluations  = True,    # → run_evals.csv    (every evaluation)
    history_every    = 10,      # write history every 10 iterations
)

All CSV files are readable directly in Excel or with pandas.read_csv().


Decoding engineering variables

Variables like ACIRebar and SteelSection store integer codes in the harmony. Use decode() to get full properties:

rebar_var = ACIRebar(d_expr=0.55, cc_expr=40.0)
code = result.best_harmony["rebar"]
diameter_mm, bar_count = rebar_var.decode(code)
print(rebar_var.describe(code))   # "8 bars of Ø19.00 mm"

section_var = SteelSection(series=["IPE"])
sec = section_var.decode(result.best_harmony["section"])
print(sec.name, sec.Iy_cm4, "cm4")

grade_var = ConcreteGrade()
grade = grade_var.decode(result.best_harmony["concrete"])
print(grade.name, grade.fck_MPa, "MPa", grade.Ecm_GPa, "GPa")

Steel section catalogue

The built-in catalogue covers IPE 80–600, HEA 100–500, HEB 100–500, and W-sections. Override with your own file:

var = SteelSection(catalogue="my_sections.json")  # custom catalogue
var = SteelSection(series=["HEA", "HEB"])          # filter series

Algorithm background

harmonix implements Harmony Search with several enhancements:

Dynamic bandwidth narrowing — pitch adjustment step size decays exponentially. Early iterations explore broadly; late iterations converge precisely.

Intelligent pitch adjustmentneighbor() is called with the current dependency context so the perturbed value stays feasible. The common incorrect approach of calling sample() on PAR is avoided.

Dependent search spaces — variables are sampled in definition order; each receives a context dict of previously assigned values. Dependent bounds, catalogue filters, and feasibility checks can reference earlier variables without any special handling in the optimiser loop.

Deb constraint handling — feasible solutions always rank above infeasible ones; among infeasible solutions ranking is by total penalty.

References

  • Geem, Z. W., Kim, J. H., & Loganathan, G. V. (2001). A new heuristic optimization algorithm: Harmony search. Simulation, 76(2), 60–68.
  • Lee, K. S., & Geem, Z. W. (2005). A new meta-heuristic algorithm for continuous engineering optimization. Computer Methods in Applied Mechanics and Engineering, 194(36–38), 3902–3933.
  • Deb, K. (2000). An efficient constraint handling method for genetic algorithms. Computer Methods in Applied Mechanics and Engineering, 186(2–4), 311–338.
  • Ricart, J., Hüttemann, G., Lima, J., & Barán, B. (2011). Multiobjective harmony search algorithm proposals. Electronic Notes in Theoretical Computer Science, 281, 51–67.

Testing

pip install -r requirements-dev.txt
pytest tests/ -v

325 tests across 9 test files covering:

  • All variable types — sample, filter, neighbor, edge cases, lo > hi validation
  • DesignSpace — dependency chains, empty space, 50-variable stress test
  • Optimisers — Minimization, Maximization, MultiObjective
  • New features — bandwidth decay, resume modes, evaluation cache, CSV logging
  • Pareto archive — dominance, crowding distance, serialization
  • Engineering physics — EC2 formulas, ACI 318 feasibility, steel section properties
  • Determinism — same seed produces identical results
  • Numerical correctness — Sphere, Rosenbrock, constrained minimization

Project structure

harmonix/
├── harmonix/
│   ├── variables.py       # Continuous, Discrete, Integer, Categorical
│   ├── space.py           # DesignSpace
│   ├── optimizer.py       # Minimization, Maximization, MultiObjective
│   ├── pareto.py          # Pareto archive, crowding distance
│   ├── registry.py        # register_variable, make_variable
│   ├── logging.py         # EvaluationCache, RunLogger
│   └── spaces/
│       ├── math.py        # Mathematical search spaces
│       └── engineering.py # Engineering domain spaces
├── examples/
│   ├── 01_quickstart.py
│   ├── 02_dependent_bounds.py
│   ├── 03_engineering_rc_beam.py
│   ├── 04_custom_variables.py
│   ├── 05_multi_objective.py
│   ├── 06_steel_beam_design.py
│   └── 07_rc_section_full.py
├── tests/                 # 325 tests across 9 files
├── requirements-dev.txt
├── ruff.toml
├── pyproject.toml
└── LICENSE

License

MIT © Abdulkadir Özcan

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

harmonix_opt-1.0.0.tar.gz (67.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

harmonix_opt-1.0.0-py3-none-any.whl (44.9 kB view details)

Uploaded Python 3

File details

Details for the file harmonix_opt-1.0.0.tar.gz.

File metadata

  • Download URL: harmonix_opt-1.0.0.tar.gz
  • Upload date:
  • Size: 67.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for harmonix_opt-1.0.0.tar.gz
Algorithm Hash digest
SHA256 7845a7c36b95d38c1a00a8506934f189c10f2f6aa67ae0ff66ff0d015cab9070
MD5 aa0769456b56f7dada747fafc83fbd14
BLAKE2b-256 e32207ce15fb431a7d7fbf0e443bfea3774fb180a1cb35da3e386c7d649114e6

See more details on using hashes here.

Provenance

The following attestation bundles were made for harmonix_opt-1.0.0.tar.gz:

Publisher: publish.yml on AutoPyloter/harmonix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file harmonix_opt-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: harmonix_opt-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 44.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for harmonix_opt-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0365deec6fc12217fc152828d996d1422206a8caa047b9f13b5aabbcc128e6e8
MD5 38ea0d4a35a4574a2e8a71caa46660ca
BLAKE2b-256 f79c1a356dae61e0dfdb2b145c9d9a2b1db588f350a00c13a232486b7b2db1cc

See more details on using hashes here.

Provenance

The following attestation bundles were made for harmonix_opt-1.0.0-py3-none-any.whl:

Publisher: publish.yml on AutoPyloter/harmonix

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page