Source code for pymoo.algorithms.soo.nonconvex.cmaes

"""Covariance Matrix Adaptation Evolution Strategy (CMA-ES)."""

import cma
import numpy as np

from pymoo.algorithms.base.local import LocalSearch
from pymoo.core.population import Population
from pymoo.core.termination import NoTermination
from pymoo.docs import parse_doc_string
from pymoo.termination.max_eval import MaximumFunctionCallTermination
from pymoo.termination.max_gen import MaximumGenerationTermination
from pymoo.util.display.column import Column
from pymoo.util.display.single import SingleObjectiveOutput
from pymoo.util.normalization import ZeroToOneNormalization, NoNormalization
from pymoo.util.optimum import filter_optimum
from pymoo.vendor.vendor_cmaes import my_fmin


# =========================================================================================================
# Implementation
# =========================================================================================================


class CMAESOutput(SingleObjectiveOutput):
    def __init__(self):
        super().__init__()

        self.sigma = Column("sigma")
        self.min_std = Column("min_std", width=8)
        self.max_std = Column("max_std", width=8)
        self.axis = Column("axis", width=8)

        self.run = Column("run", width=4)
        self.fpop = Column("fpop", width=8)
        self.n_pop = Column("n_pop", width=5)

    def initialize(self, algorithm):
        super().initialize(algorithm)

        if algorithm.restarts > 0:
            self.columns += [self.run, self.fpop, self.n_pop]

        self.columns += [self.sigma, self.min_std, self.max_std, self.axis]

    def update(self, algorithm):
        super().update(algorithm)

        if not algorithm.es.gi_frame:
            return

        fmin = algorithm.es.gi_frame.f_locals
        cma = fmin["es"]

        self.sigma.set(cma.sigma)

        val = cma.sigma_vec * cma.dC**0.5
        self.min_std.set((cma.sigma * min(val)))
        self.max_std.set((cma.sigma * max(val)))

        if algorithm.restarts > 0:
            self.run.set(int(fmin["irun"] - fmin["runs_with_small"]) + 1)
            self.fpop.set(algorithm.pop.get("F").min())
            self.n_pop.set(int(cma.opts["popsize"]))

        axis = (
            cma.D.max() / cma.D.min()
            if not cma.opts["CMA_diagonal"] or cma.countiter > cma.opts["CMA_diagonal"]
            else max(cma.sigma_vec * 1) / min(cma.sigma_vec * 1)
        )
        self.axis.set(axis)


