Basin

Search documentation

Search the guides, solvers, and migration docs.

  • Guide
  • Visualizer
  • Benchmarks

Documentation

  • Overview
  • Getting started
  • Migrating from Argmin
  • Migrating to Basin 2.0
  • Migrating to Basin 3.0

Solvers

  • Choosing a solver
  • First-order
    • Gradient descent
    • Nonlinear CG
    • Stochastic gradient descent
    • Projected gradient descent
  • Quasi-Newton
    • BFGS
    • L-BFGS
    • L-BFGS-B
  • Derivative-free
    • Nelder–Mead
    • MADS
    • NEWUOA
    • BOBYQA
    • LINCOA
    • COBYLA
    • Solis–Wets
  • Trust-region
    • Trust-region Newton
  • Nonlinear least squares
    • Gauss–Newton
    • Levenberg–Marquardt
    • Levenberg–Marquardt QR
    • Bounded Levenberg–Marquardt
    • Trust-region reflective
  • Global and stochastic
    • DIRECT
    • Globalized bounded Nelder–Mead
    • Simulated annealing
    • CMA-ES
    • Bounded CMA-ES
    • Differential evolution
    • Particle swarm
    • Random search
    • Steady-state genetic algorithm
    • Basin-hopping
  • Memetic
    • CMA-ES with local search
    • Bounded CMA-ES with local search
    • DE with local search
    • Local-search chains
    • CMA-ES chains
    • Solis–Wets chains
  • General constrained
    • SLSQP
    • Barrier method
    • Augmented Lagrangian
  • Scalar minimization
    • Brent
    • Brent with derivatives
    • Golden-section search
  • Root finding
    • Bisection
    • Brent
    • Secant
    • Newton
    • Halley
    • TOMS 748
Navigation

Documentation

  • Overview
  • Getting started
  • Migrating from Argmin
  • Migrating to Basin 2.0
  • Migrating to Basin 3.0

Solvers

  • Choosing a solver
  • First-order
    • Gradient descent
    • Nonlinear CG
    • Stochastic gradient descent
    • Projected gradient descent
  • Quasi-Newton
    • BFGS
    • L-BFGS
    • L-BFGS-B
  • Derivative-free
    • Nelder–Mead
    • MADS
    • NEWUOA
    • BOBYQA
    • LINCOA
    • COBYLA
    • Solis–Wets
  • Trust-region
    • Trust-region Newton
  • Nonlinear least squares
    • Gauss–Newton
    • Levenberg–Marquardt
    • Levenberg–Marquardt QR
    • Bounded Levenberg–Marquardt
    • Trust-region reflective
  • Global and stochastic
    • DIRECT
    • Globalized bounded Nelder–Mead
    • Simulated annealing
    • CMA-ES
    • Bounded CMA-ES
    • Differential evolution
    • Particle swarm
    • Random search
    • Steady-state genetic algorithm
    • Basin-hopping
  • Memetic
    • CMA-ES with local search
    • Bounded CMA-ES with local search
    • DE with local search
    • Local-search chains
    • CMA-ES chains
    • Solis–Wets chains
  • General constrained
    • SLSQP
    • Barrier method
    • Augmented Lagrangian
  • Scalar minimization
    • Brent
    • Brent with derivatives
    • Golden-section search
  • Root finding
    • Bisection
    • Brent
    • Secant
    • Newton
    • Halley
    • TOMS 748

Bounded CMA-ES

Bounded CMA-ES combines covariance adaptation with an adaptive quadratic boundary penalty. It samples in unrestricted model coordinates, clips each sample into the feasible box for evaluation, and uses the distance moved by that repair to guide selection. Every reported parameter and objective value refers to an evaluated point inside the box.

API reference

PropertyRequirement
ObjectiveA scalar function of continuous parameters
EvaluationsCostFunction and a complete box-only Constraints declaration
SearchStochastic population search from an initial mean
ConstraintsBox bounds, including unbounded coordinate sides
ParametersAll four dense backends, with f32 or f64

When to use it

Consider bounded CMA-ES when derivatives are unavailable, parameters interact strongly, and the objective must only receive feasible points. It learns the same full covariance as CMA-ES while handling minima on box faces and corners without repeatedly rejecting and redrawing infeasible samples.

Hansen (2016) describes the CMA core. The boundary penalty follows Hansen et al. (2009), with the specific adaptation used by the pinned pycma BoundPenalty implementation.

For smooth objectives with reliable gradients, compare L-BFGS-B. For a local interpolation model and an expensive objective, compare BOBYQA. Bounded CMA-ES can explore multiple basins, but one finite run does not certify a global minimum. Compare different seeds and starting means.

How it works

The CMA distribution remains N(m, sigma² C) in unrestricted coordinates. For each sampled point x, the solver constructs its box repair x_rep by clamping each component. It evaluates only f(x_rep) and ranks the sample using

fitness(x) = f(x_rep) + sum(gamma[i] * (x[i] - x_rep[i])²) / n

The original, unrepaired point enters mean and covariance adaptation. This distinction lets a sample’s distance outside the box influence the search model even though the objective never sees that point. The per-coordinate penalty weights gamma start at 1 and adapt from recent objective spreads, coordinate variances, and the mean’s boundary violation.

The solver uses active covariance adaptation, evolution paths, and an eigendecomposition on every generation, as in plain CMA-ES. The default population size is 4 + floor(3 ln n). Fresh initialization projects the starting mean into the box before drawing the first population.

The published current point is the clipped mean and its raw objective value. Population members are also clipped, with raw costs, but retain the ranking determined by penalized fitness. Their costs can therefore appear out of numerical order. The historical incumbent compares raw objective values across these evaluated members and means.

Example

