diff --git a/README.md b/README.md index 4acb02f..c91ac81 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,13 @@ This README provides a high level overview of the implemented modules, and provi - [Python Installation](#python-installation) - [Installation from source](#installation-from-source) - [Usage ](#usage-) - - [Python-only ](#python-only-) - - [Ask-Tell Interface ](#ask-tell-interface-) - [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-) @@ -31,6 +35,9 @@ This README provides a high level overview of the implemented modules, and provi - [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-) @@ -83,167 +90,300 @@ If you want to work on a development version of the library, you should follow t ## Usage -To optimize a single function, we provide a basic fmin interface, which requires two parameters: `func`, which is the function to be minimized, and `x0`, the initial estimate for the function. Optionally, any parameter that is valid for the `ModularCMAES` class, is valid for this function as keyword argument. For example, to minimize the value of the sum function, in 4D with a budget of 1000 function evaluations, using an active CMA-ES with an intial stepsize $\sigma$ of 2.5, we could use the following: +> **Note:** +> The **C++ backend** is the primary and recommended interface for ModularCMAES. +> The **pure Python implementation** remains available for reference and educational purposes, +> but is **no longer actively developed** and may not include all new module options or performance improvements. -```python -from modcma import fmin -xopt, fopt, used_budget = fmin(func=sum, x0=[1, 2, 3, 4], budget=1000, active=True, sigma0=2.5) -``` +### C++ Backend -### Python-only +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. -The Python-only implentation revolves around the `ModularCMAES` class. The class has a `run` method, which runs the specified algorithm until any break conditions arise. +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 -from modcma import ModularCMAES +from modcma import c_maes -def func(x: np.ndarray): - return sum(x) +# Instantiate module configuration +modules = c_maes.parameters.Modules() +modules.active = True +modules.matrix_adaptation = c_maes.parameters.MatrixAdaptationType.MATRIX -dim = 10 -budget = 10_000 +# Create Settings and Parameters objects +settings = c_maes.parameters.Settings(dim=10, modules=modules, sigma0=2.5) +parameters = c_maes.Parameters(settings) -# Create an instance of the CMA-ES (no modules active) -cma = ModularCMAES(func, dim, budget=budget) +# Instantiate the optimizer +cma = c_maes.ModularCMAES(parameters) -# Run until break conditions are met -cma = cma.run() +# Define objective function +def func(x): + return sum(x**2) + +# Run the optimization +cma.run(func) ``` -Alternatively, we could also iteratively run the `step` method, for a more fine grained control on how the algorithm is executed. +The API provides fine-grained control over the optimization process. Instead of calling `run`, you can explicitly step through the algorithm: ```python -cma = ModularCMAES(func, dim, budget=budget) - while not cma.break_conditions(): - cma.step() + cma.step(func) ``` -At an even lower level, we could run all methods ran by the `step` methods seperately, which are (in order) `mutate`, `select`, `recombine` and `adapt`. The following snippet shows an example of all three methods. +Or execute the internal components of each iteration separately: ```python -cma = ModularCMAES(func, dim, budget=budget) - while not cma.break_conditions(): - cma.mutate() - cma.select() - cma.recombine() - cma.adapt() + cma.mutate(func) + cma.select() + cma.recombine() + cma.adapt() ``` -### Ask-Tell Interface +This modularity allows experimentation with specific parts of the evolution strategy, +such as custom selection, recombination, or adaptation routines. + +#### High Level interface -Often, it can be usefull consider the algorithm in an Ask-Tell fashion, such that we can sequentally evaluate points while having outside control of the objective function. For this purpose, we provide the `AskTellCMAES` interface, which can be used as follows: +In addition to the fully specified method described above, we can also call the optimizer via a more friendly `fmin` interface: ```python -from modcma import AskTellCMAES +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. -# Instantiate an ask-tell cmaes. Note that the objective function argument is omitted here. -# All other parameters, e.g. the active modules can be passed by keyword, similar to ModularCMAES -cma = AskTellCMAES(dim, budget=budget, active=True) +--- -while not cma.break_conditions(): - # Retrieve a single new candidate solution - xi = cma.ask() - # Evaluate the objective function - fi = func(xi) - # Update the algorithm with the objective function value - cma.tell(xi, fi) +#### 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). + +#### Configuration Space Generation + +The configuration space is automatically derived from the available modules and tunable parameters via the function: + +```python +from modcma.cmaescpp import get_configspace ``` -### C++ Backend +Usage: + +```python +from modcma.cmaescpp import get_configspace + +# Create a configuration space for a 10-dimensional problem +cs = get_configspace(dim=10) +``` + +This function returns a `ConfigSpace.ConfigurationSpace` object containing: -For obvious performance reasons, we've also implemented the algorithm in C++, with an interface to Python. The algorithm can be accessed similarly in Python, but calling it is slightly more verbose. The `ModularCMAES` class in C++ accepts a single argument, which is an `Parameters` object. This object must be instantiated with a `Settings` object, which in turn is built from the problem dimension and a `Modules` object, which can be used to specify certain module options. A boilerplate code example for this process is given in the following: +- **Categorical parameters** for all available modules (e.g., mirrored sampling, restart strategy, bound correction). +- **Numeric parameters** for key internal strategy settings such as `lambda0`, `mu0`, `sigma0`, `cs`, `cc`, `cmu`, `c1`, and `damps`. +- A built-in constraint ensuring `mu0 ≤ lambda0`. +- Optionally, the configuration space can include **only module-level options** by setting `only_modules=True`. + +Example: + +```python +# Get only the module configuration space (no numeric parameters) +cs_modules = get_configspace(add_popsize=False, add_sigma=False, add_learning_rates=False) +``` + +#### Creating Settings from a Configuration + +Once a configuration has been selected—either manually or from a tuner—the library provides a simple interface to construct a corresponding `Settings` object: + +```python +from modcma.cmaescpp import settings_from_config +from ConfigSpace import Configuration + +# Sample or load a configuration +config = cs.sample_configuration() + +# Or for defaults +default = cs.default_configuration() + +# The config can be edited +config['sampler'] = 'HALTON' + +# Convert the configuration to a Settings object +# Note that keyword arguments like lb in the next example, can be passed to settings like so +settings = settings_from_config(dim=10, config=config, lb=np.ones(10)) +``` + +The resulting `Settings` object can then be passed directly to the C++ backend: ```python -# import the c++ subpackage from modcma import c_maes -# Instantate a modules object -modules = c_maes.parameters.Modules() -# Create a settings object, here also optional parameters such as sigma0 can be specified -settings = c_maes.parameters.Settings(dim, modules, sigma0 = 2.5) -# Create a parameters object + parameters = c_maes.Parameters(settings) -# Pass the parameters object to the ModularCMAES optimizer class cma = c_maes.ModularCMAES(parameters) +cma.run(func) ``` -Then, the API for both the Python-only and C++ interface is mostly similar, and a single run of the algorithm can be performed by using the `run` function. A difference is that now the objective function is a parameter of the run function, and not pass when the class is instantiated. +--- + +### Python-only (Legacy) + +> **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. + +The Python interface provides a simple API and includes a convenience `fmin` function for optimizing a single objective function in one call: ```python -cma.run(func) +from modcma import fmin +xopt, fopt, used_budget = fmin(func=sum, x0=[1, 2, 3, 4], budget=1000, active=True, sigma0=2.5) ``` -Similarly, the `step` function is also directly exposed: +The main class is `ModularCMAES`, which mimics the structure of the C++ version but runs entirely in Python: + +```python +from modcma import ModularCMAES +import numpy as np + +def func(x: np.ndarray): + return sum(x) + +dim = 10 +budget = 10_000 + +# Instantiate and run +cma = ModularCMAES(func, dim, budget=budget) +cma = cma.run() +``` + +You can also run the algorithm step by step: ```python while not cma.break_conditions(): - cma.step(func) + cma.step() ``` -Or by calling the function in the `step` seperately: +Or explicitly call each internal phase — analogous to the C++ interface: + +```python +while not cma.break_conditions(): + cma.mutate() + cma.select() + cma.recombine() + cma.adapt() +``` + +--- + +#### Ask–Tell Interface + +The **Ask–Tell interface** is only available in the **Python implementation**. It provides an alternative interaction model where function evaluations are managed externally. This is particularly useful for **parallel**, **asynchronous**, or **expensive** objective evaluations, +where you want to control when and how points are evaluated. + +The optimizer generates candidate solutions via `ask()`, and their objective values are later supplied with `tell()`. ```python +from modcma import AskTellCMAES + +def func(x): + return sum(x**2) + +# Instantiate an ask-tell CMA-ES optimizer +# Note: the objective function is not passed at construction +cma = AskTellCMAES(dim=10, budget=10_000, active=True) + while not cma.break_conditions(): - cma.mutate(func) - cma.select() - cma.recombine() - cma.adapt() + # Get a candidate solution + xi = cma.ask() + # Evaluate externally + fi = func(xi) + # Report the result back + cma.tell(xi, fi) ``` +This design provides flexibility when the evaluation process is nontrivial, +such as when objectives are computed through simulations, APIs, or distributed systems. +However, note that this interface is **not available** in the C++ backend. + ## Modules The CMA-ES Modular package provides various modules, grouped into 13 categories. For each of these categories a given option can be selected, which can be arbitrarly combined. The following table lists the categories and the available options. Not all modules are available in both versions (i.e. some are only implemented in C++), an overview is given in the table. By default, the first option in the table is selected for a given category. Boolean modules, i.e. modules that only can be turned on or off are turned off by default.
- -| Category | Option | Python | C++ | -| -------- | ------ | ------ | ---- | -| [Matrix Adaptation](#matrix-adaptation) | Covariance | :green_circle: | :green_circle: | -| | Matrix | :red_circle: | :green_circle: | -| | Separable | :red_circle: | :green_circle: | -| | None | :red_circle: | :green_circle: | -| [Active Update](#active-update) | Off/On | :green_circle: | :green_circle: | -| [Elitism](#elitism) | Off/On | :green_circle: | :green_circle: | -| [Orthogonal Sampling](#orthogonal-sampling) | Off/On | :green_circle: | :green_circle: | -| [Sequential Selection](#sequential-selection) | Off/On | :green_circle: | :green_circle: | -| [Threshold Convergence](#threshold-convergence) | Off/On | :green_circle: | :green_circle: | -| [Sample Sigma](#sample-sigma) | Off/On | :green_circle: | :green_circle: | -| [Base Sampler](#base-sampler) | Gaussian | :green_circle: | :green_circle: | -| | Sobol | :green_circle: | :green_circle: | -| | Halton | :green_circle: | :green_circle: | -| [Recombination Weights](#recombination-weights) | Default | :green_circle: | :green_circle: | -| | Equal | :green_circle: | :green_circle: | -| | $1/2^\lambda$ | :green_circle: | :green_circle: | -| [Mirrored Sampling](#mirrored-sampling) | Off | :green_circle: | :green_circle: | -| | On | :green_circle: | :green_circle: | -| | Pairwise | :green_circle: | :green_circle: | - - - -| Category | Option | Python | C++ | -| -------- | ------ | ------ | ---- | -| [Step size adaptation](#step-size-adaptation) | CSA | :green_circle: | :green_circle: | -| | TPA | :green_circle: | :green_circle: | -| | MSR | :green_circle: | :green_circle: | -| | PSR | :green_circle: | :green_circle: | -| | XNES | :green_circle: | :green_circle: | -| | MXNES | :green_circle: | :green_circle: | -| | MPXNES | :green_circle: | :green_circle: | -| [Restart Strategy](#restart-strategy) | Off | :green_circle: | :green_circle: | -| | Restart | :green_circle: | :green_circle: | -| | IPOP | :green_circle: | :green_circle: | -| | BIPOP | :green_circle: | :green_circle: | -| [Bound Correction](#bound-correction) | Off | :green_circle: | :green_circle: | -| | Saturate | :green_circle: | :green_circle: | -| | Mirror | :green_circle: | :green_circle: | -| | COTN | :green_circle: | :green_circle: | -| | Toroidal | :green_circle: | :green_circle: | -| | Uniform resample | :green_circle: | :green_circle: | +| Category | Option | Python | C++ | +|---|---|---|---| +| [Matrix Adaptation](#matrix-adaptation) | COVARIANCE | :green_circle: | :green_circle: | +| | MATRIX | :red_circle: | :green_circle: | +| | SEPARABLE | :red_circle: | :green_circle: | +| | NONE | :red_circle: | :green_circle: | +| | CHOLESKY | :red_circle: | :green_circle: | +| | CMSA | :red_circle: | :green_circle: | +| | NATURAL_GRADIENT | :red_circle: | :green_circle: | +| [Active Update](#active-update) | Off/On | :green_circle: | :green_circle: | +| [Elitism](#elitism) | Off/On | :green_circle: | :green_circle: | +| [Orthogonal Sampling](#orthogonal-sampling) | Off/On | :green_circle: | :green_circle: | +| [Sequential Selection](#sequential-selection) | Off/On | :green_circle: | :green_circle: | +| [Threshold Convergence](#threshold-convergence) | Off/On | :green_circle: | :green_circle: | +| [Sample Sigma](#sample-sigma) | Off/On | :green_circle: | :green_circle: | +| [Base Sampler](#base-sampler) | GAUSSIAN | :green_circle: | :red_circle: *(use Sample Transformer = GAUSSIAN)* | +| | SOBOL | :green_circle: | :green_circle: | +| | HALTON | :green_circle: | :green_circle: | +| | UNIFORM | :red_circle: | :green_circle: | +| [Sample Transformer](#sample-transformer) | GAUSSIAN | :red_circle: | :green_circle: | +| | SCALED_UNIFORM | :red_circle: | :green_circle: | +| | LAPLACE | :red_circle: | :green_circle: | +| | LOGISTIC | :red_circle: | :green_circle: | +| | CAUCHY | :red_circle: | :green_circle: | +| | DOUBLE_WEIBULL | :red_circle: | :green_circle: | +| [Recombination Weights](#recombination-weights) | DEFAULT | :green_circle: | :green_circle: | +| | EQUAL | :green_circle: | :green_circle: | +| | 1/2^λ | :green_circle: | :red_circle: *(use EXPONENTIAL instead)* | +| | EXPONENTIAL | :red_circle: | :green_circle: | +| [Mirrored Sampling](#mirrored-sampling) | NONE | :green_circle: | :green_circle: | +| | MIRRORED | :green_circle: | :green_circle: | +| | PAIRWISE | :green_circle: | :green_circle: | +| [Step size adaptation](#step-size-adaptation) | CSA | :green_circle: | :green_circle: | +| | TPA | :green_circle: | :green_circle: | +| | MSR | :green_circle: | :green_circle: | +| | XNES | :green_circle: | :green_circle: | +| | MXNES | :green_circle: | :green_circle: | +| | LPXNES | :green_circle: | :green_circle: | +| | PSR | :green_circle: | :green_circle: | +| | SR | :red_circle: | :green_circle: | +| | SA | :red_circle: | :green_circle: | +| [Restart Strategy](#restart-strategy) | NONE | :green_circle: | :green_circle: | +| | RESTART | :green_circle: | :green_circle: | +| | IPOP | :green_circle: | :green_circle: | +| | BIPOP | :green_circle: | :green_circle: | +| | STOP | :red_circle: | :green_circle: | +| [Bound correction](#bound-correction) | NONE | :green_circle: | :green_circle: | +| | SATURATE | :green_circle: | :green_circle: | +| | MIRROR | :green_circle: | :green_circle: | +| | COTN | :green_circle: | :green_circle: | +| | TOROIDAL | :green_circle: | :green_circle: | +| | UNIFORM_RESAMPLE | :green_circle: | :green_circle: | +| | RESAMPLE | :red_circle: | :green_circle: | +| [Repelling Restart](#repelling-restart) | Off/On | :red_circle: | :green_circle: | +| [Center Placement](#center-placement) | X0 | :red_circle: | :green_circle: | +| | ZERO | :red_circle: | :green_circle: | +| | UNIFORM | :red_circle: | :green_circle: | +| | CENTER | :red_circle: | :green_circle: |
+ +**Notes** +- In C++, `BaseSampler` generates uniform points in \[0,1)^d; the actual search-space distribution is controlled by `SampleTranformerType` (e.g., GAUSSIAN, CAUCHY). +- Python’s “1/2^λ” recombination weights correspond to C++’s `EXPONENTIAL`. +- New C++-only modules: `repelling_restart` (bool), `center_placement` (enum), and extended `ssa`, `bound_correction`, and sampling options. + ### Matrix Adaptation The ModularCMAES can be turned into an implementation of the (fast)-MA-ES algortihm by changing the `matrix_adaptation` option from `COVARIANCE` to `MATRIX` in the `Modules` object. This is currently only available in the C++ version of the framework. An example of specifying this, using the required `MatrixAdaptationType` enum: @@ -285,11 +425,6 @@ modules.active = True When this option is selected, (𝜇 + 𝜆)-selection instead of (𝜇, 𝜆)-selection is enabled. This can be usefull to speed up convergence on unimodal problems, but can have a negative impact on population diversity. -For the Python only version, this can be enabled by passing the option `elitist=True`: - -```python -cma = ModularCMAES(func, dim, elitist=True) -``` For the C++ version, this can be done by setting the appropriate value in the `Modules` object: @@ -298,6 +433,12 @@ For the C++ version, this can be done by setting the appropriate value in the `M modules.elitist = True ``` +For the Python only version, this can be enabled by passing the option `elitist=True`: + +```python +cma = ModularCMAES(func, dim, elitist=True) +``` + ### Orthogonal Sampling Orthogonal Sampling was introduced by Wang et al. as an extension of Mirrored Sampling. This method improves sampling by ensuring that the newly sampled points in the population are orthonormalized using a Gram-Schmidt procedure. @@ -429,7 +570,7 @@ modules.weights = c_maes.options.RecombinationWeights.DEFAULT # or modules.weights = c_maes.options.RecombinationWeights.EQUAL # or -modules.weights = c_maes.options.RecombinationWeights.HALF_POWER_LAMBDA +modules.weights = c_maes.options.RecombinationWeights.EXPONENTIAL ``` ### Mirrored Sampling @@ -541,6 +682,23 @@ modules.bound_correction = c_maes.options.CorrectionMethod.NONE # or modules.bound_correction = c_maes.options.CorrectionMethod.SATURATE ``` + +### Sample Transformer + +Controls the transformation from a base uniform sampler in \[0,1)^d to a target search-space distribution (e.g., `GAUSSIAN, CAUCHY, LAPLACE`, …) in the C++ backend. See the [paper](https://dl.acm.org/doi/10.1145/3712256.3726479). By default, this performs a transform to a standard Gaussian. + +### Repelling Restart + +C++-only boolean module to bias restarts away from previously explored regions (helpful in multimodal settings). See the [paper](https://link.springer.com/chapter/10.1007/978-3-031-70068-2_17) + +### Center Placement + +C++-only enum controlling the initial center of mass of the sampling distribution (`X0, ZERO, UNIFORM, CENTER`) on restart, iniatiated by a `restart_strategy`. The options are: + - `X0` sets the intial center to x0 on every restart + - `ZERO` set the intial center to the all-zero vector + - `UNIFORM` picks a uniform random location inside the search space + - `CENTER` sets the center to the precise center of the search space. + - ## Citation diff --git a/modcma/c_maes/__init__.py b/modcma/c_maes/__init__.py index 6404c8b..8818374 100644 --- a/modcma/c_maes/__init__.py +++ b/modcma/c_maes/__init__.py @@ -1,19 +1,216 @@ +import warnings +from enum import Enum +from typing import Union + +import numpy as np +from ConfigSpace import ( + ConfigurationSpace, + Configuration, + Categorical, + CategoricalHyperparameter, + ForbiddenGreaterThanRelation, + UniformIntegerHyperparameter, + NormalFloatHyperparameter, + EqualsCondition, +) + from .cmaescpp import ( constants, - utils, - sampling, - mutation, - selection, - parameters, - bounds, - restart, - options, - repelling, + utils, # pyright: ignore[reportMissingModuleSource] + sampling, # pyright: ignore[reportMissingModuleSource] + mutation, # pyright: ignore[reportMissingModuleSource] + selection, # pyright: ignore[reportMissingModuleSource] + parameters, # pyright: ignore[reportMissingModuleSource] + bounds, # pyright: ignore[reportMissingModuleSource] + restart, # pyright: ignore[reportMissingModuleSource] + options, # pyright: ignore[reportMissingModuleSource] + repelling, # pyright: ignore[reportMissingModuleSource] Population, Parameters, ModularCMAES, - center, - es + center, # pyright: ignore[reportMissingModuleSource] + es, # pyright: ignore[reportMissingModuleSource] ) -from .cmaescpp.parameters import Settings \ No newline at end of file +from .cmaescpp.parameters import ( # pyright: ignore[reportMissingModuleSource] + Settings, + Modules, +) # pyright: ignore[reportMissingModuleSource] + + +def _get_module_options(name: str) -> tuple: + if not hasattr(Modules, name): + raise NameError(f"Modules has no member {name}") + + default_value = getattr(Modules(), name) + if isinstance(default_value, bool): + return (default_value, not default_value) + + module_class = default_value.__class__ + if issubclass(module_class, Enum): + other_values = [ + x.name for x in module_class.__members__.values() + if x is not default_value + ] + return tuple([default_value.name] + other_values) + raise TypeError(f"{name} has a unparsable type {type(default_value)}") + + +def get_all_module_options() -> dict: + return { + name: _get_module_options(name) + for name in dir(Modules) + if not name.startswith("_") + } + + +def _make_numeric_parameter( + name: str, dim: int, lb: float, ub: float +) -> Union[UniformIntegerHyperparameter, NormalFloatHyperparameter]: + + settings = Parameters(Settings(dim)) + default = getattr(settings.weights, name, None) + if default is None: + default = getattr(settings.settings, name) + + if isinstance(default, int): + return UniformIntegerHyperparameter(name, lb, ub, default, log=True) + + elif isinstance(default, float): + db = min(default - lb, ub - default) + return NormalFloatHyperparameter(name, default, 0.3 * db, lb, ub) + + raise TypeError( + f"default value for {name} ({default}) " + f"has an unparsable type {type(default)}" + ) + + +def get_configspace( + dim: int = None, + add_popsize: bool = True, + add_sigma: bool = True, + add_learning_rates: bool = True +) -> ConfigurationSpace: + cspace = ConfigurationSpace() + for name, options in get_all_module_options().items(): + cspace.add(Categorical(name, options, default=options[0])) + + if dim is None and (add_popsize or add_sigma or add_learning_rates): + warnings.warn( + "Filling configspace with default numeric values for dim=2, " + "since no dim was provided and only_modules was set to False" + ) + dim = 2 + + if add_popsize: + cspace.add(_make_numeric_parameter("lambda0", dim, 1, 50 * dim)) + cspace.add(_make_numeric_parameter("mu0", dim, 1, 50 * dim)) + cspace.add(ForbiddenGreaterThanRelation(cspace["mu0"], cspace["lambda0"])) + + if add_sigma: + cspace.add(_make_numeric_parameter("sigma0", dim, 1e-15, 1e15)) + + if add_learning_rates: + cspace.add(_make_numeric_parameter("cs", dim, 0, 1.0)) + cspace.add(_make_numeric_parameter("cc", dim, 0, 1.0)) + cspace.add(_make_numeric_parameter("cmu", dim, 0, 1.0)) + cspace.add(_make_numeric_parameter("c1", dim, 0, 1.0)) + cspace.add(_make_numeric_parameter("damps", dim, 0, 10.0)) + + return cspace + + +def set_module(modules: Modules, name: str, value: Enum) -> bool: + if hasattr(modules, name): + attr_class = type(getattr(modules, name)) + if issubclass(attr_class, Enum): + value = getattr(attr_class, value) + setattr(modules, name, value) + return True + return False + + +def settings_from_dict(dim: int, **config: dict) -> Settings: + modules = Modules() + via_settings = {} + for name, value in dict(config).items(): + if set_module(modules, name, value): + continue + via_settings[name] = value + settings = Settings(dim, modules, **via_settings) + return settings + + +def settings_from_config( + dim: int, + config: Configuration, + **kwargs +) -> Settings: + via_settings = kwargs + default_config = get_configspace(dim).get_default_configuration() + modules = Modules() + for name, value in dict(config).items(): + if set_module(modules, name, value): + continue + if default_config[name] != value: + via_settings[name] = value + + settings = Settings(dim, modules, **via_settings) + return settings + +def fmin(func: callable, x0: np.ndarray, sigma0: float, budget: int, **kwargs): + """Minimize a function using the modular CMA-ES. + + Parameters + ---------- + func: callable + The objective function to be minimized. + x0 np.ndarray: + The first solution estimate + sigma0: float + The estimate of the stepsize (rule of thumb: 0.3 * (ub - lb)) + budget: int + Maximum number of function evaluations to make. + **kwargs + These are directly passed into the instance of ModularCMAES, + in this manner parameters can be specified for the optimizer. + Returns + ------- + xopt + The variables which minimize the function during this run + fopt + The value of function at found xopt + evals + The number of evaluations performed + es + The ModularCMAES instance + """ + settings = settings_from_dict(len(x0), sigma0=sigma0, budget=budget, **kwargs) + es = ModularCMAES(settings) + es(func) + + return es.p.stats.global_best.x, es.p.stats.global_best.y, es.p.stats.evaluations, es + +__all__ = ( + "settings_from_config", + "get_configspace", + "get_all_module_options", + "Settings", + "Modules", + "constants", + "utils", + "sampling", + "mutation", + "selection", + "parameters", + "bounds", + "restart", + "options", + "repelling", + "Population", + "Parameters", + "ModularCMAES", + "center", + "es", +) diff --git a/modcma/c_maes/cmaescpp/options.pyi b/modcma/c_maes/cmaescpp/options.pyi index 2de7091..038bd4a 100644 --- a/modcma/c_maes/cmaescpp/options.pyi +++ b/modcma/c_maes/cmaescpp/options.pyi @@ -75,6 +75,7 @@ class SampleTranformerType: class CenterPlacement: __members__: ClassVar[dict] = ... # read-only UNIFORM: ClassVar[CenterPlacement] = ... + CENTER: ClassVar[CenterPlacement] = ... X0: ClassVar[CenterPlacement] = ... ZERO: ClassVar[CenterPlacement] = ... __entries: ClassVar[dict] = ... diff --git a/modcma/parameters.py b/modcma/parameters.py index 492647b..3500a24 100644 --- a/modcma/parameters.py +++ b/modcma/parameters.py @@ -265,16 +265,16 @@ class Parameters(AnnotatedStruct): sequential: bool = False threshold_convergence: bool = False bound_correction: ( - None, "saturate", "unif_resample", "COTN", "toroidal", "mirror") = None + None, "saturate", "unif_resample", "COTN", "toroidal", "mirror") = None # pyright: ignore[reportInvalidTypeForm] orthogonal: bool = False - local_restart: (None, "restart", "IPOP", "BIPOP", "STOP") = None - base_sampler: ("gaussian", "sobol", "halton") = "gaussian" - mirrored: (None, "mirrored", "mirrored pairwise") = None - weights_option: ("default", "equal", "1/2^lambda") = "default" + local_restart: (None, "restart", "IPOP", "BIPOP", "STOP") = None # pyright: ignore[reportInvalidTypeForm] + base_sampler: ("gaussian", "sobol", "halton") = "gaussian" # pyright: ignore[reportInvalidTypeForm] + mirrored: (None, "mirrored", "mirrored pairwise") = None # pyright: ignore[reportInvalidTypeForm] + weights_option: ("default", "equal", "1/2^lambda") = "default" # pyright: ignore[reportInvalidTypeForm] step_size_adaptation: ( - "csa", "tpa", "msr", "xnes", "m-xnes", "lp-xnes", "psr") = "csa" - population: TypeVar("Population") = None - old_population: TypeVar("Population") = None + "csa", "tpa", "msr", "xnes", "m-xnes", "lp-xnes", "psr") = "csa" # pyright: ignore[reportInvalidTypeForm] + population: TypeVar("Population") = None # pyright: ignore[reportInvalidTypeForm] + old_population: TypeVar("Population") = None # pyright: ignore[reportInvalidTypeForm] termination_criteria: dict = {} ipop_factor: int = 2 @@ -286,8 +286,8 @@ class Parameters(AnnotatedStruct): compute_termination_criteria: bool = False sample_sigma: bool = False # TODO make this a module vectorized_fitness: bool = False - sobol: TypeVar("Sobol") = None - halton: TypeVar("Halton") = None + sobol: TypeVar("Sobol") = None # pyright: ignore[reportInvalidTypeForm] + halton: TypeVar("Halton") = None # pyright: ignore[reportInvalidTypeForm] __modules__ = ( "active", diff --git a/perf.data b/perf.data deleted file mode 100644 index 7da2c82..0000000 Binary files a/perf.data and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index cc1bf99..625f176 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [build-system] requires = [ + "configspace", + "ioh", + "numpy", + "mypy", + "pybind11", "setuptools>=42", + "scipy", "wheel", - "pybind11>=2.6.0", - "ioh>=0.3.14", - "numpy>=1.18.5, <2.0.0", - "mypy", - "pybind11>=2.6.0", - "scipy>=1.5.1", ] build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/scripts/tuning/run_smac.py b/scripts/tuning/run_smac.py new file mode 100644 index 0000000..f97fa80 --- /dev/null +++ b/scripts/tuning/run_smac.py @@ -0,0 +1,126 @@ +import os +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" + +import time +import argparse +from itertools import product +from functools import partial + +import ioh +import numpy as np +from smac import Scenario +from smac.acquisition.function import PriorAcquisitionFunction +from smac import AlgorithmConfigurationFacade, HyperparameterOptimizationFacade +from ConfigSpace import Configuration + +from modcma import c_maes + + +DATA_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "data")) + + +def calc_aoc(logger, budget, fid, iid, dim): + data = logger.data() + data1 = data["None"][fid][dim][iid][0] + fvals = [x["raw_y_best"] for x in data1.values()] + fvals = np.array(fvals) + if np.isnan(fvals).any(): + np.nan_to_num(fvals, copy=False, nan=1e8) + if len(fvals) < budget: + fvals = np.concatenate([fvals, (budget - len(fvals)) * [np.min(fvals)]]) + parts = np.log10(np.clip(fvals[:budget], 1e-8, 1e2)) + 8 + return np.mean(parts) / 10 + + +def get_bbob_performance( + config: Configuration, seed: int = 0, fid: int = 0, dim: int = 5 +): + # print(fid, seed, dim) + iid = 1 + (seed % 10) + # fid, iid = instance.split(",") + # fid = int(fid[1:]) + # iid = int(iid[:-1]) + np.random.seed(seed + iid) + c_maes.utils.set_seed(seed + iid) + BUDGET = dim * 10_000 + l3 = ioh.logger.Store( + triggers=[ioh.logger.trigger.ALWAYS], properties=[ioh.logger.property.RAWYBEST] + ) + problem = ioh.get_problem(fid, iid, dim) + problem.attach_logger(l3) + + settings = c_maes.settings_from_config( + dim, + config, + budget=BUDGET, + target= problem.optimum.y + 1e-9, + ub=problem.bounds.ub, + lb=problem.bounds.lb + ) + par = c_maes.Parameters(settings) + + try: + cma = c_maes.ModularCMAES(par) + cma.run(problem) + except Exception as e: + print( + f"Found target {problem.state.current_best.y} target, but exception ({e}), so run failed" + ) + print(config) + return [np.inf] + + auc = calc_aoc(l3, BUDGET, fid, iid, dim) + return [auc] + + +def run_smac(fid, dim, use_learning_rates): + print(f"Running SMAC with fid={fid}, lr={use_learning_rates} and d={dim}") + cma_cs = c_maes.get_configspace(dim, add_learning_rates=use_learning_rates) + + iids = range(1, 51) + if fid == "all": + fids = range(1, 25) + max_budget = 240 + else: + fids = [fid] + max_budget = 50 + + # args = list(product(fids, iids)) + # np.random.shuffle(args) + # inst_feats = {str(arg): [arg[0]] for idx, arg in enumerate(args)} + scenario = Scenario( + cma_cs, + name=str(int(time.time())) + "-" + "CMA", + deterministic=False, + n_trials=5_000, + # instances=args, + # instance_features=inst_feats, + output_directory=os.path.join( + DATA_DIR, f"BBOB_F{fid}_{dim}D_LR{use_learning_rates}" + ), + n_workers=1, + ) + pf = PriorAcquisitionFunction( + acquisition_function=AlgorithmConfigurationFacade.get_acquisition_function( + scenario + ), + decay_beta=scenario.n_trials / 10, + ) + eval_func = partial(get_bbob_performance, fid=fid, dim=dim) + intensifier = HyperparameterOptimizationFacade.get_intensifier( + scenario, max_config_calls=max_budget + ) + smac = HyperparameterOptimizationFacade( + scenario, eval_func, acquisition_function=pf, intensifier=intensifier + ) + smac.optimize() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--fid", type=int, default=1) + parser.add_argument("--dim", type=int, default=5) + parser.add_argument("--use_learning_rates", action="store_true") + args = parser.parse_args() + run_smac(args.fid, args.dim, args.use_learning_rates) diff --git a/scripts/tuning/tuning_smac.py b/scripts/tuning/tuning_smac.py new file mode 100644 index 0000000..c9d6083 --- /dev/null +++ b/scripts/tuning/tuning_smac.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +# +import os +os.environ["OMP_NUM_THREADS"] = "1" +os.environ["OPENBLAS_NUM_THREADS"] = "1" +import ioh +from ioh import logger +import sys + +from ConfigSpace import Configuration, ConfigurationSpace, ForbiddenGreaterThanRelation +from ConfigSpace import NormalFloatHyperparameter +from ConfigSpace import Normal, Integer +from ConfigSpace.hyperparameters import MultiConditionalNormalFloatHyperparameter +# from IPython.display import display +from modcma.c_maes import ( + ModularCMAES, + Parameters, + options, + parameters, + utils, + constants +) + + +from itertools import product +from functools import partial +from multiprocessing import Pool + +import numpy as np +import time + +import argparse + +def create_search_space(DIM, use_learning_rates): + # print(DIM, use_learning_rates) + BUDGET = DIM * 10_000 + default_lambda = (4 + np.floor(3 * np.log(DIM))).astype(int) + + def mueff(lambda_, mu, weight_option): + modules = parameters.Modules() + weights_mapping = { + "default": options.RecombinationWeights.DEFAULT, + "equal": options.RecombinationWeights.EQUAL, + "1/2^lambda": options.RecombinationWeights.EXPONENTIAL, + } + modules.weights = weights_mapping[weight_option] + settings = parameters.Settings(DIM, modules, budget=BUDGET, lambda0=lambda_, mu0=mu) + return Parameters(settings).weights.mueff + + def c1(lambda_, mu, weight_option): + _mueff = mueff(lambda_, mu, weight_option) + return 2 / (_mueff + pow(DIM + 1.3, 2)) + + def cmu(lambda_, mu, weight_option, c1): + _mueff = mueff(lambda_, mu, weight_option) + return min(1-c1, 2.0 * ((_mueff - 2.0 + (1.0 / _mueff)) / (pow(DIM + 2.0, 2) + (2.0 * _mueff / 2)))) + + def cc(lambda_, mu, weight_option): + _mueff = mueff(lambda_, mu, weight_option) + return (4 + (_mueff / DIM)) / (DIM + 4 + (2 * _mueff / DIM)) + + + cma_cs = ConfigurationSpace( + { + "covariance": ["covariance", "matrix", "seperable", "none", "cholesky", "cmsa", "natural_gradient"], + "active": [False, True], + "elitist": [False, True], + "orthogonal": [False, True], + "sequential": [False, True], + "threshold": [False, True], + "base_sampler": ["halton", "sobol", "gaussian"], + "weights_option": ["default", "equal", "1/2^lambda"], + "mirrored": ["nan", "mirrored", "mirrored_pairwise"], + "step_size_adaptation": ["csa", "psr", "tpa", "msr", "mxnes", "sr", "sa"], + "local_restart": ["nan", "IPOP", "BIPOP", "Restart"], + # "bound_correction": ["nan", "saturate"],# "cotn", "mirror", "toroidal", "uniform"], + "sample_transform": ["Gaussian", "Cauchy", "Uniform", "dWeibull"], #, "Laplace", "Logistic", "dWeibull", + # "repelling_restart": [False, True], + "lambda_": Integer( + 'lambda_', bounds=(1,20*DIM), distribution = Normal(default_lambda, 10), log=True + ), + "mu": Integer( + 'mu', bounds=(1,10*DIM), distribution = Normal(default_lambda // 2, 10), log=True + ), + } + ) + + + forbidden_clause = ForbiddenGreaterThanRelation(cma_cs['mu'], cma_cs['lambda_']) + cma_cs.add(forbidden_clause) + # cond = NotEqualsCondition(cma_cs['repelling_restart'], cma_cs['local_restart'], 'nan') + # cma_cs.add(cond) + + if use_learning_rates: + cs_param = NormalFloatHyperparameter( + "cs", lower=0.0, upper=1.5, default_value=0.3, mu=0.3, sigma=0.2 + ) + + # Define the conditional normal float hyperparameter + c1_param = MultiConditionalNormalFloatHyperparameter( + "c1", ["lambda_", "mu", "weights_option"], c1, + lambda lambda_, mu, weight_option: 0.1, + lower=0.0001, upper=1.0, + default_value=c1(default_lambda, default_lambda // 2, "default")) + cc_param = MultiConditionalNormalFloatHyperparameter( + "cc", + ["lambda_", "mu", "weights_option"], cc, + lambda lambda_, mu, weight_option: 0.1, + lower=0.0001, upper=1.0, default_value=cc(default_lambda, default_lambda // 2, "default")) + cmu_param = MultiConditionalNormalFloatHyperparameter("cmu", ["lambda_", "mu", "weights_option", "c1"], cmu, lambda lambda_, mu, weight_option, c1: 0.1, lower=0.0001, upper=1.0, default_value=cmu(default_lambda, default_lambda // 2, "default", c1(default_lambda, default_lambda // 2, "default"))) + + cma_cs.add([cs_param, c1_param, cc_param, cmu_param]) + return cma_cs + +def configspace_to_irace_params_txt(cs: ConfigurationSpace, filename: str, dim = 5, use_learning_rates = False, fid = 1): + """ + Generate an irace parameters txt file from a ConfigSpace object. + + :param cs: ConfigurationSpace object + :param filename: Output file path + """ + with open(filename, "w") as f: + f.write(f"fid \"--fid \" c ({fid})\n") + f.write(f"dim \"--dim \" c ({dim})\n") + f.write(f"use_learning_rates \"--use_learning_rates \" c ({use_learning_rates})\n") + for hp in list(cs.values()): + # breakpoint() + name = hp.name + if hasattr(hp, "choices"): + # Categorical + values = ", ".join(str(v) for v in hp.choices) + f.write(f"{name} \"--{name} \" c ({values})\n") + elif hasattr(hp, "lower"): + # Integer or Float + lower, upper = hp.lower, hp.upper + if "Integer" in type(hp).__name__: + type_str = "i" + else: + type_str = "r" + if hasattr(hp, "log") and hp.log: + type_str = f"{type_str},log" + + f.write(f"{name} \"--{name} \" {type_str} ({lower},{upper})\n") + + else: + # Fallback + print(f"# {name} type not recognized\n") + # Forbidden clauses + if cs.forbidden_clauses: + f.write("\n[forbidden]\n") + for fc in cs.forbidden_clauses: + if isinstance(fc, ForbiddenGreaterThanRelation): + f.write(f"{fc.left.name} > {fc.right.name}\n") + +def config_to_cma_parameters(config, dim, budget, target): + # modules first + modules = parameters.Modules() + active = bool(config.get("active")) + if config.get("active") == "True": + active = True + if config.get("active") == "False": + active = False + modules.active = active + + elitist = bool(config.get("elitist")) + if config.get("elitist") == "True": + elitist = True + if config.get("elitist") == "False": + elitist = False + modules.elitist = elitist + + if "orthogonal" in config.keys(): + orthogonal = bool(config.get("orthogonal")) + if config.get("orthogonal") == "True": + orthogonal = True + if config.get("orthogonal") == "False": + orthogonal = False + modules.orthogonal = orthogonal + + if "sigma" in config.keys(): + sigma = bool(config.get("sigma")) + if config.get("sigma") == "True": + sigma = True + if config.get("sigma") == "False": + sigma = False + modules.sample_sigma = sigma + + if "sequential" in config.keys(): + sequential = bool(config.get("sequential")) + if config.get("sequential") == "True": + sequential = True + if config.get("sequential") == "False": + sequential = False + modules.sequential_selection = sequential + + if "threshold" in config.keys(): + threshold = bool(config.get("threshold")) + if config.get("threshold") == "True": + threshold = True + if config.get("threshold") == "False": + threshold = False + modules.threshold_convergence = threshold + + if "repelling_restart" in config.keys(): + repelling_restart = bool(config.get("repelling_restart")) + if config.get("repelling_restart") == "True": + repelling_restart = True + if config.get("repelling_restart") == "False": + repelling_restart = False + modules.repelling_restart = repelling_restart + + if "bound_correction" in config.keys(): + correction_mapping = { + "cotn": options.CorrectionMethod.COTN, + "mirror": options.CorrectionMethod.MIRROR, + "nan": options.CorrectionMethod.NONE, + "saturate": options.CorrectionMethod.SATURATE, + "toroidal": options.CorrectionMethod.TOROIDAL, + "uniform": options.CorrectionMethod.UNIFORM_RESAMPLE, + } + modules.bound_correction = correction_mapping[config.get("bound_correction")] + + mirrored_mapping = { + "mirrored": options.Mirror.MIRRORED, + "nan": options.Mirror.NONE, + "mirrored_pairwise": options.Mirror.PAIRWISE, + } + modules.mirrored = mirrored_mapping[config.get("mirrored")] + + restart_strategy_mapping = { + "IPOP": options.RestartStrategy.IPOP, + "nan": options.RestartStrategy.NONE, + "BIPOP": options.RestartStrategy.BIPOP, + "Restart": options.RestartStrategy.RESTART, + } + modules.restart_strategy = restart_strategy_mapping[config.get("local_restart")] + + sampler_mapping = { + "sobol": options.BaseSampler.SOBOL, + "gaussian": options.BaseSampler.UNIFORM, + "halton": options.BaseSampler.HALTON, + } + modules.sampler = sampler_mapping[config.get("base_sampler")] + + sample_transform_mapping = { + "Gaussian": options.SampleTranformerType.GAUSSIAN, + "Cauchy": options.SampleTranformerType.CAUCHY, + "dWeibull": options.SampleTranformerType.DOUBLE_WEIBULL, + "Laplace": options.SampleTranformerType.LAPLACE, + "Logistic": options.SampleTranformerType.LOGISTIC, + "Uniform": options.SampleTranformerType.SCALED_UNIFORM, + } + modules.sample_transformation = sample_transform_mapping[config.get("sample_transform")] + + ssa_mapping = { + "csa": options.StepSizeAdaptation.CSA, + "psr": options.StepSizeAdaptation.PSR, + "lpxnes": options.StepSizeAdaptation.LPXNES, + "msr": options.StepSizeAdaptation.MSR, + "mxnes": options.StepSizeAdaptation.MXNES, + "tpa": options.StepSizeAdaptation.TPA, + "xnes": options.StepSizeAdaptation.XNES, + "sr": options.StepSizeAdaptation.SR, + "sa": options.StepSizeAdaptation.SA, + + } + + modules.ssa = ssa_mapping[config.get("step_size_adaptation")] + + weights_mapping = { + "default": options.RecombinationWeights.DEFAULT, + "equal": options.RecombinationWeights.EQUAL, + "1/2^lambda": options.RecombinationWeights.EXPONENTIAL, + } + modules.weights = weights_mapping[config.get("weights_option")] + + + covariance_mapping = { + "covariance": options.MatrixAdaptationType.COVARIANCE, + "seperable": options.MatrixAdaptationType.SEPARABLE, + "matrix": options.MatrixAdaptationType.MATRIX, + "none": options.MatrixAdaptationType.NONE, + "cholesky": options.MatrixAdaptationType.CHOLESKY, + "cmsa": options.MatrixAdaptationType.CMSA, + "natural_gradient": options.MatrixAdaptationType.NATURAL_GRADIENT, + "covariance_no_eigv": options.MatrixAdaptationType.COVARIANCE_NO_EIGV, + } + modules.matrix_adaptation = covariance_mapping[config.get("covariance")] + #TODO: BOUNDS + settings = parameters.Settings(dim, modules, budget=budget, lambda0=int(config.get("lambda_")), mu0=int(config.get("mu")), sigma0=2, #config.get("sigma0"), + c1=config.get("c1"), cc=config.get("cc"), cmu=config.get("cmu"), cs=config.get("cs"), target=target, + lb= -5 * np.ones(dim), ub=5 * np.ones(dim), verbose=False) + return Parameters(settings) + + + +def calc_aoc(problem, logger, budget, fid, iid, DIM): + data = logger.data() + data1 = data['None'][fid][DIM][iid][0] + fvals = [x['raw_y_best'] for x in data1.values()] + fvals = np.array(fvals) + if np.isnan(fvals).any(): + np.nan_to_num(fvals, copy=False, nan=1e8) + if len(fvals) < budget: + fvals = np.concatenate([fvals, (budget-len(fvals))*[np.min(fvals)]]) + parts = np.log10(np.clip(fvals[:budget], 1e-8, 1e2))+8 + return np.mean(parts)/10 + + +def get_bbob_performance(config : Configuration, instance: str, seed: int = 0, DIM = 5): + # print(instance, seed, DIM) + # print(config.get_dictionary()) + constants.use_box_muller = False + fid, iid = instance.split(",") + fid = int(fid[1:]) + iid = int(iid[:-1]) + np.random.seed(seed + iid) + utils.set_seed(seed + iid) + BUDGET = DIM * 10_000 + l3 = logger.Store(triggers=[ioh.logger.trigger.ALWAYS], properties=[ioh.logger.property.RAWYBEST]) + problem = ioh.get_problem(fid, iid, DIM) + problem.attach_logger(l3) + par = config_to_cma_parameters(config, DIM, int(BUDGET), problem.optimum.y + 1e-9) + par.bounds.lb = problem.bounds.lb + par.bounds.ub = problem.bounds.ub + par.settings.lb = problem.bounds.lb + par.settings.ub = problem.bounds.ub + if par == False: + print("Wrong mu/lambda") + return [np.inf] + cma = ModularCMAES(par) + try: + cma.run(problem) + except Exception as e: + print( + f"Found target {problem.state.current_best.y} target, but exception ({e}), so run failed" + ) + print(config) + return [np.inf] + auc = calc_aoc(problem, l3, BUDGET, fid, iid, DIM) + return [auc] #minimizing + + +# def run_smac(arg): +# from smac import Scenario +# from smac.acquisition.function import PriorAcquisitionFunction +# from smac import AlgorithmConfigurationFacade, HyperparameterOptimizationFacade + +# fid, DIM, USE_LEARNING_RATES = arg +# print(f"Running SMAC with fid={fid}, USE_LEARNING_RATES={USE_LEARNING_RATES} and DIM={DIM}") +# cma_cs = create_search_space(DIM, USE_LEARNING_RATES) +# iids = range(1, 51) +# if fid == 'all': +# fids = range(1, 25) +# min_budget = 24 +# max_budget = 240 +# else: +# fids = [fid] +# min_budget = 3 +# max_budget = 50 +# args = list(product(fids, iids)) +# np.random.shuffle(args) +# inst_feats = {str(arg): [arg[0]] for idx, arg in enumerate(args)} +# scenario = Scenario( +# cma_cs, +# name=str(int(time.time())) + "-" + "CMA", +# deterministic=False, +# n_trials=100_000, +# instances=args, +# instance_features=inst_feats, +# output_directory=f"/local/vermettendl/TuningCMA/BBOB/F{fid}_{DIM}D_LR{USE_LEARNING_RATES}", +# n_workers=1 +# ) +# pf = PriorAcquisitionFunction( +# acquisition_function=AlgorithmConfigurationFacade.get_acquisition_function(scenario), +# decay_beta=scenario.n_trials / 10, +# ) +# eval_func = partial(get_bbob_performance, DIM=DIM) +# intensifier = HyperparameterOptimizationFacade.get_intensifier(scenario, max_config_calls=max_budget) +# smac = HyperparameterOptimizationFacade(scenario, eval_func, acquisition_function = pf, +# intensifier=intensifier) +# smac.optimize() + + +def runParallelFunction(runFunction, arguments): + """ + Return the output of runFunction for each set of arguments, + making use of as much parallelization as possible on this system + + :param runFunction: The function that can be executed in parallel + :param arguments: List of tuples, where each tuple are the arguments + to pass to the function + :return: + """ + + + arguments = list(arguments) + p = Pool(min(200, len(arguments))) + results = p.map(runFunction, arguments) + p.close() + return results + +def parse_paramters(configuration_paramters): + """ + Function to parse parameters into usable dictionaries + + Parameters + ---------- + configuration_paramters: + The command line arguments, without the executables name + Notes + ----- + Returns two dicts: one for the problem-specific settings and one for the algorithm settings. + This algorithm settings contains the splitpoint, C1 as dict and C2 as dict + """ + parser = argparse.ArgumentParser(description="Run basic single switch CMA-ES configuration", + argument_default=argparse.SUPPRESS) + + + # First, extract dim and include_lr from configuration_paramters + temp_parser = argparse.ArgumentParser(add_help=False) + temp_parser.add_argument('--dim', dest='dim', type=int, required=True) + temp_parser.add_argument('--use_learning_rates', dest='use_learning_rates', type=bool, required=True, default=False) + temp_args, _ = temp_parser.parse_known_args(configuration_paramters) + dim = temp_args.dim + include_lr = temp_args.use_learning_rates + cs = create_search_space(dim, include_lr) + + # Positional paramters + parser.add_argument('configuration_id', type=str) + parser.add_argument('instance_name', type=str) + parser.add_argument('seed', type=int) + parser.add_argument('iid', type=int) + parser.add_argument('budget', type=float) + + # 'global' parameters + parser.add_argument('--fid', dest='fid', type=int, required=True) + parser.add_argument('--dim', dest='dim', type=int, required=True) + parser.add_argument('--use_learning_rates', dest='use_learning_rates', type=bool, required=True, default=False) + + param_names = [] + for hp in list(cs.values()): + if hasattr(hp, "choices"): + parser.add_argument(f"--{hp.name}", dest=f"{hp.name}", type=str) + elif hasattr(hp, "lower"): + if "Integer" in type(hp).__name__: + parser.add_argument(f"--{hp.name}", dest=f"{hp.name}", type=int) + else: + parser.add_argument(f"--{hp.name}", dest=f"{hp.name}", type=float) + param_names.append(hp.name) + + # Process into dicts + argsdict = parser.parse_args(configuration_paramters).__dict__ + alg_config = {} + for k, v in argsdict.items(): + if v in ["False", "True", "None"]: + v = eval(v) + if k in param_names: + alg_config[k] = v + problem_config = {k: argsdict[k] for k in ('fid', 'dim', 'iid', 'seed', 'budget')} + return [problem_config, alg_config] + +if __name__ == "__main__": + if sys.argv[1] == "--generate_parameters": + use_LR = sys.argv[3]=='1' + cs = create_search_space(int(sys.argv[2]), use_LR) + configspace_to_irace_params_txt(cs, sys.argv[5], int(sys.argv[2]), use_LR, int(sys.argv[4])) + sys.exit(0) + else: + problem_config, alg_config = parse_paramters(sys.argv[1:]) + + result = get_bbob_performance(alg_config, f"({problem_config['fid']}, {problem_config['iid']})", problem_config['seed'], DIM=problem_config['dim']) + print(result[0]) + if False: + fids = list(range(1, 25)) + fids.append('all') + dims = [2,3,5,10] + lrs = [True, False] + args = list(product(fids, dims, lrs)) + np.random.shuffle(args) + runParallelFunction(run_smac, args) + \ No newline at end of file diff --git a/setup.py b/setup.py index 2675001..7fc541e 100644 --- a/setup.py +++ b/setup.py @@ -58,9 +58,10 @@ ext_modules=[ext], python_requires=">=3.8", install_requires=[ + "configspace", "numpy", "scipy", - "ioh>=0.3.12,!=0.3.15" + "ioh" ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/src/c_maes.cpp b/src/c_maes.cpp index 2f26b95..36ad1b3 100644 --- a/src/c_maes.cpp +++ b/src/c_maes.cpp @@ -48,7 +48,7 @@ void ModularCMAES::operator()(FunctionType& objective) const bool ModularCMAES::break_conditions() const { const auto target_reached = p->settings.target and p->settings.target.value() >= p->stats.global_best.y; - const auto budget_used_up = p->stats.evaluations >= p->settings.budget; + const auto budget_used_up = (p->stats.evaluations + p->lambda) >= p->settings.budget; const auto exceed_gens = p->settings.max_generations and p->stats.t >= p->settings.max_generations; const auto restart_strategy_criteria = p->settings.modules.restart_strategy == parameters::RestartStrategyType::STOP and p->criteria.any(); diff --git a/src/interface.cpp b/src/interface.cpp index 5fbe178..934327b 100644 --- a/src/interface.cpp +++ b/src/interface.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "c_maes.hpp" #include "to_string.hpp" @@ -15,7 +16,6 @@ namespace py = pybind11; PYBIND11_MAKE_OPAQUE(restart::vCriteria); - template Float random_double() { @@ -23,23 +23,23 @@ Float random_double() return gen(rng::GENERATOR); } -void define_options(py::module& main) +void define_options(py::module &main) { auto m = main.def_submodule("options"); using namespace parameters; - py::enum_(m, "RecombinationWeights") + py::native_enum(m, "RecombinationWeights", "enum.Enum") .value("DEFAULT", parameters::RecombinationWeights::DEFAULT) .value("EQUAL", parameters::RecombinationWeights::EQUAL) .value("EXPONENTIAL", parameters::RecombinationWeights::EXPONENTIAL) - .export_values(); + .finalize(); - py::enum_(m, "BaseSampler") + py::native_enum(m, "BaseSampler", "enum.Enum") .value("UNIFORM", BaseSampler::UNIFORM) .value("SOBOL", BaseSampler::SOBOL) .value("HALTON", BaseSampler::HALTON) - .export_values(); + .finalize(); - py::enum_(m, "SampleTranformerType") + py::native_enum(m, "SampleTranformerType", "enum.Enum") .value("NONE", SampleTranformerType::NONE) .value("GAUSSIAN", SampleTranformerType::GAUSSIAN) .value("SCALED_UNIFORM", SampleTranformerType::SCALED_UNIFORM) @@ -47,15 +47,15 @@ void define_options(py::module& main) .value("LOGISTIC", SampleTranformerType::LOGISTIC) .value("CAUCHY", SampleTranformerType::CAUCHY) .value("DOUBLE_WEIBULL", SampleTranformerType::DOUBLE_WEIBULL) - .export_values(); + .finalize(); - py::enum_(m, "Mirror") + py::native_enum(m, "Mirror", "enum.Enum") .value("NONE", Mirror::NONE) .value("MIRRORED", Mirror::MIRRORED) .value("PAIRWISE", Mirror::PAIRWISE) - .export_values(); + .finalize(); - py::enum_(m, "StepSizeAdaptation") + py::native_enum(m, "StepSizeAdaptation", "enum.Enum") .value("CSA", StepSizeAdaptation::CSA) .value("TPA", StepSizeAdaptation::TPA) .value("MSR", StepSizeAdaptation::MSR) @@ -65,9 +65,9 @@ void define_options(py::module& main) .value("PSR", StepSizeAdaptation::PSR) .value("SR", StepSizeAdaptation::SR) .value("SA", StepSizeAdaptation::SA) - .export_values(); + .finalize(); - py::enum_(m, "CorrectionMethod") + py::native_enum(m, "CorrectionMethod", "enum.Enum") .value("NONE", CorrectionMethod::NONE) .value("MIRROR", CorrectionMethod::MIRROR) .value("COTN", CorrectionMethod::COTN) @@ -75,17 +75,17 @@ void define_options(py::module& main) .value("SATURATE", CorrectionMethod::SATURATE) .value("TOROIDAL", CorrectionMethod::TOROIDAL) .value("RESAMPLE", CorrectionMethod::RESAMPLE) - .export_values(); + .finalize(); - py::enum_(m, "RestartStrategy") + py::native_enum(m, "RestartStrategy", "enum.Enum") .value("NONE", RestartStrategyType::NONE) .value("STOP", RestartStrategyType::STOP) .value("RESTART", RestartStrategyType::RESTART) .value("IPOP", RestartStrategyType::IPOP) .value("BIPOP", RestartStrategyType::BIPOP) - .export_values(); + .finalize(); - py::enum_(m, "MatrixAdaptationType") + py::native_enum(m, "MatrixAdaptationType", "enum.Enum") .value("COVARIANCE", MatrixAdaptationType::COVARIANCE) .value("NONE", MatrixAdaptationType::NONE) .value("MATRIX", MatrixAdaptationType::MATRIX) @@ -94,14 +94,14 @@ void define_options(py::module& main) .value("CMSA", MatrixAdaptationType::CMSA) .value("COVARIANCE_NO_EIGV", MatrixAdaptationType::COVARIANCE_NO_EIGV) .value("NATURAL_GRADIENT", MatrixAdaptationType::NATURAL_GRADIENT) - .export_values(); + .finalize(); - py::enum_(m, "CenterPlacement") + py::native_enum(m, "CenterPlacement", "enum.Enum") .value("X0", CenterPlacement::X0) .value("ZERO", CenterPlacement::ZERO) .value("UNIFORM", CenterPlacement::UNIFORM) .value("CENTER", CenterPlacement::CENTER) - .export_values(); + .finalize(); } struct PySampler : sampling::Sampler @@ -119,7 +119,7 @@ struct PySampler : sampling::Sampler }; }; -void define_samplers(py::module& main) +void define_samplers(py::module &main) { using namespace sampling; @@ -165,7 +165,7 @@ void define_samplers(py::module& main) py::class_>(m, "Orthogonal") .def(py::init, size_t>(), - py::arg("sampler"), py::arg("n_samples")) + py::arg("sampler"), py::arg("n_samples")) .def("__call__", &Orthogonal::operator()); py::class_>(m, "SampleTransformer") @@ -214,7 +214,7 @@ void define_samplers(py::module& main) .def("expected_length", &DoubleWeibullTransformer::expected_length); } -void define_utils(py::module& main) +void define_utils(py::module &main) { auto m = main.def_submodule("utils"); m.def("cdf", &cdf, py::arg("x")); @@ -245,7 +245,7 @@ void define_utils(py::module& main) .def("next", &rng::CachedShuffleSequence::next); } -void define_selection(py::module& main) +void define_selection(py::module &main) { auto m = main.def_submodule("selection"); using namespace selection; @@ -272,7 +272,7 @@ void define_selection(py::module& main) .def_readwrite("elitsm", &Strategy::elitsm); } -void define_center_placement(py::module& main) +void define_center_placement(py::module &main) { auto m = main.def_submodule("center"); using namespace center; @@ -292,7 +292,7 @@ void define_center_placement(py::module& main) .def(py::init<>()); } -void define_repelling(py::module& main) +void define_repelling(py::module &main) { using namespace repelling; auto m = main.def_submodule("repelling"); @@ -307,10 +307,9 @@ void define_repelling(py::module& main) .def_readwrite("solution", &TabooPoint::solution) .def_readwrite("shrinkage", &TabooPoint::shrinkage) .def_readwrite("criticality", &TabooPoint::criticality) - .def("__repr__", [] (TabooPoint& tb) { - return ""; - }); + .def("__repr__", [](TabooPoint &tb) + { return ""; }); py::class_>(m, "Repelling") .def(py::init<>()) @@ -319,8 +318,7 @@ void define_repelling(py::module& main) .def("prepare_sampling", &Repelling::prepare_sampling, py::arg("p")) .def_readwrite("archive", &Repelling::archive) .def_readwrite("coverage", &Repelling::coverage) - .def_readwrite("attempts", &Repelling::attempts) - ; + .def_readwrite("attempts", &Repelling::attempts); py::class_>(m, "NoRepelling") .def(py::init<>()); @@ -329,10 +327,10 @@ void define_repelling(py::module& main) m.def("manhattan", &distance::manhattan, py::arg("u"), py::arg("v")); m.def("mahanolobis", &distance::mahanolobis, py::arg("u"), py::arg("v"), py::arg("C_inv")); m.def("hill_valley_test", &distance::hill_valley_test, - py::arg("u"), py::arg("v"), py::arg("f"), py::arg("n_evals")); + py::arg("u"), py::arg("v"), py::arg("f"), py::arg("n_evals")); } -void define_matrix_adaptation(py::module& main) +void define_matrix_adaptation(py::module &main) { using namespace matrix_adaptation; auto m = main.def_submodule("matrix_adaptation"); @@ -345,34 +343,34 @@ void define_matrix_adaptation(py::module& main) .def_readwrite("dd", &Adaptation::dd) .def_readwrite("expected_length_z", &Adaptation::expected_length_z) .def("adapt_evolution_paths", &Adaptation::adapt_evolution_paths, - py::arg("pop"), - py::arg("weights"), - py::arg("stats"), - py::arg("settings"), - py::arg("mu"), - py::arg("lamb")) + py::arg("pop"), + py::arg("weights"), + py::arg("stats"), + py::arg("settings"), + py::arg("mu"), + py::arg("lamb")) .def("adapt_evolution_paths_innner", &Adaptation::adapt_evolution_paths_inner, - py::arg("pop"), - py::arg("weights"), - py::arg("stats"), - py::arg("settings"), - py::arg("mu"), - py::arg("lamb")) + py::arg("pop"), + py::arg("weights"), + py::arg("stats"), + py::arg("settings"), + py::arg("mu"), + py::arg("lamb")) .def("adapt_matrix", &Adaptation::adapt_matrix, - py::arg("weights"), - py::arg("modules"), - py::arg("population"), - py::arg("mu"), - py::arg("settings"), - py::arg("stats")) + py::arg("weights"), + py::arg("modules"), + py::arg("population"), + py::arg("mu"), + py::arg("settings"), + py::arg("stats")) .def("restart", &Adaptation::restart, py::arg("settings"), py::arg("sigma")) .def("distance", &Adaptation::distance, py::arg("u"), py::arg("v")) .def("distance_from_center", &Adaptation::distance_from_center, py::arg("x")) .def("compute_y", &Adaptation::compute_y, py::arg("zi")) .def("invert_x", &Adaptation::invert_x, py::arg("xi"), py::arg("sigma")) .def("invert_y", &Adaptation::invert_y, py::arg("yi")) - .def("__repr__", [] (Adaptation& dyn) - { + .def("__repr__", [](Adaptation &dyn) + { std::stringstream ss; ss << std::boolalpha; ss << ""; return ss.str(); }); - py::class_>(m, "NoAdaptation") .def(py::init(), py::arg("dimension"), py::arg("x0"), py::arg("expected_length_z")) - .def("__repr__", [] (None& dyn) - { + .def("__repr__", [](None &dyn) + { std::stringstream ss; ss << std::boolalpha; ss << ""; return ss.str(); }); - py::class_>(m, "CovarianceAdaptation") .def(py::init(), py::arg("dimension"), py::arg("x0"), py::arg("expected_length_z")) .def_readwrite("pc", &CovarianceAdaptation::pc) @@ -413,14 +409,14 @@ void define_matrix_adaptation(py::module& main) .def_readwrite("inv_root_C", &CovarianceAdaptation::inv_root_C) .def_readwrite("hs", &CovarianceAdaptation::hs) .def("adapt_covariance_matrix", &CovarianceAdaptation::adapt_covariance_matrix, - py::arg("weights"), - py::arg("modules"), - py::arg("population"), - py::arg("mu")) + py::arg("weights"), + py::arg("modules"), + py::arg("population"), + py::arg("mu")) .def("perform_eigendecomposition", &CovarianceAdaptation::perform_eigendecomposition, py::arg("stats")) .def("adapt_ps", &CovarianceAdaptation::adapt_ps, py::arg("weights")) - .def("__repr__", [] (CovarianceAdaptation& dyn) - { + .def("__repr__", [](CovarianceAdaptation &dyn) + { std::stringstream ss; ss << std::boolalpha; ss << "(), py::arg("dimension"), py::arg("x0"), py::arg("expected_length_z")) .def_readwrite("M", &MatrixAdaptation::M) .def_readwrite("M_inv", &MatrixAdaptation::M_inv) - .def("__repr__", [] (MatrixAdaptation& dyn) - { + .def("__repr__", [](MatrixAdaptation &dyn) + { std::stringstream ss; ss << std::boolalpha; ss << ">(m, "CovarianceNoEigvAdaptation") - ; + py::class_>(m, "CovarianceNoEigvAdaptation"); py::class_>(m, "NaturalGradientAdaptation") .def(py::init(), py::arg("dimension"), py::arg("x0"), py::arg("expected_length_z"), py::arg("sigma")) @@ -500,16 +495,14 @@ void define_matrix_adaptation(py::module& main) .def_readwrite("G", &NaturalGradientAdaptation::G) .def_readwrite("sigma_g", &NaturalGradientAdaptation::sigma_g) .def("compute_gradients", &NaturalGradientAdaptation::compute_gradients, py::arg("pop"), - py::arg("weights"), - py::arg("stats"), - py::arg("settings"), - py::arg("mu"), - py::arg("lamb") - ) - ; + py::arg("weights"), + py::arg("stats"), + py::arg("settings"), + py::arg("mu"), + py::arg("lamb")); } -void define_parameters(py::module& main) +void define_parameters(py::module &main) { auto m = main.def_submodule("parameters"); using namespace parameters; @@ -532,8 +525,8 @@ void define_parameters(py::module& main) .def_readwrite("matrix_adaptation", &Modules::matrix_adaptation) .def_readwrite("center_placement", &Modules::center_placement) .def_readwrite("sample_transformation", &Modules::sample_transformation) - .def("__repr__", [] (Modules& mod) - { return to_string(mod); }); + .def("__repr__", [](Modules &mod) + { return to_string(mod); }); py::class_(m, "Solution") .def(py::init<>()) @@ -556,8 +549,8 @@ void define_parameters(py::module& main) .def_readwrite("success_ratio", &Stats::success_ratio) .def_readwrite("last_update", &Stats::last_update) .def_readwrite("n_updates", &Stats::n_updates) - .def("__repr__", [] (Stats& stats) - { + .def("__repr__", [](Stats &stats) + { std::stringstream ss; ss << std::boolalpha; ss << ">(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, - bool, bool>(), - py::arg("dim"), - py::arg("modules") = std::nullopt, - py::arg("target") = std::nullopt, - py::arg("max_generations") = std::nullopt, - py::arg("budget") = std::nullopt, - py::arg("sigma0") = std::nullopt, - py::arg("lambda0") = std::nullopt, - py::arg("mu0") = std::nullopt, - py::arg("x0") = std::nullopt, - py::arg("lb") = std::nullopt, - py::arg("ub") = std::nullopt, - py::arg("cs") = std::nullopt, - py::arg("cc") = std::nullopt, - py::arg("cmu") = std::nullopt, - py::arg("c1") = std::nullopt, - py::arg("damps") = std::nullopt, - py::arg("acov") = std::nullopt, - py::arg("verbose") = false, - py::arg("always_compute_eigv") = false - - ) + std::optional, std::optional, std::optional, + std::optional, std::optional, + std::optional, std::optional, std::optional, + std::optional, std::optional, std::optional, + bool, bool>(), + py::arg("dim"), + py::arg("modules") = std::nullopt, + py::arg("target") = std::nullopt, + py::arg("max_generations") = std::nullopt, + py::arg("budget") = std::nullopt, + py::arg("sigma0") = std::nullopt, + py::arg("lambda0") = std::nullopt, + py::arg("mu0") = std::nullopt, + py::arg("x0") = std::nullopt, + py::arg("lb") = std::nullopt, + py::arg("ub") = std::nullopt, + py::arg("cs") = std::nullopt, + py::arg("cc") = std::nullopt, + py::arg("cmu") = std::nullopt, + py::arg("c1") = std::nullopt, + py::arg("damps") = std::nullopt, + py::arg("acov") = std::nullopt, + py::arg("verbose") = false, + py::arg("always_compute_eigv") = false + ) .def_readonly("dim", &Settings::dim) .def_readonly("modules", &Settings::modules) .def_readwrite("target", &Settings::target) @@ -661,8 +652,8 @@ void define_parameters(py::module& main) .def_readwrite("acov", &Settings::acov) .def_readwrite("verbose", &Settings::verbose) .def_readonly("one_plus_one", &Settings::one_plus_one) - .def("__repr__", [] (Settings& settings) - { + .def("__repr__", [](Settings &settings) + { std::stringstream ss; ss << std::boolalpha; ss << ", std::shared_ptr, std::shared_ptr, - std::shared_ptr - >; + std::shared_ptr>; py::class_>(main, "Parameters") .def(py::init(), py::arg("dimension")) @@ -704,36 +694,36 @@ void define_parameters(py::module& main) .def("adapt", &Parameters::adapt) .def("start", &Parameters::start, py::arg("objective")) .def("perform_restart", &Parameters::perform_restart, py::arg("objective"), - py::arg("sigma") = std::nullopt) + py::arg("sigma") = std::nullopt) .def_readwrite("settings", &Parameters::settings) .def_readwrite("mu", &Parameters::mu) .def_readwrite("lamb", &Parameters::lambda) .def_property( "adaptation", - [] (Parameters& self) -> AdaptationType + [](Parameters &self) -> AdaptationType { switch (self.settings.modules.matrix_adaptation) { - case MatrixAdaptationType::MATRIX: - return std::dynamic_pointer_cast(self.adaptation); - case MatrixAdaptationType::NONE: - return std::dynamic_pointer_cast(self.adaptation); - case MatrixAdaptationType::SEPARABLE: - return std::dynamic_pointer_cast(self.adaptation); - case MatrixAdaptationType::CHOLESKY: - return std::dynamic_pointer_cast(self.adaptation); - case MatrixAdaptationType::CMSA: - return std::dynamic_pointer_cast(self.adaptation); - case MatrixAdaptationType::COVARIANCE_NO_EIGV: - return std::dynamic_pointer_cast(self.adaptation); - case MatrixAdaptationType::NATURAL_GRADIENT: - return std::dynamic_pointer_cast(self.adaptation); - default: - case MatrixAdaptationType::COVARIANCE: - return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::MATRIX: + return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::NONE: + return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::SEPARABLE: + return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::CHOLESKY: + return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::CMSA: + return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::COVARIANCE_NO_EIGV: + return std::dynamic_pointer_cast(self.adaptation); + case MatrixAdaptationType::NATURAL_GRADIENT: + return std::dynamic_pointer_cast(self.adaptation); + default: + case MatrixAdaptationType::COVARIANCE: + return std::dynamic_pointer_cast(self.adaptation); } }, - [] (Parameters& self, std::shared_ptr adaptation) + [](Parameters &self, std::shared_ptr adaptation) { self.adaptation = adaptation; }) @@ -751,20 +741,19 @@ void define_parameters(py::module& main) .def_readwrite("center_placement", &Parameters::center_placement); } -void define_bounds(py::module& main) +void define_bounds(py::module &main) { auto m = main.def_submodule("bounds"); using namespace bounds; py::class_>(m, "BoundCorrection") - + .def_readonly("n_out_of_bounds", &BoundCorrection::n_out_of_bounds) .def("correct", &BoundCorrection::correct, - py::arg("index"), py::arg("parameters")) + py::arg("index"), py::arg("parameters")) .def("correct_x", &BoundCorrection::correct_x, py::arg("xi"), py::arg("oob"), py::arg("sigma"), py::arg("settings")) - .def("delta_out_of_bounds", &BoundCorrection::delta_out_of_bounds, py::arg("xi"), py::arg("oob"), py::arg("settings")) - .def("is_out_of_bounds", &BoundCorrection::is_out_of_bounds, py::arg("xi"), py::arg("settings")) - ; + .def("delta_out_of_bounds", &BoundCorrection::delta_out_of_bounds, py::arg("xi"), py::arg("oob"), py::arg("settings")) + .def("is_out_of_bounds", &BoundCorrection::is_out_of_bounds, py::arg("xi"), py::arg("settings")); py::class_>(m, "Resample") .def(py::init<>()); @@ -789,7 +778,7 @@ void define_bounds(py::module& main) .def(py::init<>()); } -void define_mutation(py::module& main) +void define_mutation(py::module &main) { auto m = main.def_submodule("mutation"); using namespace mutation; @@ -805,20 +794,20 @@ void define_mutation(py::module& main) py::class_>(m, "SequentialSelection") .def(py::init(), - py::arg("mirror"), - py::arg("mu"), - py::arg("seq_cuttoff_factor") = 1.0) + py::arg("mirror"), + py::arg("mu"), + py::arg("seq_cuttoff_factor") = 1.0) .def("break_conditions", &SequentialSelection::break_conditions, - py::arg("i"), - py::arg("f"), - py::arg("fopt"), - py::arg("mirror")); + py::arg("i"), + py::arg("f"), + py::arg("fopt"), + py::arg("mirror")); py::class_>(m, "NoSequentialSelection") .def(py::init(), - py::arg("mirror"), - py::arg("mu"), - py::arg("seq_cuttoff_factor") = 1.0); + py::arg("mirror"), + py::arg("mu"), + py::arg("seq_cuttoff_factor") = 1.0); py::class_>(m, "SigmaSampler") .def(py::init(), py::arg("dimension")) @@ -830,21 +819,20 @@ void define_mutation(py::module& main) py::class_>(m, "Strategy") .def( py::init< - std::shared_ptr, - std::shared_ptr, - std::shared_ptr, - Float - >(), + std::shared_ptr, + std::shared_ptr, + std::shared_ptr, + Float>(), py::arg("threshold_convergence"), py::arg("sequential_selection"), py::arg("sigma_sampler"), py::arg("sigma0")) .def("adapt", &Strategy::adapt, py::arg("weights"), - py::arg("dynamic"), - py::arg("population"), - py::arg("old_population"), - py::arg("stats"), - py::arg("lamb")) + py::arg("dynamic"), + py::arg("population"), + py::arg("old_population"), + py::arg("stats"), + py::arg("lamb")) .def( "mutate", &CSA::mutate, py::arg("objective"), py::arg("n_offspring"), @@ -853,8 +841,7 @@ void define_mutation(py::module& main) .def_readwrite("sequential_selection", &Strategy::sq) .def_readwrite("sigma_sampler", &Strategy::ss) .def_readwrite("sigma", &Strategy::sigma) - .def_readwrite("s", &Strategy::s) - ; + .def_readwrite("s", &Strategy::s); py::class_>(m, "CSA"); py::class_>(m, "TPA") @@ -871,11 +858,9 @@ void define_mutation(py::module& main) py::class_>(m, "LPXNES"); py::class_>(m, "SR"); py::class_>(m, "SA"); - - } -void define_population(py::module& main) +void define_population(py::module &main) { py::class_(main, "Population") .def(py::init(), py::arg("dimension"), py::arg("n")) @@ -896,63 +881,60 @@ void define_population(py::module& main) } class constants_w -{}; +{ +}; -void define_constants(py::module& m) +void define_constants(py::module &m) { py::class_(m, "constants") .def_property_static( "cache_max_doubles", - [] (py::object) + [](py::object) { return constants::cache_max_doubles; }, - [] (py::object, size_t a) + [](py::object, size_t a) { constants::cache_max_doubles = a; }) .def_property_static( "cache_min_samples", - [] (py::object) + [](py::object) { return constants::cache_min_samples; }, - [] (py::object, size_t a) + [](py::object, size_t a) { constants::cache_min_samples = a; }) .def_property_static( "cache_samples", - [] (py::object) + [](py::object) { return constants::cache_samples; }, - [] (py::object, bool a) + [](py::object, bool a) { constants::cache_samples = a; }) .def_property_static( "clip_sigma", - [] (py::object) + [](py::object) { return constants::clip_sigma; }, - [] (py::object, bool a) + [](py::object, bool a) { constants::clip_sigma = a; }) .def_property_static( "use_box_muller", - [] (py::object) + [](py::object) { return constants::use_box_muller; }, - [] (py::object, bool a) - { constants::use_box_muller = a; }) - ; + [](py::object, bool a) + { constants::use_box_muller = a; }); } struct PyCriterion : restart::Criterion { - PyCriterion(const std::string& name) : restart::Criterion(name) {} + PyCriterion(const std::string &name) : restart::Criterion(name) {} - void update(const parameters::Parameters& p) override + void update(const parameters::Parameters &p) override { PYBIND11_OVERRIDE_PURE(void, restart::Criterion, update, p); } - void on_reset(const parameters::Parameters& p) override + void on_reset(const parameters::Parameters &p) override { PYBIND11_OVERRIDE(void, restart::Criterion, on_reset, p); } }; - - - -void define_restart_criteria(py::module& main) +void define_restart_criteria(py::module &main) { auto m = main.def_submodule("restart"); using namespace restart; @@ -965,8 +947,8 @@ void define_restart_criteria(py::module& main) .def_readwrite("met", &Criterion::met) .def_readwrite("name", &Criterion::name) .def_readwrite("last_restart", &Criterion::last_restart) - .def("__repr__", [] (Criterion& self) - { return "<" + self.name + " met: " + std::to_string(self.met) + ">"; }); + .def("__repr__", [](Criterion &self) + { return "<" + self.name + " met: " + std::to_string(self.met) + ">"; }); ; py::class_>(m, "ExceededMaxIter") @@ -998,8 +980,7 @@ void define_restart_criteria(py::module& main) py::class_>(m, "TolX") .def(py::init<>()) .def_readwrite("tolx_vector", &TolX::tolx_vector) - .def_readwrite_static("tolerance", &TolX::tolerance) - ; + .def_readwrite_static("tolerance", &TolX::tolerance); py::class_>(m, "MaxDSigma") .def(py::init<>()) @@ -1015,8 +996,7 @@ void define_restart_criteria(py::module& main) py::class_>(m, "NoEffectAxis") .def(py::init<>()) - .def_readwrite_static("tolerance", &NoEffectAxis::tolerance) - ; + .def_readwrite_static("tolerance", &NoEffectAxis::tolerance); py::class_>(m, "NoEffectCoord") .def(py::init<>()) @@ -1030,22 +1010,23 @@ void define_restart_criteria(py::module& main) .def_readwrite_static("tolerance", &Stagnation::tolerance); py::bind_vector(m, "CriteriaVector", py::module_local()) - .def("__repr__", [](const vCriteria& v) { + .def("__repr__", [](const vCriteria &v) + { std::string s = "CriteriaVector["; for (size_t i = 0; i < v.size(); ++i) { if (i) s += ", "; s += v[i] ? py::str(py::cast(v[i])).cast() : "None"; } - return s + "]"; - }); - + return s + "]"; }); + py::class_(m, "Criteria") .def_property( - "items", - [](Criteria &self) -> vCriteria& { return self.items; }, - [](Criteria &self, vCriteria v) { self.items = std::move(v); }, - py::return_value_policy::reference_internal - ) + "items", + [](Criteria &self) -> vCriteria & + { return self.items; }, + [](Criteria &self, vCriteria v) + { self.items = std::move(v); }, + py::return_value_policy::reference_internal) .def("reset", &Criteria::reset, py::arg("parameters")) .def("update", &Criteria::update, py::arg("parameters")) .def("reason", &Criteria::reason) @@ -1054,10 +1035,9 @@ void define_restart_criteria(py::module& main) py::class_>(m, "TooMuchRepelling") .def(py::init<>()) .def_readwrite_static("tolerance", &TooMuchRepelling::tolerance); - } -void define_restart_strategy(py::module& main) +void define_restart_strategy(py::module &main) { auto m = main.def_submodule("restart"); using namespace restart; @@ -1085,7 +1065,7 @@ void define_restart_strategy(py::module& main) .def_readonly("used_budget", &BIPOP::used_budget); } -void define_cmaes(py::module& m) +void define_cmaes(py::module &m) { py::class_(m, "ModularCMAES") .def(py::init>(), py::arg("parameters")) @@ -1102,7 +1082,7 @@ void define_cmaes(py::module& m) .def_readonly("p", &ModularCMAES::p); } -void define_es(py::module& main) +void define_es(py::module &main) { auto m = main.def_submodule("es"); parameters::Modules default_modules; @@ -1110,13 +1090,13 @@ void define_es(py::module& main) py::class_>(m, "OnePlusOneES") .def( py::init< - size_t, - Vector, - Float, - Float, - size_t, - Float, - parameters::Modules>(), + size_t, + Vector, + Float, + Float, + size_t, + Float, + parameters::Modules>(), py::arg("d"), py::arg("x0"), py::arg("f0"), @@ -1142,12 +1122,12 @@ void define_es(py::module& main) py::class_>(m, "MuCommaLambdaES") .def( py::init< - size_t, - Vector, - Float, - size_t, - Float, - parameters::Modules>(), + size_t, + Vector, + Float, + size_t, + Float, + parameters::Modules>(), py::arg("d"), py::arg("x0"), py::arg("sigma0") = 1.0, @@ -1184,8 +1164,6 @@ void define_es(py::module& main) .def_readwrite("corrector", &MuCommaLambdaES::corrector); } - - PYBIND11_MODULE(cmaescpp, m) { define_constants(m); diff --git a/tests/test_c_mutation.py b/tests/test_c_mutation.py index 44f4d9a..a73a79c 100644 --- a/tests/test_c_mutation.py +++ b/tests/test_c_mutation.py @@ -68,7 +68,7 @@ def get_cma(self, ssa, adapt_sigma=True): return cma def test_adapt_csa(self): - cma = self.get_cma(options.CSA) + cma = self.get_cma(options.StepSizeAdaptation.CSA) self.assertAlmostEqual( cma.p.mutation.sigma, @@ -80,22 +80,22 @@ def test_adapt_csa(self): ) def test_adapt_tpa(self): - cma = self.get_cma(options.TPA) + cma = self.get_cma(options.StepSizeAdaptation.TPA) s = ((1 - cma.p.weights.cs) * 0) + (cma.p.weights.cs * cma.p.mutation.a_tpa) self.assertAlmostEqual(cma.p.mutation.sigma, cma.p.settings.sigma0 * np.exp(s)) def test_adapt_msr(self): - cma = self.get_cma(options.MSR) + cma = self.get_cma(options.StepSizeAdaptation.MSR) def test_adapt_psr(self): - cma = self.get_cma(options.PSR) + cma = self.get_cma(options.StepSizeAdaptation.PSR) def test_adapt_mxnes(self): - cma = self.get_cma(options.MXNES) + cma = self.get_cma(options.StepSizeAdaptation.MXNES) def test_adapt_xnes(self): - cma = self.get_cma(options.XNES) + cma = self.get_cma(options.StepSizeAdaptation.XNES) w = cma.p.weights.weights.clip(0)[: cma.p.pop.n] z = ( @@ -112,10 +112,8 @@ def test_adapt_xnes(self): def test_adapt_lpxnes(self): - cma = self.get_cma(options.LPXNES) - + 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))) 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_tuning.py b/tests/test_c_tuning.py new file mode 100644 index 0000000..79e1d9c --- /dev/null +++ b/tests/test_c_tuning.py @@ -0,0 +1,54 @@ +import unittest +from copy import deepcopy +from modcma import c_maes + +from ConfigSpace import ConfigurationSpace + +def sphere(x): + return sum(xi**2 for xi in x) + +class TestTuning(unittest.TestCase): + def test_module_options(self): + options = c_maes.get_all_module_options() + self.assertIsInstance(options, dict) + self.assertEqual(options, {**options, "active": (False, True)}) + self.assertEqual(options, {**options, "elitist": (False, True)}) + self.assertEqual(options, {**options, "orthogonal": (False, True)}) + self.assertEqual(options, {**options, "sequential_selection": (False, True)}) + self.assertEqual(options, {**options, "threshold_convergence": (False, True)}) + self.assertEqual(options, {**options, "sample_sigma": (False, True)}) + self.assertEqual(options, {**options, "repelling_restart": (False, True)}) + + + def test_configspace(self): + cspace = c_maes.get_configspace(2) + default = cspace.get_default_configuration() + self.assertIsInstance(cspace, ConfigurationSpace) + settings = c_maes.settings_from_config(2, default) + self.assertIsInstance(settings, c_maes.Settings) + self.assertEqual(settings.cc, None) + self.assertEqual(settings.modules.sampler, c_maes.options.BaseSampler.UNIFORM) + + changed = deepcopy(default) + changed['cc'] = 0.1 + changed['sampler'] = "HALTON" + settings_changed = c_maes.settings_from_config(2, changed) + self.assertEqual(settings_changed.cc, 0.1) + self.assertEqual(settings_changed.modules.sampler, c_maes.options.BaseSampler.HALTON) + + def test_from_dict(self): + settings = c_maes.settings_from_dict(2, active=True, cc=1) + self.assertEqual(settings.modules.active, True) + self.assertEqual(settings.cc, 1) + + + def test_fmin(self): + xopt, fopt, evals, es = c_maes.fmin(sphere, [1, 2], 0.2, 100, active=True) + self.assertLess(fopt, 1e-4) + self.assertLessEqual(evals, 100) + self.assertEqual(sphere(xopt), fopt) + + +if __name__ == "__main__": + unittest.main() + \ No newline at end of file diff --git a/tests/test_fmin.py b/tests/test_fmin.py index 47e1307..a71815f 100644 --- a/tests/test_fmin.py +++ b/tests/test_fmin.py @@ -20,6 +20,8 @@ def test_fmin(self): self.assertAlmostEqual(sum(xopt), fopt) self.assertGreater(evaluations, 0) self.assertAlmostEqual(len(xopt), 5) + +