[docs] class CMAES(LocalSearch): def __init__( self, x0=None, sigma=0.1, normalize=True, parallelize=True, maxfevals=np.inf, tolfun=1e-11, tolx=1e-11, restarts=0, restart_from_best="False", incpopsize=2, eval_initial_x=False, noise_handler=None, noise_change_sigma_exponent=1, noise_kappa_exponent=0, bipop=False, cmaes_verbose=-9, verb_log=0, output=CMAESOutput(), pop_size=None, **kwargs, ): """Covariance Matrix Adaptation Evolution Strategy. Args: x0: Initial guess of minimum solution (array or string expression). sigma: Initial standard deviation in each coordinate. normalize: Whether to normalize problem bounds. parallelize: Whether to call objective function batch-wise. maxfevals: Maximum number of function evaluations. tolfun: Termination tolerance in function value. tolx: Termination tolerance in x-changes. restarts: Number of restarts with increasing population size (IPOP-CMA-ES). restart_from_best: Whether to restart from best solution. incpopsize: Multiplier for population size increase. eval_initial_x: Whether to evaluate initial solution. noise_handler: Noise handling instance or class. noise_change_sigma_exponent: Exponent for sigma increment. noise_kappa_exponent: Kappa exponent for noise treatment. bipop: Whether to use BIPOP-CMA-ES restart strategy. cmaes_verbose: Verbosity level for CMA-ES output. verb_log: Verbosity for logging to files. output: Output display configuration. pop_size: Population size (overrides CMA-ES default). **kwargs: Additional CMA-ES options passed to CMAEvolutionStrategy. """ if pop_size is not None: parallelize = True kwargs["popsize"] = pop_size super().__init__(x0=x0, output=output, **kwargs) self.termination = NoTermination() self.es = None self.cma = None self.normalize = normalize self.norm = None self.sigma = sigma self.restarts = restarts self.restart_from_best = restart_from_best self.incpopsize = incpopsize self.eval_initial_x = eval_initial_x self.noise_handler = noise_handler self.noise_change_sigma_exponent = noise_change_sigma_exponent self.noise_kappa_exponent = noise_kappa_exponent self.bipop = bipop self.options = dict( verbose=cmaes_verbose, verb_log=verb_log, maxfevals=maxfevals, tolfun=tolfun, tolx=tolx, **kwargs, ) self.send_array_to_yield = True self.parallelize = parallelize self.al = None def _setup(self, problem, **kwargs): xl, xu = problem.bounds() if self.normalize: self.norm, self.options["bounds"] = bounds_if_normalize(xl, xu) else: self.norm = NoNormalization() self.options["bounds"] = [xl, xu] seed = kwargs.get("seed", self.seed) self.options["seed"] = seed if isinstance(self.termination, MaximumGenerationTermination): self.options["maxiter"] = self.termination.n_max_gen elif isinstance(self.termination, MaximumFunctionCallTermination): self.options["maxfevals"] = self.termination.n_max_evals def _initialize_advance(self, **kwargs): super()._initialize_advance(**kwargs) kwargs = dict( options=self.options, parallel_objective=self.parallelize, restarts=self.restarts, restart_from_best=self.restart_from_best, incpopsize=self.incpopsize, eval_initial_x=self.eval_initial_x, noise_handler=self.noise_handler, noise_change_sigma_exponent=self.noise_change_sigma_exponent, noise_kappa_exponent=self.noise_kappa_exponent, bipop=self.bipop, random_state=self.random_state, ) x0 = self.norm.forward(self.x0.X) self.es = my_fmin(x0, self.sigma, **kwargs) # do this to allow the printout in the first generation self.next_X = next(self.es) def _infill(self): X = np.array(self.next_X) self.send_array_to_yield = X.ndim > 1 X = np.atleast_2d(X) # evaluate the population self.pop = Population.new("X", self.norm.backward(X)) return self.pop def _advance(self, infills=None, **kwargs): if infills is None: self.termination.force_termination = True else: # set infeasible individual's objective values to np.nan - then CMAES can handle it for ind in infills: if not ind.feas: ind.F[:] = np.inf F = infills.get("f").tolist() if not self.send_array_to_yield: F = F[0] try: self.next_X = self.es.send(F) except: # noqa: E722 self.next_X = None if self.next_X is None: self.termination.force_termination = True def _set_optimum(self): pop = self.pop if self.opt is None else Population.merge(self.opt, self.pop) self.opt = filter_optimum(pop, least_infeasible=True) def __getstate__(self): state = self.__dict__.copy() state.pop("es", None) return state def __setstate__(self, state): self.__dict__.update(state) self.ers = None
class SimpleCMAES(LocalSearch): def __init__(self, sigma=0.1, opts=None, normalize=True, **kwargs): super().__init__(**kwargs) self.termination = NoTermination() self.es = None self.sigma = sigma self.normalize = normalize self.norm = None DEFAULTS = {"verb_disp": 0} if opts is None: opts = {} for k, v in DEFAULTS.items(): if k not in kwargs: opts[k] = v self.opts = opts def _setup(self, problem, **kwargs): xl, xu = problem.bounds() if self.normalize: self.norm, self.opts["bounds"] = bounds_if_normalize(xl, xu) else: self.norm = NoNormalization() self.opts["bounds"] = [xl, xu] self.opts["seed"] = self.seed def _initialize_advance(self, infills=None, **kwargs): super()._initialize_advance(infills, **kwargs) x = self.norm.forward(self.x0.X) self.es = cma.CMAEvolutionStrategy(x, self.sigma, inopts=self.opts) def _infill(self): X = self.norm.backward(np.array(self.es.ask())) return Population.new("X", X) def _advance(self, infills=None, **kwargs): X, F = infills.get("X", "F") X = self.norm.forward(X) self.es.tell(X, F[:, 0]) self.pop = infills if self.es.stop(): self.termination.force_termination = True def _set_optimum(self): pop = self.pop if self.opt is None else Population.merge(self.opt, self.pop) self.opt = filter_optimum(pop, least_infeasible=True) class BIPOPCMAES(CMAES): def __init__(self, restarts=4, **kwargs): super().__init__(restarts=restarts, bipop=True, **kwargs) def bounds_if_normalize(xl, xu): norm = ZeroToOneNormalization(xl=xl, xu=xu) _xl, _xu = np.zeros_like(xl), np.ones_like(xu) if xl is not None: _xl[np.isnan(xl)] = np.nan if xu is not None: _xu[np.isnan(xu)] = np.nan return norm, [_xl, _xu] parse_doc_string(CMAES.__init__)