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

DIRECT

DIRECT is a deterministic global optimizer for a scalar function over a finite box. It divides the box into rectangles and samples their centers, balancing further exploration of large regions against refinement around good objective values. It needs neither derivatives nor a supplied Lipschitz constant.

API reference

PropertyRequirement
ObjectiveA scalar function on a finite box
EvaluationsCostFunction
SearchDeterministic global partitioning
ConstraintsA box-only declaration through Constraints
ParametersAll four dense backends, with f32 or f64

When to use it

Consider DIRECT when the search region has meaningful finite bounds and you want a reproducible exploration of several possible minima. Jones, Perttunen, and Stuckman (1993) introduced the method to balance local and global search without choosing a Lipschitz constant. Its rectangle geometry is most practical when the number of free coordinates is modest.

Compare differential evolution or CMA-ES when a stochastic population search suits the problem. GBNM instead combines bounded local simplex searches with random restarts. If you already have a promising starting point and need local refinement, consider BOBYQA.

A finite DIRECT run provides the best sampled point, rather than a certificate that no lower point exists elsewhere in the box. In particular, a small rectangle around the incumbent describes local search resolution; other rectangles can remain large.

How it works

DIRECT normalizes each free coordinate to [0, 1] and evaluates the center of the resulting unit box. Coordinates with equal lower and upper bounds are fixed and do not enter the partition geometry.

For each rectangle, the solver keeps the objective at its center and the length of its Euclidean half-diagonal. A rectangle is potentially optimal when its cost minus K times its radius is competitive for some positive value of K, and its predicted improvement clears the configured margin. Small values of K favor good observed costs; large values favor broad unexplored rectangles. Basin selects across these trade-offs using a lower convex hull.

The solver samples two new centers along each longest axis of a selected rectangle, then trisects along those axes. Axes with better samples split first, giving their promising samples larger child rectangles. One iteration processes the entire selected set, from larger rectangles to smaller ones. Exact cost ties are retained.

Basin implements original DIRECT, using Euclidean half-diagonals. The locally biased DIRECT-L variant has different selection behavior and is not the algorithm described here.

Example

This two-dimensional quartic has several local minima on [-5, 5]². DIRECT searches the box until it finds an objective below -78.33 or reaches the evaluation budget. The PointState parameter supplies the vector’s shape; each fresh DIRECT run starts at the box midpoint, regardless of the values in that vector.

use basin::{
    BoxBounds, ConstraintDescription, ConstraintSet, Constraints,
    CostFunction, Direct, Executor, PointState,
};
use std::convert::Infallible;

struct BoundedQuartic {
    lower: Vec<f64>,
    upper: Vec<f64>,
}

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

    fn cost(&self, x: &Self::Param) -> Result<f64, Self::Error> {
        Ok(x.iter()
            .map(|v| 0.5 * (v.powi(4) - 16.0 * v * v + 5.0 * v))
            .sum())
    }
}

impl Constraints for BoundedQuartic {
    type Set = ConstraintSet<BoxBounds<Vec<f64>>>;

    fn constraints(&self) -> <Self::Set as ConstraintDescription>::Borrowed<'_> {
        ConstraintSet::new().with_bounds(BoxBounds::new(
            Some(&self.lower),
            Some(&self.upper),
        ))
    }
}

fn main() {
    let problem = BoundedQuartic {
        lower: vec![-5.0; 2],
        upper: vec![5.0; 2],
    };
    let solver = Direct::new();

    let result = Executor::new(problem, solver, PointState::new(vec![0.0; 2]))
        .target_objective(-78.33)
        .max_cost_evals(2_000)
        .run()
        .unwrap();

    assert!(result.best_cost() < -78.33);
    assert!(result.best_param().iter().all(|v| (-5.0..=5.0).contains(v)));
}

The objective target gives this example a useful stopping point. It does not ask DIRECT to identify the exact minimizer or exhaust the box.

Practical considerations

Choose the box deliberately

The problem must declare ConstraintSet<BoxBounds<V>> with both bound vectors present, finite, ordered, and matching the parameter dimension. Additional constraint blocks are unsupported. Equal endpoints pin a coordinate; an entirely fixed box needs just one evaluation.

Normalization gives each free interval the same initial geometric weight. Very wide bounds therefore spend resolution over a large physical range, even if the useful region occupies a small part of it. Use bounds that describe the intended global search region.

Separate selection from stopping

with_epsilon sets the potential-improvement margin and defaults to 1e-4. Smaller values permit more local refinement; zero removes the required improvement margin. It controls rectangle selection, rather than the accuracy of the reported solution.

with_absolute_radius_tolerance defaults to Some(1e-6) and measures the best rectangle’s half-diagonal after normalization. Optional with_relative_volume_tolerance measures its volume relative to the original box and is disabled by default. Enabled geometry checks combine with OR. None disables a check, and zero requests exact zero. Volumes shrink rapidly with dimension, so a small relative volume can coexist with substantial uncertainty along individual coordinates.

Budget for complete sweeps

Use executor limits such as max_cost_evals, max_iter, or max_time. The executor checks limits between complete subdivision sweeps. A sweep can evaluate many points, particularly when several rectangles tie, so the last sweep can exceed an evaluation budget. Storage grows with both the number of coordinates and the number of sampled rectangles.

Independent probes use Problem::cost_batch. The optional parallel feature can evaluate them concurrently while preserving the numerical results for a deterministic objective. Callback scheduling can change.

Handle rejected evaluations consistently

NaN and positive infinity reject a sample. Basin additionally explores the oldest largest rejected rectangle in each sweep, allowing recovery when the initial midpoint is rejected. Geometry stopping requires a usable incumbent; an all-rejected search needs an executor limit. Negative infinity is accepted and stops the search. Application errors propagate.

If floating-point resolution prevents a split from producing distinct probes in both normalized and original coordinates, DIRECT reports NumericalNoProgress. Tighter geometric tolerances cannot overcome that limit.

Implementation

PointState publishes the best sampled center and its matching objective. The solver owns the partition and all rectangle-selection work. Retain it with Executor::run_with_solver to inspect best_rectangle(), including the normalized center, radius, trisection depths, free-coordinate indices, and rectangle count. This diagnostic performs no evaluations.

Fresh initialization clears the partition and evaluates the midpoint. Reusing a previous point does not warm-start the partition. Exact solver-and-state checkpoints preserve the partition and evaluation counts and skip initialization. Keep the bounds and objective unchanged across that continuation. The evolving solver is serializable with serde.

Supported parameter types are Vec<F>, nalgebra::DVector<F>, ndarray::Array1<F>, and faer::Col<F>, with F = f32 or f64. Custom vectors need only Clone, VectorLen, and VectorIndex<F>; see the API’s Backends section. The solver uses no matrix factorization or BLAS/LAPACK provider.

References

  1. Jones, D. R., Perttunen, C. D., and Stuckman, B. E. (1993). Lipschitzian optimization without the Lipschitz constant. Journal of Optimization Theory and Applications, 79(1), 157–181. doi:10.1007/BF00941892.

Basin v3.1.0 — numerical optimization for Rust.

GitHub docs.rs crates.io