diff --git a/README.md b/README.md index c91ac81..2b9afcc 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,57 @@ -# ModularCMAES ![Unittest](https://github.com/IOHprofiler/ModularCMAES/workflows/Unittest/badge.svg) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e25b2d338c194d67954fc9e138ca69cc)](https://app.codacy.com/gh/IOHprofiler/ModularCMAES?utm_source=github.com&utm_medium=referral&utm_content=IOHprofiler/ModularCMAES&utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73720e228a89480585bdde05d3806661)](https://www.codacy.com/gh/IOHprofiler/ModularCMAES/dashboard?utm_source=github.com&utm_medium=referral&utm_content=IOHprofiler/ModularCMAES&utm_campaign=Badge_Coverage) + +

+ Modular CMA-ES Banner +

+ +
+ +![Unittest](https://github.com/IOHprofiler/ModularCMAES/workflows/Unittest/badge.svg) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e25b2d338c194d67954fc9e138ca69cc)](https://app.codacy.com/gh/IOHprofiler/ModularCMAES?utm_source=github.com&utm_medium=referral&utm_content=IOHprofiler/ModularCMAES&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/73720e228a89480585bdde05d3806661)](https://www.codacy.com/gh/IOHprofiler/ModularCMAES/dashboard?utm_source=github.com&utm_medium=referral&utm_content=IOHprofiler/ModularCMAES&utm_campaign=Badge_Coverage) +![PyPI - Version](https://img.shields.io/pypi/v/modcma) +![PyPI - Downloads](https://img.shields.io/pypi/dm/modcma) The **Modular CMA-ES** is a Python and C++ package that provides a modular implementation of the Covariance Matrix Adaptation Evolution Strategy (CMA-ES) algorithm. This package allows you to create various algorithmic variants of CMA-ES by enabling or disabling different modules, offering flexibility and customization in evolutionary optimization. In addition to the CMA-ES, the library includes an implementation of the Matrix Adaptation Evolution Strategy (MA-ES) algorithm, which has similar emprical performance on most problems, but signifanctly lower runtime. All modules implemented are compatible with both the CMA-ES and MA-ES. This implementation is based on the algorithm introduced in the paper "[Evolving the Structure of Evolution Strategies. (2016)](https://ieeexplore.ieee.org/document/7850138)" by Sander van Rijn et. al. If you would like to cite this work in your research, please cite the paper: "[Tuning as a Means of Assessing the Benefits of New Ideas in Interplay with Existing Algorithmic Modules (2021)](https://doi.org/10.1145/3449726.3463167)" by Jacob de Nobel, Diederick Vermetten, Hao Wang, Carola Doerr and Thomas Bäck. -This README provides a high level overview of the implemented modules, and provides some usage examples for both the Python-only and the C++-based versions of the framework. A complete API documentation can be found [here](https://modularcmaes.readthedocs.io/) (under construction). +This README provides a high level overview of the implemented modules, and provides some usage examples for both the Python-only and the C++-based versions of the framework. ## Table of Contents -- [ModularCMAES ](#modularcmaes---) - - [Table of Contents](#table-of-contents) - - [Installation ](#installation-) - - [Python Installation](#python-installation) - - [Installation from source](#installation-from-source) - - [Usage ](#usage-) - - [C++ Backend ](#c-backend-) - - [High Level interface ](#high-level-interface-) - - [Tuning ](#tuning-) - - [Configuration Space Generation](#configuration-space-generation) - - [Creating Settings from a Configuration](#creating-settings-from-a-configuration) - - [Python-only (Legacy) ](#python-only-legacy-) - - [Ask–Tell Interface ](#asktell-interface-) - - [Modules ](#modules-) - - [Matrix Adaptation ](#matrix-adaptation-) - - [Active Update ](#active-update-) - - [Elitism ](#elitism-) - - [Orthogonal Sampling ](#orthogonal-sampling-) - - [Sequential Selection ](#sequential-selection-) - - [Threshold Convergence ](#threshold-convergence-) - - [Sample Sigma ](#sample-sigma-) - - [Quasi-Gaussian Sampling ](#quasi-gaussian-sampling-) - - [Recombination Weights ](#recombination-weights-) - - [Mirrored Sampling ](#mirrored-sampling-) - - [Step size adaptation](#step-size-adaptation) - - [Restart Strategy](#restart-strategy) - - [Bound correction](#bound-correction) - - [Sample Transformer ](#sample-transformer-) - - [Repelling Restart ](#repelling-restart-) - - [Center Placement ](#center-placement-) - - [Citation ](#citation-) - - [License ](#license-) +- [Table of Contents](#table-of-contents) +- [Installation ](#installation-) + - [Python Installation](#python-installation) + - [Installation from source](#installation-from-source) +- [Usage ](#usage-) + - [C++ Backend ](#c-backend-) + - [High Level interface ](#high-level-interface-) + - [Low Level interface ](#low-level-interface-) + - [Tuning ](#tuning-) + - [Configuration Space Generation](#configuration-space-generation) + - [Creating Settings from a Configuration](#creating-settings-from-a-configuration) + - [Integer Variables](#integer-variables) + - [Python-only (Legacy) ](#python-only-legacy-) + - [Ask–Tell Interface ](#asktell-interface-) +- [Modules ](#modules-) + - [Matrix Adaptation ](#matrix-adaptation-) + - [Active Update ](#active-update-) + - [Elitism ](#elitism-) + - [Orthogonal Sampling ](#orthogonal-sampling-) + - [Sequential Selection ](#sequential-selection-) + - [Threshold Convergence ](#threshold-convergence-) + - [Sample Sigma ](#sample-sigma-) + - [Quasi-Gaussian Sampling ](#quasi-gaussian-sampling-) + - [Recombination Weights ](#recombination-weights-) + - [Mirrored Sampling ](#mirrored-sampling-) + - [Step size adaptation](#step-size-adaptation) + - [Restart Strategy](#restart-strategy) + - [Bound correction](#bound-correction) + - [Sample Transformer ](#sample-transformer-) + - [Repelling Restart ](#repelling-restart-) + - [Center Placement ](#center-placement-) +- [Citation ](#citation-) +- [License ](#license-) ## Installation @@ -99,6 +111,27 @@ If you want to work on a development version of the library, you should follow t For performance, completeness, and modularity, the **C++ backend** is the **recommended interface** for ModularCMAES. It exposes all available modules and options through a direct Python binding. +#### High Level interface + +In addition to the fully specified method described below, we can run an optimization via a friendly `fmin` interface: + +```python +x0 = [0, 1, 2, 3] # Location to start the search from +sigma0 = 0.234 # Initial estimate of the stepsize, try 0.3 * (ub - lb) if you're unsure +budget = 100 # Total number of function evaluations +xopt, fopt, evals, cma = c_maes.fmin( + func, x0, sigma0, budget, + # We can specify modules and setting values as keyword arguments + active=True, + target=10.0, + cc=0.8 + matrix_adaptation='NONE' +) +``` +Note that the `func`, `x0`, `sigma0` and `budget` arguments are **required**. Modules and settings can be specified via keyword arguments by they corresponding names in the `Modules` and `Settings` objects. Module options, such as `matrix_adaptation` in the above example, can be specified by their name as `str`. + +#### Low Level interface + To run an optimization, first create a `Modules` object specifying which modules and options to enable. Then construct a `Settings` object (which defines problem dimension and strategy parameters), and pass it through a `Parameters` object to the optimizer: ```python @@ -144,26 +177,12 @@ while not cma.break_conditions(): This modularity allows experimentation with specific parts of the evolution strategy, such as custom selection, recombination, or adaptation routines. -#### High Level interface -In addition to the fully specified method described above, we can also call the optimizer via a more friendly `fmin` interface: -```python -x0 = [0, 1, 2, 3] # Location to start the search from -sigma0 = 0.234 # Initial estimate of the stepsize, try 0.3 * (ub - lb) if you're unsure -budget = 100 # Total number of function evaluations -xopt, fopt, evals, cma = c_maes.fmin( - func, x0, sigma0, budget, - # We can specify modules and setting values as keyword arguments - active=True, - cc=0.8 -) -``` -Note that the `func`, `x0`, `sigma0` and `budget` arguments are required. Via keyword arguments, modules and settings can be specified, provided by their names in the `Modules` and `Settings` objects. --- -#### Tuning +### Tuning To facilitate **automated hyperparameter tuning**, ModularCMAES now provides functionality to **create and manipulate configuration spaces** directly compatible with popular optimization and AutoML tools, such as **SMAC**, **BOHB**. This functionality allows users to systematically explore both **algorithmic module combinations** and **numerical hyperparameters** (e.g., population size, learning rates, damping coefficients). @@ -230,6 +249,25 @@ cma = c_maes.ModularCMAES(parameters) cma.run(func) ``` +### Integer Variables +A rudimentary mechanism for dealing with integer variables is implemented, which applies a lower bound on `sigma` for integer coordinates and rounds them to the nearest integer before evaluation, inspired by http://www.cmap.polytechnique.fr/~nikolaus.hansen/marty2024lb+ic-cma.pdf. + +An option can be provided to the `Settings` object or `settings_from_dict`/`settings_from_config` functions: + +```python +dim = 5 +settings = Settings( + dim, + integer_variables=[1, 2] # A list or numpy array of integer indices +) + +settings = settings_from_dict( + dim, + integer_variables=list(range(dim)) # All variables are integer +) +``` + + --- ### Python-only (Legacy) @@ -237,6 +275,7 @@ cma.run(func) > **Legacy notice:** > The Python-only implementation is **no longer actively developed** and does not include all features of the C++ version. > It remains available for experimentation and teaching purposes. +> A complete API documentation can be found [here](https://modularcmaes.readthedocs.io/) (under construction). The Python interface provides a simple API and includes a convenience `fmin` function for optimizing a single objective function in one call: diff --git a/banner.png b/banner.png new file mode 100644 index 0000000..5155f64 Binary files /dev/null and b/banner.png differ diff --git a/include/bounds.hpp b/include/bounds.hpp index 3edce9a..1f84670 100644 --- a/include/bounds.hpp +++ b/include/bounds.hpp @@ -14,8 +14,6 @@ namespace parameters namespace bounds { - using Mask = Eigen::Array; - Mask is_out_of_bounds(const Vector &xi, const Vector &lb, const Vector &ub); bool any_out_of_bounds(const Vector &xi, const Vector &lb, const Vector &ub); diff --git a/include/common.hpp b/include/common.hpp index 0cfff21..9b5183c 100644 --- a/include/common.hpp +++ b/include/common.hpp @@ -28,10 +28,13 @@ using Matrix = Eigen::Matrix; using Vector = Eigen::Matrix; using Array = Eigen::Array; using size_to = std::optional; +using Mask = Eigen::Array; +using Indices = Eigen::ArrayXi; template std::ostream &operator<<(std::ostream &os, const std::vector &x); + using FunctionType = std::function; namespace constants diff --git a/include/mapping.hpp b/include/mapping.hpp new file mode 100644 index 0000000..ec574ec --- /dev/null +++ b/include/mapping.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include "common.hpp" + +namespace parameters +{ + struct Parameters; +} + +namespace mapping +{ + + // struct IntegerHandling + // { + // Float lb_sigma; + + // IntegerHandling(const size_t d, const Float mueff) + // : lb_sigma(std::min(0.2, mueff / static_cast(d))) + // { + // } + + // virtual Array get_effective_sigma(const parameters::Parameters &p, const size_t idx); + + // virtual Vector round_to_integer(const Vector &x, const Indices iidx) + // { + // auto x_rounded = x; + // for (const auto &idx : iidx) + // x_rounded[idx] = std::round(x[idx]); + // return x_rounded; + // } + // }; + + // struct NoIntegerHandling : IntegerHandling + // { + // using IntegerHandling::IntegerHandling; + + // // virtual void get_effective_sigma(const parameters::Parameters& p) override {} + // // void round_to_integer(Eigen::Ref x, const Indices iidx) override {} + // }; + // Array IntegerHandling::get_effective_sigma(const parameters::Parameters& p, const size_t idx) + // { + // Array effective_sigma = Array::Constant(p.settings.dim, p.pop.S(idx)); + + // const Array& Cdiag = p.adaptation->coordinate_wise_variances; + // for (const auto& iidx: p.settings.integer_variables) + // { + // const Float Cii = std::max(Cdiag[iidx], Float(1e-16)); + // const Float sigma_from_lb = lb_sigma / std::sqrt(Cii); + // effective_sigma[iidx] = std::max(sigma_from_lb, effective_sigma[iidx]); + // } + + // return effective_sigma; + // } + + using CoordinateTransformType = std::function; + + inline Float identity(Float x) { return x; } + inline Float iround(Float x) { return std::round(x); } + + struct CoordinateMapping + { + size_t d; + std::vector transformations; + + CoordinateMapping(const Indices &integer_idx, const size_t d, const Float mueff) + : d(d), transformations(d, identity) + { + for (int i = 0; i < integer_idx.size(); i++) + transformations[i] = iround; + } + + Vector transform(const Vector &x) + { + Vector y = x; + for (size_t i = 0; i < d; i++) + y(i) = transformations[i](x(i)); + return y; + } + }; + + inline std::shared_ptr get(const Indices &integer_idx, const size_t d, const Float mueff) + { + return std::make_shared(integer_idx, d, mueff); + } +} \ No newline at end of file diff --git a/include/matrix_adaptation.hpp b/include/matrix_adaptation.hpp index 24642d8..6e8dfa6 100644 --- a/include/matrix_adaptation.hpp +++ b/include/matrix_adaptation.hpp @@ -11,12 +11,18 @@ namespace matrix_adaptation struct Adaptation { Vector m, m_old, dm, ps, dz; + Vector coordinate_wise_variances; + Float dd; Float expected_length_z; Adaptation(const size_t dim, const Vector& x0, const Vector& ps, const Float expected_length_z) : - m(x0), m_old(dim), dm(Vector::Zero(dim)), - ps(ps), dd(static_cast(dim)), + m(x0), + m_old(dim), + dm(Vector::Zero(dim)), + ps(ps), + coordinate_wise_variances(Vector::Ones(dim)), + dd(static_cast(dim)), expected_length_z(expected_length_z) { } @@ -50,7 +56,7 @@ namespace matrix_adaptation virtual Vector compute_y(const Vector&) = 0; - virtual Vector invert_x(const Vector&, Float sigma); + virtual Vector invert_x(const Vector&, Vector sigma); virtual Vector invert_y(const Vector&) = 0; @@ -61,6 +67,7 @@ namespace matrix_adaptation dm.setZero(); ps.setZero(); dz.setZero(); + coordinate_wise_variances.setOnes(); } Float distance(const Vector u, const Vector& v) diff --git a/include/mutation.hpp b/include/mutation.hpp index 94ee08f..4553391 100644 --- a/include/mutation.hpp +++ b/include/mutation.hpp @@ -29,12 +29,12 @@ namespace mutation { Float init_threshold = 0.1; Float decay_factor = 0.995; - virtual Vector scale(const Vector& zi, const Float diameter, const size_t budget, const size_t evaluations); + virtual Vector scale(const Vector &zi, const Float diameter, const size_t budget, const size_t evaluations); }; struct NoThresholdConvergence : ThresholdConvergence { - Vector scale(const Vector& zi, const Float diameter, const size_t budget, const size_t evaluations) override + Vector scale(const Vector &zi, const Float diameter, const size_t budget, const size_t evaluations) override { return zi; } @@ -46,41 +46,38 @@ namespace mutation size_t seq_cutoff; public: - SequentialSelection(const parameters::Mirror& m, const size_t mu, const Float seq_cutoff_factor = 1.0) : seq_cutoff_factor(m == parameters::Mirror::PAIRWISE ? std::max(Float{ 2. }, seq_cutoff_factor) : seq_cutoff_factor), - seq_cutoff(static_cast(mu* seq_cutoff_factor)) - {} - virtual bool break_conditions(const size_t i, const Float f, Float fopt, const parameters::Mirror& m); + SequentialSelection(const parameters::Mirror &m, const size_t mu, const Float seq_cutoff_factor = 1.0) : seq_cutoff_factor(m == parameters::Mirror::PAIRWISE ? std::max(Float{2.}, seq_cutoff_factor) : seq_cutoff_factor), + seq_cutoff(static_cast(mu * seq_cutoff_factor)) + { + } + virtual bool break_conditions(const size_t i, const Float f, Float fopt, const parameters::Mirror &m); }; struct NoSequentialSelection : SequentialSelection { using SequentialSelection::SequentialSelection; - bool break_conditions(const size_t i, const Float f, Float fopt, const parameters::Mirror& m) override { return false; } + bool break_conditions(const size_t i, const Float f, Float fopt, const parameters::Mirror &m) override { return false; } }; struct SigmaSampler { sampling::GaussianTransformer sampler; - SigmaSampler(const Float d) : sampler{ std::make_shared(1) } - {} - - virtual void sample(const Float sigma, Population& pop, const Float tau) + SigmaSampler(const Float d) : sampler{std::make_shared(d)} { - sampler.sampler->d = pop.s.rows(); - pop.s.noalias() = (sigma * (tau * sampler().array()).exp()).matrix().eval(); } + + void apply_integer_bounds(parameters::Parameters &p); + + virtual void sample(const Float sigma, parameters::Parameters &p); }; struct NoSigmaSampler : SigmaSampler { using SigmaSampler::SigmaSampler; - void sample(const Float sigma, Population& pop, const Float tau) override - { - pop.s.setConstant(sigma); - } + void sample(const Float sigma, parameters::Parameters &p) override; }; struct Strategy @@ -92,24 +89,25 @@ namespace mutation Float s = 0; Strategy( - const std::shared_ptr& threshold_covergence, - const std::shared_ptr& sequential_selection, - const std::shared_ptr& sigma_sampler, + const std::shared_ptr &threshold_covergence, + const std::shared_ptr &sequential_selection, + const std::shared_ptr &sigma_sampler, const Float sigma0) : tc(threshold_covergence), sq(sequential_selection), ss(sigma_sampler), sigma(sigma0) - {} + { + } - virtual void mutate(FunctionType& objective, const size_t n_offspring, parameters::Parameters& p); + virtual void mutate(FunctionType &objective, const size_t n_offspring, parameters::Parameters &p); - virtual void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) {}; + virtual void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) {}; }; struct CSA : Strategy { using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; struct TPA : Strategy @@ -120,18 +118,18 @@ namespace mutation Float b_tpa = 0.0; Float rank_tpa = 0.0; - void mutate(FunctionType& objective, const size_t n_offspring, parameters::Parameters& p) override; + void mutate(FunctionType &objective, const size_t n_offspring, parameters::Parameters &p) override; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; struct MSR : Strategy { using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; struct PSR : Strategy @@ -142,65 +140,60 @@ namespace mutation std::vector idx; std::vector oidx; - using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; struct XNES : Strategy { using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; struct MXNES : Strategy { using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; struct LPXNES : Strategy { using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; - struct SR : Strategy { constexpr static Float tgt_success_ratio = 2.0 / 11.0; using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; }; - struct SA : Strategy { using Strategy::Strategy; - void adapt(const parameters::Weights& w, std::shared_ptr adaptation, Population& pop, - const Population& old_pop, const parameters::Stats& stats, const size_t lambda) override; + void adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, + const Population &old_pop, const parameters::Stats &stats, const size_t lambda) override; - void mutate(FunctionType& objective, const size_t n_offspring, parameters::Parameters& p) override; + void mutate(FunctionType &objective, const size_t n_offspring, parameters::Parameters &p) override; private: Float mean_sigma; }; - - - std::shared_ptr get(const parameters::Modules& m, const size_t mu, const Float d, const Float sigma); + std::shared_ptr get(const parameters::Modules &m, const size_t mu, const Float d, const Float sigma); } \ No newline at end of file diff --git a/include/parameters.hpp b/include/parameters.hpp index fd625db..2c140b5 100644 --- a/include/parameters.hpp +++ b/include/parameters.hpp @@ -12,6 +12,7 @@ #include "weights.hpp" #include "repelling.hpp" #include "center_placement.hpp" +#include "mapping.hpp" namespace parameters { @@ -39,7 +40,8 @@ namespace parameters std::shared_ptr bounds; std::shared_ptr repelling; std::shared_ptr center_placement; - + std::shared_ptr coordinate_mapping; + Parameters(const size_t dim); Parameters(const Settings &settings); diff --git a/include/population.hpp b/include/population.hpp index f6f068a..9a1f1e8 100644 --- a/include/population.hpp +++ b/include/population.hpp @@ -5,33 +5,33 @@ struct Population { Matrix X; + Matrix X_transformed; Matrix Z; Matrix Y; + Matrix S; Vector f; - Vector s; Vector t; size_t d; size_t n; Population(const size_t d, const size_t n) - : X(d, n), Z(d, n), Y(d, n), f(Vector::Constant(n, std::numeric_limits::infinity())), s(n), t(n), d(d), n(n) {} + : X(d, n), X_transformed(d, n), Z(d, n), Y(d, n), S(d, n), f(Vector::Constant(n, std::numeric_limits::infinity())), t(n), d(d), n(n) {} - Population(const Matrix &X, const Matrix &Z, const Matrix &Y, const Vector &f, const Vector &s) - : X(X), Z(Z), Y(Y), f(f), s(s), t(f.rows()), d(X.rows()), n(X.cols()) {} + Population(const Matrix &X, const Matrix &Z, const Matrix &Y, const Vector &f, const Matrix &S) + : X(X), X_transformed(X), Z(Z), Y(Y), S(S), f(f), t(f.rows()), d(X.rows()), n(X.cols()) {} Population() : Population(0, 0) {} void sort(); - Population& operator+=(const Population& other); + Population &operator+=(const Population &other); void resize_cols(const size_t size); - void keep_only(const std::vector& idx); + void keep_only(const std::vector &idx); size_t n_finite() const; }; - std::ostream &operator<<(std::ostream &os, const Population &dt); \ No newline at end of file diff --git a/include/settings.hpp b/include/settings.hpp index ed36cf9..4c34f5d 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -22,6 +22,7 @@ namespace parameters Vector lb; Vector ub; + Indices integer_variables; Vector db; Vector center; Float diameter; @@ -49,6 +50,7 @@ namespace parameters std::optional x0 = std::nullopt, std::optional lb = std::nullopt, std::optional ub = std::nullopt, + std::optional integer_variables = std::nullopt, std::optional cs = std::nullopt, std::optional cc = std::nullopt, std::optional cmu = std::nullopt, @@ -68,6 +70,7 @@ namespace parameters x0(x0), lb(lb.value_or(Vector::Ones(dim) * -5)), ub(ub.value_or(Vector::Ones(dim) * 5)), + integer_variables(integer_variables.value_or(Indices{})), db(this->ub - this->lb), center(this->lb + (db * 0.5)), diameter(db.norm()), diff --git a/include/weights.hpp b/include/weights.hpp index 966f998..5d5f76e 100644 --- a/include/weights.hpp +++ b/include/weights.hpp @@ -17,6 +17,7 @@ namespace parameters Float expected_length_z; Float expected_length_ps; Float beta; + Float int_lb_sigma; Weights(const size_t dim, const size_t mu, const size_t lambda, const Settings &settings, const Float expected_length_z); diff --git a/scripts/matrix/get_data.py b/scripts/matrix/get_data.py index e0860e1..4a1f546 100644 --- a/scripts/matrix/get_data.py +++ b/scripts/matrix/get_data.py @@ -1,7 +1,7 @@ from time import perf_counter import warnings import numpy as np -import modcma.c_maes as modcma +import modcma.c_maes as ccma import ioh import pandas as pd @@ -43,7 +43,7 @@ def run_modma(problem: ioh.ProblemType, # modules.ssa = modcma.options.StepSizeAdaptation.CSA # modules.restart_strategy = modcma.options.RestartStrategy.STOP - settings = modcma.Settings( + settings = ccma.Settings( problem.meta_data.n_variables, x0=x0, modules=modules, @@ -55,7 +55,7 @@ def run_modma(problem: ioh.ProblemType, budget=problem.meta_data.n_variables * BUDGET, ) - cma = modcma.ModularCMAES(settings) + cma = ccma.ModularCMAES(settings) start = perf_counter() while not cma.break_conditions(): if cma.p.criteria.any(): @@ -71,14 +71,14 @@ def run_modma(problem: ioh.ProblemType, class RestartCollector: - def __init__(self, strategy = modcma.options.RestartStrategy.STOP): - modules = modcma.parameters.Modules() + def __init__(self, strategy = ccma.options.RestartStrategy.STOP): + modules = ccma.parameters.Modules() modules.restart_strategy = strategy - settings = modcma.Settings( + settings = ccma.Settings( 2, modules=modules, ) - cma = modcma.ModularCMAES(settings) + cma = ccma.ModularCMAES(settings) self.names = [x.name for x in cma.p.criteria.items] self.reset() @@ -106,7 +106,7 @@ def collect(name, module_name, option): problem.attach_logger(logger) runs = [] for i in range(N_REPEATS): - modcma.utils.set_seed(21 + fid * d * i) + ccma.utils.set_seed(21 + fid * d * i) collector.reset() run_modma(problem, np.zeros(d), collector, module_name, option) # print(name, fid, d, problem.state.current_best_internal.y, problem.state.evaluations) @@ -116,13 +116,13 @@ def collect(name, module_name, option): def make_modules(module_name, option): - modules = modcma.parameters.Modules() - modules.restart_strategy = modcma.options.RestartStrategy.STOP + modules = ccma.parameters.Modules() + modules.restart_strategy = ccma.options.RestartStrategy.STOP setattr(modules, module_name, option) return modules def collect_modcma(): - options = modcma.options.MatrixAdaptationType.__members__ + options = ccma.options.MatrixAdaptationType.__members__ del options['COVARIANCE_NO_EIGV'] with Pool(6) as p: @@ -139,7 +139,7 @@ def run_pycma(problem: ioh.ProblemType, x0: np.ndarray): options['maxfevals'] = problem.meta_data.n_variables * BUDGET cma = pycma.CMAEvolutionStrategy(x0, 2.0, options=options) - settings = modcma.Settings(problem.meta_data.n_variables) + settings = ccma.Settings(problem.meta_data.n_variables) assert settings.lambda0 == cma.sp.popsize start = perf_counter() @@ -191,5 +191,5 @@ def collect_pycma(): # p2.join() # collect_modcma() - mods = modcma.parameters.Modules() + mods = ccma.parameters.Modules() \ No newline at end of file diff --git a/scripts/matrix/test_bbob5d.py b/scripts/matrix/test_bbob5d.py index 9a2a963..7c53a45 100644 --- a/scripts/matrix/test_bbob5d.py +++ b/scripts/matrix/test_bbob5d.py @@ -1,6 +1,6 @@ from time import perf_counter import ioh -import modcma.c_maes as modcma +import modcma.c_maes as ccma import iohinspector as ins import matplotlib.colors as mcolors import numpy as np @@ -18,10 +18,10 @@ def inner(*args, **kwargs): @timeit def run_modma(f: ioh.ProblemType, dim: int, n_evaluations): - modules = modcma.parameters.Modules() + modules = ccma.parameters.Modules() # modules.restart_strategy = modcma.options.RestartStrategy.IPOP # modules.active = True - settings = modcma.Settings( + settings = ccma.Settings( dim, budget=n_evaluations, target=f.optimum.y + 1e-8, @@ -31,7 +31,7 @@ def run_modma(f: ioh.ProblemType, dim: int, n_evaluations): modules=modules, verbose=False ) - cma = modcma.ModularCMAES(settings) + cma = ccma.ModularCMAES(settings) cma.run(f) return cma diff --git a/scripts/matrix/test_timing.py b/scripts/matrix/test_timing.py index 59e6519..b23a3e8 100644 --- a/scripts/matrix/test_timing.py +++ b/scripts/matrix/test_timing.py @@ -9,7 +9,7 @@ import numpy as np from modcma import ModularCMAES -import modcma.c_maes as modcma +import modcma.c_maes as ccma import cma as pycma import ioh from fcmaes import optimizer, retry diff --git a/scripts/matrix/test_timing_modules.py b/scripts/matrix/test_timing_modules.py index 92e6cce..64e21ff 100644 --- a/scripts/matrix/test_timing_modules.py +++ b/scripts/matrix/test_timing_modules.py @@ -2,7 +2,7 @@ import warnings import numpy as np -import modcma.c_maes as modcma +import modcma.c_maes as ccma import ioh import pandas as pd import matplotlib.pyplot as plt @@ -12,10 +12,10 @@ np.random.seed(12) -def run_modma(problem: ioh.ProblemType, x0: np.ndarray, matrix_adaptation = modcma.options.COVARIANCE, max_generations=1000): - modules = modcma.parameters.Modules() +def run_modma(problem: ioh.ProblemType, x0: np.ndarray, matrix_adaptation = ccma.options.COVARIANCE, max_generations=1000): + modules = ccma.parameters.Modules() modules.matrix_adaptation = matrix_adaptation - settings = modcma.Settings( + settings = ccma.Settings( problem.meta_data.n_variables, x0=x0, modules=modules, @@ -25,7 +25,7 @@ def run_modma(problem: ioh.ProblemType, x0: np.ndarray, matrix_adaptation = modc max_generations=max_generations ) - cma = modcma.ModularCMAES(settings) + cma = ccma.ModularCMAES(settings) start = perf_counter() cma.run(problem) @@ -45,7 +45,7 @@ def run_pycma(problem: ioh.ProblemType, x0: np.ndarray, max_generations=1000): pprint(options) cma = pycma.CMAEvolutionStrategy(x0, 2.0, options=options) - settings = modcma.Settings(problem.meta_data.n_variables) + settings = ccma.Settings(problem.meta_data.n_variables) assert settings.lambda0 == cma.sp.popsize np.random.seed(1) start = perf_counter() @@ -64,7 +64,7 @@ def collect(): dims = 2, 3, 5, 10, 20, 40, 100, 200, 500, 1000 n_repeats = 15 - options = modcma.options.MatrixAdaptationType.__members__ + options = ccma.options.MatrixAdaptationType.__members__ del options['COVARIANCE_NO_EIGV'] pprint(options) diff --git a/setup.py b/setup.py index 7fc541e..11d943c 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ with open("README.md", "r", encoding="Latin-1") as fh: long_description = fh.read() -__version__ = "1.1.0" +__version__ = "1.2.0" if platform.system() in ("Linux", "Darwin"): os.environ["CC"] = "g++" diff --git a/src/bounds.cpp b/src/bounds.cpp index 8bcf2e5..5a14417 100644 --- a/src/bounds.cpp +++ b/src/bounds.cpp @@ -28,7 +28,6 @@ namespace bounds Vector BoundCorrection::delta_out_of_bounds(const Vector &xi, const Mask &oob, const parameters::Settings &settings) const { return (oob).select((xi - settings.lb).cwiseQuotient(settings.db), xi); - ; } void BoundCorrection::correct(const Eigen::Index i, parameters::Parameters &p) @@ -44,7 +43,8 @@ namespace bounds return; p.pop.X.col(i) = correct_x(p.pop.X.col(i), oob, p.mutation->sigma, p.settings); - p.pop.Y.col(i) = p.adaptation->invert_x(p.pop.X.col(i), p.pop.s(i)); + p.pop.X_transformed.col(i) = p.pop.X.col(i); + p.pop.Y.col(i) = p.adaptation->invert_x(p.pop.X.col(i), p.pop.S.col(i)); p.pop.Z.col(i) = p.adaptation->invert_y(p.pop.Y.col(i)); } } diff --git a/src/interface.cpp b/src/interface.cpp index 934327b..9428eb6 100644 --- a/src/interface.cpp +++ b/src/interface.cpp @@ -6,12 +6,11 @@ #include #include #include -#include +#include #include "c_maes.hpp" #include "to_string.hpp" #include "es.hpp" - namespace py = pybind11; PYBIND11_MAKE_OPAQUE(restart::vCriteria); @@ -341,6 +340,7 @@ void define_matrix_adaptation(py::module &main) .def_readwrite("ps", &Adaptation::ps) .def_readwrite("dz", &Adaptation::dz) .def_readwrite("dd", &Adaptation::dd) + .def_readwrite("coordinate_wise_variances", &Adaptation::coordinate_wise_variances) .def_readwrite("expected_length_z", &Adaptation::expected_length_z) .def("adapt_evolution_paths", &Adaptation::adapt_evolution_paths, py::arg("pop"), @@ -604,7 +604,7 @@ void define_parameters(py::module &main) py::class_>(m, "Settings") .def(py::init, std::optional, size_to, size_to, std::optional, std::optional, std::optional, std::optional, - std::optional, std::optional, + std::optional, std::optional, std::optional, std::optional, std::optional, std::optional, std::optional, std::optional, std::optional, bool, bool>(), @@ -619,6 +619,7 @@ void define_parameters(py::module &main) py::arg("x0") = std::nullopt, py::arg("lb") = std::nullopt, py::arg("ub") = std::nullopt, + py::arg("integer_variables") = std::nullopt, py::arg("cs") = std::nullopt, py::arg("cc") = std::nullopt, py::arg("cmu") = std::nullopt, @@ -626,8 +627,7 @@ void define_parameters(py::module &main) py::arg("damps") = std::nullopt, py::arg("acov") = std::nullopt, py::arg("verbose") = false, - py::arg("always_compute_eigv") = false - ) + py::arg("always_compute_eigv") = false) .def_readonly("dim", &Settings::dim) .def_readonly("modules", &Settings::modules) .def_readwrite("target", &Settings::target) @@ -668,6 +668,7 @@ void define_parameters(py::module &main) ss << " x0: " << to_string(settings.x0); ss << " lb: " << settings.lb.transpose(); ss << " ub: " << settings.ub.transpose(); + ss << " integer_variables: " << settings.integer_variables.transpose(); ss << " cs: " << to_string(settings.cs); ss << " cc: " << to_string(settings.cc); ss << " cmu: " << to_string(settings.cmu); @@ -688,6 +689,10 @@ void define_parameters(py::module &main) std::shared_ptr, std::shared_ptr>; + py::class_>(m, "CoordinateMapping") + .def(py::init()) + .def("transform", &mapping::CoordinateMapping::transform, py::arg("x")); + py::class_>(main, "Parameters") .def(py::init(), py::arg("dimension")) .def(py::init(), py::arg("settings")) @@ -738,7 +743,8 @@ void define_parameters(py::module &main) .def_readwrite("restart_strategy", &Parameters::restart_strategy) .def_readwrite("repelling", &Parameters::repelling) .def_readwrite("bounds", &Parameters::bounds) - .def_readwrite("center_placement", &Parameters::center_placement); + .def_readwrite("center_placement", &Parameters::center_placement) + .def_readwrite("coordinate_mapping", &Parameters::coordinate_mapping); } void define_bounds(py::module &main) @@ -811,7 +817,8 @@ void define_mutation(py::module &main) py::class_>(m, "SigmaSampler") .def(py::init(), py::arg("dimension")) - .def("sample", &SigmaSampler::sample, py::arg("sigma"), py::arg("population"), py::arg("tau")); + .def("apply_integer_bounds", &SigmaSampler::apply_integer_bounds) + .def("sample", &SigmaSampler::sample, py::arg("sigma"), py::arg("parameters")); py::class_>(m, "NoSigmaSampler") .def(py::init(), py::arg("dimension")); @@ -864,17 +871,18 @@ void define_population(py::module &main) { py::class_(main, "Population") .def(py::init(), py::arg("dimension"), py::arg("n")) - .def(py::init(), py::arg("X"), py::arg("Z"), py::arg("Y"), py::arg("f"), py::arg("s")) + .def(py::init(), py::arg("X"), py::arg("Z"), py::arg("Y"), py::arg("f"), py::arg("S")) .def("sort", &Population::sort) .def("resize_cols", &Population::resize_cols, py::arg("size")) .def("keep_only", &Population::keep_only, py::arg("idx")) .def_property_readonly("n_finite", &Population::n_finite) .def("__add__", &Population::operator+=, py::arg("other")) .def_readwrite("X", &Population::X) + .def_readwrite("X_transformed", &Population::X_transformed) .def_readwrite("Z", &Population::Z) .def_readwrite("Y", &Population::Y) + .def_readwrite("S", &Population::S) .def_readwrite("f", &Population::f) - .def_readwrite("s", &Population::s) .def_readwrite("d", &Population::d) .def_readwrite("n", &Population::n) .def_readwrite("t", &Population::t); diff --git a/src/mapping.cpp b/src/mapping.cpp new file mode 100644 index 0000000..0d1f0fc --- /dev/null +++ b/src/mapping.cpp @@ -0,0 +1,8 @@ +#include "mapping.hpp" +#include "parameters.hpp" + +namespace mapping +{ + + +} \ No newline at end of file diff --git a/src/matrix_adaptation.cpp b/src/matrix_adaptation.cpp index 9d4509c..eff9b06 100644 --- a/src/matrix_adaptation.cpp +++ b/src/matrix_adaptation.cpp @@ -5,9 +5,9 @@ namespace matrix_adaptation using namespace parameters; - Vector Adaptation::invert_x(const Vector& xi, const Float sigma) + Vector Adaptation::invert_x(const Vector& xi, const Vector sigma) { - return (xi - m) / sigma; + return ((xi - m).array() / sigma.array()).matrix(); } static void one_plus_one_path_update( @@ -31,7 +31,7 @@ namespace matrix_adaptation void Adaptation::adapt_evolution_paths(const Population& pop, const Weights& w, const Stats& stats, const parameters::Settings& settings, const size_t lambda, const size_t mu) { - const auto sigma = pop.s.mean(); + const auto sigma = pop.S.mean(); dm = (m - m_old) / sigma; dz = pop.Z.leftCols(mu) * w.positive.head(mu); adapt_evolution_paths_inner(pop, w, stats, settings, mu, lambda); @@ -106,6 +106,7 @@ namespace matrix_adaptation C = old_c + rank_one + rank_mu; C = 0.5 * (C + C.transpose().eval()); + coordinate_wise_variances.noalias() = C.diagonal(); } bool CovarianceAdaptation::perform_eigendecomposition(const Settings& settings) @@ -136,6 +137,7 @@ namespace matrix_adaptation d.noalias() = d.cwiseSqrt().eval(); inv_root_C.noalias() = B * d.cwiseInverse().asDiagonal() * B.transpose(); A.noalias() = B * d.asDiagonal(); + return true; } @@ -220,6 +222,7 @@ namespace matrix_adaptation c(j) = (decay_c * c(j)) + (w.c1 * pow(pc(j), 2)) + (w.cmu * rank_mu); c(j) = std::max(c(j), Float{ 1e-12 }); d(j) = std::sqrt(c(j)); + coordinate_wise_variances(j) = c(j); } return true; @@ -288,6 +291,8 @@ namespace matrix_adaptation + ((popY * (tau_m * weights).asDiagonal()) * (popZ.transpose() * M_inv)); else outdated_M_inv = true; // Rely on moore penrose pseudo-inv (only when needed) + + coordinate_wise_variances.noalias() = (M.array().square().rowwise().sum()).matrix(); return true; } @@ -345,7 +350,7 @@ namespace matrix_adaptation for (size_t i = 0; i < pop.Y.cols() - mu; i++) Eigen::internal::llt_rank_update_lower(A, pop.Y.col(mu + i), w.cmu * w.negative(i)); - + coordinate_wise_variances.noalias() = (A.array().square().rowwise().sum()).matrix(); return true; } @@ -387,6 +392,7 @@ namespace matrix_adaptation C = (1.0 - tc_inv) * C + (tc_inv * Y); C = 0.5 * (C + C.transpose().eval()); + coordinate_wise_variances.noalias() = C.diagonal(); const Eigen::LLT chol(C); if (chol.info() != Eigen::Success) @@ -397,7 +403,7 @@ namespace matrix_adaptation return false; } A = chol.matrixL(); - + return true; } @@ -508,6 +514,7 @@ namespace matrix_adaptation A *= (w.cc * G).exp(); outdated_A_inv = true; + coordinate_wise_variances.noalias() = (A.array().square().rowwise().sum()).matrix(); return true; } diff --git a/src/mutation.cpp b/src/mutation.cpp index c5fcd45..6861dfd 100644 --- a/src/mutation.cpp +++ b/src/mutation.cpp @@ -20,9 +20,38 @@ namespace mutation return (f < fopt) and (i >= seq_cutoff) and (m != parameters::Mirror::PAIRWISE or i % 2 == 0); } + void SigmaSampler::apply_integer_bounds(parameters::Parameters &p) + { + const Array &Cdiag = p.adaptation->coordinate_wise_variances; + for (const auto &iidx : p.settings.integer_variables) + { + const Float Cii = std::max(Cdiag[iidx], Float(1e-16)); + const Float lb_sigma = p.weights.int_lb_sigma / std::sqrt(Cii); + + for (size_t i = 0; i < p.pop.n; i++) + p.pop.S(iidx, i) = std::max(p.pop.S(iidx, i), lb_sigma); + } + } + + void SigmaSampler::sample(const Float sigma, parameters::Parameters &p) + { + for (size_t i = 0; i < p.pop.n; i++) + { + const auto z = sampler(); + p.pop.S.col(i) = (sigma * (p.weights.beta * z.array()).exp()).matrix(); + } + apply_integer_bounds(p); + } + + void NoSigmaSampler::sample(const Float sigma, parameters::Parameters &p) + { + p.pop.S.setConstant(sigma); + apply_integer_bounds(p); + } + void Strategy::mutate(FunctionType &objective, const size_t n_offspring, parameters::Parameters &p) { - ss->sample(sigma, p.pop, p.weights.beta); + ss->sample(sigma, p); p.bounds->n_out_of_bounds = 0; p.repelling->prepare_sampling(p); @@ -37,14 +66,17 @@ namespace mutation zi, p.settings.diameter, p.settings.budget, p.stats.evaluations); p.pop.Z.col(i).noalias() = zi_scaled; p.pop.Y.col(i).noalias() = p.adaptation->compute_y(p.pop.Z.col(i)); - p.pop.X.col(i).noalias() = p.pop.Y.col(i) * p.pop.s(i) + p.adaptation->m; + p.pop.X.col(i).array() = p.pop.Y.col(i).array() * p.pop.S.col(i).array() + p.adaptation->m.array(); p.bounds->correct(i, p); + } while ( (p.settings.modules.bound_correction == parameters::CorrectionMethod::RESAMPLE && n_rej++ < 5 * p.settings.dim && p.bounds->is_out_of_bounds(p.pop.X.col(i), p.settings).any()) || p.repelling->is_rejected(p.pop.X.col(i), p)); - p.pop.f(i) = objective(p.pop.X.col(i)); + p.pop.X_transformed.col(i) = p.coordinate_mapping->transform(p.pop.X.col(i)); + + p.pop.f(i) = objective(p.pop.X_transformed.col(i)); p.stats.evaluations++; if (sq->break_conditions(i, p.pop.f(i), p.stats.global_best.y, p.settings.modules.mirrored)) { @@ -69,8 +101,12 @@ namespace mutation { Strategy::mutate(objective, n_offspring_, p); - const auto f_pos = objective(p.adaptation->m + (p.mutation->sigma * p.adaptation->dm)); - const auto f_neg = objective(p.adaptation->m + (p.mutation->sigma * -p.adaptation->dm)); + const auto x_pos = p.coordinate_mapping->transform(p.adaptation->m + (p.mutation->sigma * p.adaptation->dm)); + const auto x_neg = p.coordinate_mapping->transform(p.adaptation->m + (p.mutation->sigma * -p.adaptation->dm)); + const auto f_pos = objective(x_pos); + const auto f_neg = objective(x_neg); + p.stats.update_best(x_pos, f_pos); + p.stats.update_best(x_neg, f_neg); p.stats.evaluations += 2; this->rank_tpa = f_neg < f_pos ? -a_tpa : a_tpa + b_tpa; } @@ -131,13 +167,14 @@ namespace mutation combined.head(n) = pop.f.head(n); combined.tail(n) = old_pop.f.head(n); - if (idx.size() != n) { + if (idx.size() != n) + { idx.resize(n); oidx.resize(n); std::iota(idx.begin(), idx.end(), 0); std::iota(oidx.begin(), oidx.end(), 0); } - + utils::sort_index_inplace(combined, idx); utils::sort_index_inplace(idx, oidx); @@ -182,7 +219,9 @@ namespace mutation Population &pop, const Population &old_pop, const parameters::Stats &stats, const size_t lambda) { - const Float rel_log = (pop.s.array() / sigma).log().matrix().dot(w.clipped()); + const auto logS = (pop.S.array() / sigma).log(); + const Vector per_sample = logS.colwise().mean().transpose(); + const Float rel_log = per_sample.dot(w.clipped()); sigma *= std::exp(w.cs * rel_log); } @@ -196,15 +235,17 @@ namespace mutation void SA::mutate(FunctionType &objective, const size_t n_offspring, parameters::Parameters &p) { Strategy::mutate(objective, n_offspring, p); - mean_sigma = std::exp(p.pop.s.array().log().mean()); + mean_sigma = std::exp(p.pop.S.array().log().mean()); } void SA::adapt(const parameters::Weights &w, std::shared_ptr adaptation, Population &pop, const Population &old_pop, const parameters::Stats &stats, const size_t lambda) { - const auto &sigma_l = pop.s.topRows(w.positive.rows()); - sigma = std::exp((w.positive.array() * sigma_l.array().log()).sum()) / mean_sigma; + const auto &sigma_l = pop.S.topRows(w.positive.rows()); + const auto &log_sigma_dim = sigma_l.array().log().rowwise().mean(); + const Float log_sigma = (w.positive.array() * log_sigma_dim).sum(); + sigma = std::exp(log_sigma) / mean_sigma; } std::shared_ptr get(const parameters::Modules &m, const size_t mu, const Float d, const Float sigma) diff --git a/src/parameters.cpp b/src/parameters.cpp index 92f8bc3..02748c9 100644 --- a/src/parameters.cpp +++ b/src/parameters.cpp @@ -30,7 +30,8 @@ namespace parameters settings.budget)), bounds(bounds::get(settings.modules.bound_correction, settings.dim)), repelling(repelling::get(settings.modules)), - center_placement(center::get(settings.modules.center_placement)) + center_placement(center::get(settings.modules.center_placement)), + coordinate_mapping(mapping::get(settings.integer_variables, settings.dim, weights.mueff)) { criteria.reset(*this); } @@ -51,7 +52,8 @@ namespace parameters } stats.solutions.push_back(stats.current_best); stats.evaluations++; - stats.centers.emplace_back(adaptation->m, objective(adaptation->m), stats.t - 1, stats.evaluations); + const auto mean = coordinate_mapping->transform(adaptation->m); + stats.centers.emplace_back(mean, objective(mean), stats.t - 1, stats.evaluations); stats.update_best(stats.centers.back().x, stats.centers.back().y); stats.has_improved = false; repelling->update_archive(objective, *this); @@ -80,7 +82,6 @@ namespace parameters mutation->sigma = std::min(std::max(mutation->sigma, restart::MinSigma::tolerance), restart::MaxSigma::tolerance); successfull_adaptation = adaptation->adapt_matrix(weights, settings.modules, pop, mu, settings, stats); - criteria.update(*this); stats.t++; } diff --git a/src/population.cpp b/src/population.cpp index 6475a62..6fa679a 100644 --- a/src/population.cpp +++ b/src/population.cpp @@ -4,20 +4,22 @@ void Population::sort() { const auto idx = utils::sort_indexes(f); X = X(Eigen::all, idx).eval(); + X_transformed = X_transformed(Eigen::all, idx).eval(); Z = Z(Eigen::all, idx).eval(); Y = Y(Eigen::all, idx).eval(); + S = S(Eigen::all, idx).eval(); f = f(idx).eval(); - s = s(idx).eval(); t = t(idx).eval(); } Population& Population::operator+=(const Population& other) { utils::hstack(X, other.X); + utils::hstack(X_transformed, other.X_transformed); utils::hstack(Y, other.Y); utils::hstack(Z, other.Z); + utils::hstack(S, other.S); utils::concat(f, other.f); - utils::concat(s, other.s); utils::concat(t, other.t); n += other.n; return *this; @@ -27,10 +29,11 @@ void Population::resize_cols(const size_t size) { n = std::min(size, static_cast(X.cols())); X.conservativeResize(d, n); + X_transformed.conservativeResize(d, n); Y.conservativeResize(d, n); Z.conservativeResize(d, n); + S.conservativeResize(d, n); f.conservativeResize(n); - s.conservativeResize(n); t.conservativeResize(n); } @@ -38,10 +41,11 @@ void Population::resize_cols(const size_t size) void Population::keep_only(const std::vector& idx) { X = X(Eigen::all, idx).eval(); + X_transformed = X_transformed(Eigen::all, idx).eval(); Z = Z(Eigen::all, idx).eval(); Y = Y(Eigen::all, idx).eval(); + S = S(Eigen::all, idx).eval(); f = f(idx).eval(); - s = s(idx).eval(); t = t(idx).eval(); n = idx.size(); } @@ -56,7 +60,7 @@ std::ostream& operator<<(std::ostream& os, const Population& p) os << "Population" << "\nx=\n" - << p.X + << p.X_transformed << "\ny=\n" << p.Y << "\nf=\n" diff --git a/src/repelling.cpp b/src/repelling.cpp index 8ef48ac..0a3a5b4 100644 --- a/src/repelling.cpp +++ b/src/repelling.cpp @@ -33,7 +33,7 @@ namespace repelling for (size_t k = 1; k < n_evals + 1; k++) { const Float a = static_cast(k) / (static_cast(n_evals) + 1.0); - const Vector x = v.x + a * (u.x - v.x); + const Vector x = p.coordinate_mapping->transform(v.x + a * (u.x - v.x)); const Float y = f(x); p.stats.evaluations++; if (max_f < y) diff --git a/src/selection.cpp b/src/selection.cpp index c27f912..567d2cd 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -22,7 +22,7 @@ namespace selection p.pop.resize_cols(p.lambda); p.stats.current_avg = p.pop.f.array().mean(); - p.stats.update_best(p.pop.X(Eigen::all, 0), p.pop.f(0)); + p.stats.update_best(p.pop.X_transformed(Eigen::all, 0), p.pop.f(0)); } void Pairwise::operator()(parameters::Parameters& p) const @@ -45,7 +45,7 @@ namespace selection { for (Eigen::Index i = 0; i < static_cast(p.old_pop.n); i++) { - p.old_pop.Y.col(i).noalias() = p.adaptation->invert_x(p.old_pop.X.col(i), p.old_pop.s(i)); + p.old_pop.Y.col(i).noalias() = p.adaptation->invert_x(p.old_pop.X.col(i), p.old_pop.S.col(i)); p.old_pop.Z.col(i).noalias() = p.adaptation->invert_y(p.old_pop.Y.col(i)); } } diff --git a/src/weights.cpp b/src/weights.cpp index 53a5422..91913ea 100644 --- a/src/weights.cpp +++ b/src/weights.cpp @@ -162,6 +162,8 @@ namespace parameters beta = 1.0 / std::sqrt(2.0 * mueff); if (settings.modules.ssa == StepSizeAdaptation::LPXNES) beta = std::log(2.0) / (std::sqrt(d) * std::log(d)); + + int_lb_sigma = std::min(0.2, mueff / d); } diff --git a/tests/test_c_bounds.py b/tests/test_c_bounds.py index bc224b4..72700de 100644 --- a/tests/test_c_bounds.py +++ b/tests/test_c_bounds.py @@ -27,13 +27,14 @@ class TestBounds(unittest.TestCase): def setUp(self): self.lb, self.ub = np.zeros(2), np.ones(2) * 2 self.par = Parameters(parameters.Settings(2, lambda0=2, lb=self.lb, ub=self.ub)) - self.par.pop.s = np.ones(2) * 2 + self.par.pop.S = np.ones((2, 2)) * 2 self.par.adaptation.m = np.ones(2) * 0.1 Z = np.ones((2, 2)) * 0.9 Z[0, 0] *= 2 self.par.pop.Z = Z self.par.pop.Y = self.par.pop.Z.copy() - self.par.pop.X = self.par.adaptation.m + (self.par.pop.s * self.par.pop.Y) + self.par.pop.X_transformed = self.par.pop.X = self.par.adaptation.m + (self.par.pop.S * self.par.pop.Y) + def test_bound_fixers(self): for i, (boundcntrl, option) in enumerate(zip(self.__bound_fixers, self.__bound_fixers_options)): @@ -47,7 +48,6 @@ def test_bound_fixers(self): self.assertEqual(method.n_out_of_bounds, 0) method.correct(0, self.par) self.assertEqual(method.n_out_of_bounds, 1) - self.assertTrue(np.all(self.par.pop.X <= 2)) self.assertTrue(np.all(np.isclose(self.par.pop.Y.ravel()[1:], 0.9))) self.assertTrue(np.all(np.isclose(self.par.pop.X.ravel()[1:], 1.9))) diff --git a/tests/test_c_criteria.py b/tests/test_c_criteria.py index 29df4cf..22b0373 100644 --- a/tests/test_c_criteria.py +++ b/tests/test_c_criteria.py @@ -2,16 +2,16 @@ import unittest -import modcma.c_maes as modcma +import modcma.c_maes as ccma -class MyCriterion(modcma.restart.Criterion): +class MyCriterion(ccma.restart.Criterion): def __init__(self, name): super().__init__(f"MyCriterion{name}") - def on_reset(self, par: modcma.Parameters): + def on_reset(self, par: ccma.Parameters): """Called when a restart happens (also at the start)""" - def update(self, par: modcma.Parameters): + def update(self, par: ccma.Parameters): """Called after each iteration, needs to modify self.met""" self.met = True @@ -19,15 +19,15 @@ def update(self, par: modcma.Parameters): class TestCriteria(unittest.TestCase): def setUp(self): - mod = modcma.parameters.Modules() - mod.restart_strategy = modcma.options.RestartStrategy.RESTART - settings = modcma.Settings( + mod = ccma.parameters.Modules() + mod.restart_strategy = ccma.options.RestartStrategy.RESTART + settings = ccma.Settings( dim=3, budget=1000, verbose=True, modules=mod ) - self.cma = modcma.ModularCMAES(settings) + self.cma = ccma.ModularCMAES(settings) def test_modify(self): self.cma.p.criteria.items = self.cma.p.criteria.items[1:3] diff --git a/tests/test_c_integer.py b/tests/test_c_integer.py new file mode 100644 index 0000000..e71d6c4 --- /dev/null +++ b/tests/test_c_integer.py @@ -0,0 +1,32 @@ +import unittest +from modcma import c_maes +import numpy as np + +def sphere(x): + return sum(round(xi + 7)**2 for xi in x) + + +class TestInteger(unittest.TestCase): + def test_int(self): + dim = 10 + settings = c_maes.settings_from_dict( + dim, + integer_variables=list(range(dim)), + target=0, + lb=[-50] * dim, + ub=[50] * dim, + # active=True, + # lambda0=1, + sample_transformation='LAPLACE', + ssa="SA" + ) + cma = c_maes.ModularCMAES(settings) + c_maes.utils.set_seed(100) + while not cma.break_conditions(): + cma.mutate(sphere) + cma.select() + cma.recombine() + cma.adapt() + + self.assertEqual(cma.p.stats.global_best.y, 0.0) + diff --git a/tests/test_c_mutation.py b/tests/test_c_mutation.py index a73a79c..45bcfeb 100644 --- a/tests/test_c_mutation.py +++ b/tests/test_c_mutation.py @@ -15,37 +15,41 @@ class TestMutation(unittest.TestCase): def setUp(self): - self.pop = Population(2, 2) - self.pop.Z = np.ones((2, 2)) * 0.5 + + pop = Population(2, 2) + pop.Z = np.ones((2, 2)) * 0.5 + self.par = Parameters(2) + self.par.pop = pop + self.par.weights.beta = 1 def test_sigma_sampler(self): ss = mutation.SigmaSampler(2) noss = mutation.NoSigmaSampler(2) - ss.sample(2.0, self.pop, 1) - self.assertFalse(np.all(self.pop.s == 2.0)) - noss.sample(2.0, self.pop, 1) - self.assertTrue(np.all(self.pop.s == 2.0)) + ss.sample(2.0, self.par) + self.assertFalse(np.all(self.par.pop.S == 2.0)) + noss.sample(2.0, self.par) + self.assertTrue(np.all(self.par.pop.S == 2.0)) def test_threshold_convergence(self): tc = mutation.ThresholdConvergence() notc = mutation.NoThresholdConvergence() - notc.scale(self.pop.Z[0], 10, 100, 2) - self.assertTrue(np.all(self.pop.Z == 0.5)) + notc.scale(self.par.pop.Z[0], 10, 100, 2) + self.assertTrue(np.all(self.par.pop.Z == 0.5)) budget = 100 evals = 2 diam = 10 t = tc.init_threshold * diam * pow((budget - evals) / budget, tc.decay_factor) - norm = np.linalg.norm(self.pop.Z, axis=0) + norm = np.linalg.norm(self.par.pop.Z, axis=0) Z = np.ones((2, 2)) - Z[0] = tc.scale(self.pop.Z[0], diam, budget, evals) - Z[1] = tc.scale(self.pop.Z[1], diam, budget, evals) - self.pop.Z = Z + Z[0] = tc.scale(self.par.pop.Z[0], diam, budget, evals) + Z[1] = tc.scale(self.par.pop.Z[1], diam, budget, evals) + self.par.pop.Z = Z expected_z = 0.5 * ((t + (t - norm)) / norm) - self.assertTrue(np.isclose(self.pop.Z, expected_z).all()) - self.assertTrue(np.isclose(self.pop.Z, scale_with_threshold(np.ones(2) * .5, t)).all()) + self.assertTrue(np.isclose(self.par.pop.Z, expected_z).all()) + self.assertTrue(np.isclose(self.par.pop.Z, scale_with_threshold(np.ones(2) * .5, t)).all()) def get_cma(self, ssa, adapt_sigma=True): modules = parameters.Modules() @@ -113,8 +117,11 @@ def test_adapt_xnes(self): def test_adapt_lpxnes(self): cma = self.get_cma(options.StepSizeAdaptation.LPXNES) + w = cma.p.weights.weights.clip(0)[: cma.p.pop.n] - z = np.exp(cma.p.weights.cs * (w @ np.log(cma.p.pop.s))) + + z = np.exp(cma.p.weights.cs * (w @ np.log(cma.p.pop.S).mean(axis=0))) + sigma = np.power(cma.p.settings.sigma0, 1 - cma.p.weights.cs) * z self.assertTrue(np.isclose(cma.p.mutation.sigma, sigma)) diff --git a/tests/test_c_population.py b/tests/test_c_population.py index 125caa7..ee1e61d 100644 --- a/tests/test_c_population.py +++ b/tests/test_c_population.py @@ -27,20 +27,19 @@ def set_params(self): self.Y = np.dot(self.B, self.D * self.Z) self.X = self.xmean + (self._sigma * self.Y) self.f = np.array([sum(i) for i in self.X.T]) - self.s = np.ones(self._lambda) * self._sigma - self.pop = Population(self.X, self.Z, self.Y, self.f, self.s) + self.S = np.ones((self._dim, self._lambda)) * self._sigma + self.pop = Population(self.X, self.Z, self.Y, self.f, self.S) def test_sort(self): """Test sorting behaviour.""" self.pop.sort() rank = np.argsort(self.f) - for e in ("X", "Y", "Z",): + for e in ("X", "Y", "Z", "S"): self.assertListEqual( getattr(self, e)[:, rank].tolist(), getattr(self.pop, e).tolist() ) self.assertListEqual(self.f[rank].tolist(), self.pop.f.tolist()) - self.assertListEqual(self.s[rank].tolist(), self.pop.s.tolist()) def test_keeponly(self): """Test keeponly behaviour.""" @@ -54,8 +53,8 @@ def test_resize(self): self.pop.resize_cols(2) self.assertEqual(self.pop.n, 2) self.assertEqual(self.pop.f.size, 2) - self.assertEqual(self.pop.s.size, 2) self.assertEqual(self.pop.X.shape[1], 2) + self.assertEqual(self.pop.S.shape[1], 2) self.assertEqual(self.pop.Y.shape[1], 2) self.assertEqual(self.pop.Z.shape[1], 2) @@ -67,7 +66,7 @@ def test_n_finite(self): def test_add(self): """Test addition.""" - self.pop = self.pop + Population(self.X, self.Z, self.Y, self.f, self.s) + self.pop = self.pop + Population(self.X, self.Z, self.Y, self.f, self.S) self.assertEqual( self.pop.X.shape, ( diff --git a/tests/test_c_tuning.py b/tests/test_c_tuning.py index 79e1d9c..32810d7 100644 --- a/tests/test_c_tuning.py +++ b/tests/test_c_tuning.py @@ -43,7 +43,7 @@ def test_from_dict(self): def test_fmin(self): - xopt, fopt, evals, es = c_maes.fmin(sphere, [1, 2], 0.2, 100, active=True) + xopt, fopt, evals, es = c_maes.fmin(sphere, [1, 2], 0.2, 100, active=True, matrix_adaptation='NONE') self.assertLess(fopt, 1e-4) self.assertLessEqual(evals, 100) self.assertEqual(sphere(xopt), fopt)