This objective would minimize at [2.0, -1.0] without constraints. The declared box restricts its first coordinate to [-1.0, 1.0], so the constrained minimum is [1.0, -1.0], with cost 1.0. The objective asserts that every point it receives satisfies the box.

use basin::{
    BoundedCmaEs, BoxBounds, Constrained, ConstraintSet, CostFunction,
    DenseMatrix, Executor,
};
use std::convert::Infallible;

struct Quadratic;

impl CostFunction for Quadratic {
    type Param = Vec<f64>;
    type Output = f64;
    type Error = Infallible;

    fn cost(&self, x: &Self::Param) -> Result<f64, Self::Error> {
        assert!((-1.0..=1.0).contains(&x[0]));
        assert!((-2.0..=2.0).contains(&x[1]));
        Ok((x[0] - 2.0).powi(2) + (x[1] + 1.0).powi(2))
    }
}

fn main() {
    let bounds = BoxBounds::new(Some(vec![-1.0, -2.0]), Some(vec![1.0, 2.0]));
    let problem = Constrained::new(
        Quadratic,
        ConstraintSet::new().with_bounds(bounds),
    );
    let solver = BoundedCmaEs::<Vec<f64>, DenseMatrix>::new(42, 0.6)
        .with_absolute_distribution_size_tolerance(1e-10);

    let result = Executor::from_start(problem, solver, vec![0.0, 1.0])
        .max_iter(500)
        .run()
        .unwrap();

    assert!((result.best_cost() - 1.0).abs() < 1e-8);
    assert!((result.best_param()[0] - 1.0).abs() < 1e-6);
    assert!((result.best_param()[1] + 1.0).abs() < 1e-4);
}

Practical considerations

Declare the complete feasible box

Constraints belong to the problem. Constrained::new installs the supplied ConstraintSet as its complete declaration, replacing any declaration on the wrapped objective. You can instead implement Constraints directly with Set = ConstraintSet<BoxBounds<V>>.

This solver requires both lower and upper vectors. Represent an unbounded coordinate side with negative infinity in the lower vector or positive infinity in the upper vector. Each vector must match the starting mean’s dimension, and each lower bound must be at most its upper bound. NaN, inverted intervals, and infinities on the wrong side are invalid. Linear and nonlinear constraint blocks are unsupported by the box-only solver contract.

Keep raw objective values separate from search fitness

result.cost() and result.best_cost() exclude the boundary penalty. Likewise, published population costs match their clipped points. Use penalized_costs() on a retained solver when inspecting selection order, and genotypes() for the corresponding unrestricted samples.

The live mean() also describes unrestricted model coordinates and can move outside the box after initialization. Use the result’s param() for the evaluated, clipped mean. Clipping a model mean can leave the current point unchanged even while covariance and penalty adaptation continue. That observation alone is insufficient evidence of convergence.

Scale the initial distribution and budget full generations

new(seed, sigma) requires a finite, strictly positive scale. Optional with_stds sets positive, finite coordinate factors, so the initial standard deviation in coordinate i is sigma * stds[i]. Match these scales to the feasible widths and the distance worth exploring. A very wide distribution can spend much of its effort on repairs to a small part of the boundary.

with_lambda requires at least four members. Initialization and each completed generation use lambda + 1 objective evaluations, including the repaired mean. The boundary penalty itself requires no additional objective calls. Executor budgets are checked at iteration boundaries, so one generation can exceed a remaining evaluation allowance.

Configure a distribution tolerance explicitly

TolX is disabled by default. Set with_absolute_distribution_size_tolerance to compare sigma * max(d[i]) against a threshold in parameter units, where d[i] are principal-axis standard deviations before multiplication by sigma. None disables the check, zero requests exact collapse, and positive thresholds use a strict inequality. A successful check reports CmaEsTolerance.

This tests the unrestricted distribution’s size. It does not measure a projected gradient, a feasible error bound, or global optimality. Keep iteration or evaluation budgets on the executor and inspect its report. As with plain CMA-ES, other CMA stopping heuristics are not implemented.

Implementation

PopulationProgress stores repaired members, raw costs, the repaired mean, and the incumbent. The solver owns unrepaired genotypes, penalized fitness, the distribution, penalty weights and history, and its seeded ChaCha8Rng. Executor::from_start supplies the mean; explicitly constructed progress can use PopulationProgress::from_point.

Retain the solver with Executor::run_with_solver to inspect mean(), sigma(), distribution(), genotypes(), and penalized_costs(). Fresh initialization resets the distribution, penalty history, and RNG. Exact solver-and-state checkpoints preserve them with progress and counts and skip initialization; state-only snapshots start a new distribution. The same seed reproduces a deterministic objective’s trajectory within the same backend.

Supported pairs are Vec<F>/DenseMatrix<F>, nalgebra::DVector<F>/DMatrix<F>, ndarray::Array1<F>/Array2<F>, and faer::Col<F>/Mat<F>, with F = f32 or f64. All four dense paths provide covariance eigendecomposition, diagonal extraction, and box repair in pure Rust without a BLAS/LAPACK provider. Dense covariance storage and factorization impose the same dimension costs as plain CMA-ES.

References

  1. Hansen, N. (2016). The CMA Evolution Strategy: A Tutorial. Report 1604.00772. arXiv. doi:10.48550/arXiv.1604.00772.
  2. Hansen, N., Niederberger, A. S. P., Guzzella, L., and Koumoutsakos, P. (2009). A method for handling uncertainty in evolutionary optimization with an application to feedback control of combustion. IEEE Transactions on Evolutionary Computation, 13(1), 180–197. doi:10.1109/TEVC.2008.924423.

Basin v3.1.0 — numerical optimization for Rust.

GitHub docs.rs crates.io