AHD-CMA / src /ahdcma /algorithms /cmaes_wrapper.py
Akiyue's picture
Add files using upload-large-folder tool
30c3c53 verified
Raw
History Blame Contribute Delete
3.21 kB
"""Thin wrapper around :mod:`cma` (pycma) implementing our :class:`Optimizer` API.
We keep the wrapper deliberately minimal: pycma manages step-size,
covariance, and the population schedule, while we record the per-
generation history, clip into the box, and expose the same interface as
the other optimizers in this package.
Notes
-----
- pycma operates without explicit bounds by default. We pass them via
``BoundaryHandler`` and clip the asked points before evaluation, which
is robust on the unit cube used throughout the project.
- pycma's "generation" corresponds to one ``ask``/``tell`` round.
- pycma needs ``d >= 2``; raise a clear error on ``d == 1``.
"""
from __future__ import annotations
import time
from typing import Any
import cma
import numpy as np
from numpy.typing import NDArray
from ahdcma.algorithms.base import (
FitnessFn,
OptimizationResult,
Optimizer,
SearchSpace,
)
class CMAES(Optimizer):
"""CMA-ES via pycma."""
def __init__(
self,
config: dict[str, Any],
fitness_fn: FitnessFn,
search_space: SearchSpace,
*,
run_id: str = "anonymous",
) -> None:
super().__init__(config, fitness_fn, search_space, run_id=run_id)
if search_space.dim < 2:
raise ValueError("CMA-ES requires dim >= 2; got dim=1")
self.population_size: int = int(config["population_size"])
self.t_max: int = int(config["max_generations"])
self.sigma: float = float(config.get("initial_sigma", 0.3))
self.seed: int = int(config["seed"])
if self.population_size < 4:
raise ValueError("CMA-ES population_size must be >= 4")
if self.t_max < 1:
raise ValueError("max_generations must be >= 1")
def _evaluate(self, X: NDArray[np.float64]) -> NDArray[np.float64]:
return np.array([float(self.fitness_fn(x)) for x in X], dtype=np.float64)
def optimize(self) -> OptimizationResult:
sp = self.search_space
x0 = 0.5 * (sp.lower + sp.upper)
opts = {
"popsize": self.population_size,
"bounds": [sp.lower.tolist(), sp.upper.tolist()],
"seed": self.seed if self.seed > 0 else 1, # pycma forbids seed==0
"verbose": -9,
"verb_log": 0,
"verb_disp": 0,
"maxiter": self.t_max,
}
es = cma.CMAEvolutionStrategy(x0.tolist(), self.sigma, opts)
wall_t0 = time.time()
gen = 0
while not es.stop() and gen < self.t_max:
asked = es.ask()
X = sp.clip(np.asarray(asked, dtype=np.float64))
F = self._evaluate(X)
es.tell([X[i].tolist() for i in range(X.shape[0])], F.tolist())
self._history.append(gen, X, F, mode="exploit")
gen += 1
best = es.best.x
best_f = float(es.best.f) if es.best.f is not None else float("inf")
return OptimizationResult(
best_x=np.asarray(best, dtype=np.float64),
best_f=best_f,
history=self._history,
config_snapshot=self.config,
run_id=self.run_id,
wall_time=time.time() - wall_t0,
)