basin 2.0.0

Numerical optimization in pure Rust, with pluggable linear-algebra backends and WASM support.
Documentation
//! MADS progress and solver-owned mesh schedules.

use basin::{CostFunction, Executor, Mads, Problem, Solver};
use basin::{PointState, SelectedState, State};
use std::convert::Infallible;

struct Bowl {
    lower: Vec<f64>,
    upper: Vec<f64>,
}
impl Bowl {
    fn new(n: usize) -> Self {
        Self {
            lower: vec![-5.0; n],
            upper: vec![5.0; n],
        }
    }
}

impl basin::ProblemConstraints for Bowl {
    type Constraints = basin::NoConstraints;
    fn constraint_set(&self) -> basin::NoConstraints {
        basin::NoConstraints::EMPTY
    }
}
fn bounded(
    n: usize,
) -> basin::Constrained<Bowl, basin::Constraints<basin::BoxBounds<Vec<f64>>>> {
    let bowl = Bowl::new(n);
    let bounds = basin::BoxBounds::new(
        Some(bowl.lower.clone()),
        Some(bowl.upper.clone()),
    );
    basin::Constrained::new(bowl, basin::Constraints::new().with_bounds(bounds))
}
fn constrained(
    n: usize,
) -> basin::Constrained<
    Bowl,
    basin::Constraints<
        basin::NoBounds,
        basin::NoLinearConstraints,
        basin::NonlinearBlocks<basin::NoNonlinearBlock, basin::NonlinearBlock>,
    >,
> {
    basin::Constrained::new(
        Bowl::new(n),
        basin::Constraints::new().with_nonlinear_inequalities(1),
    )
}
impl CostFunction for Bowl {
    type Param = Vec<f64>;
    type Output = f64;
    type Error = Infallible;
    fn cost(&self, x: &Vec<f64>) -> Result<f64, Infallible> {
        Ok(x.iter().map(|x| x * x).sum())
    }
}

impl basin::NonlinearInequalityValues for Bowl {
    type Param = Vec<f64>;
    type Values = Vec<f64>;
    type Error = Infallible;
    fn nonlinear_inequalities(
        &self,
        x: &Self::Param,
    ) -> Result<Self::Values, basin::ConstraintSolveError<Self::Error>> {
        { Ok(vec![1.0 - x[0]]) }
            .map_err(basin::ConstraintSolveError::Evaluation)
    }
}

macro_rules! fresh {
    ($test:ident, $solver:expr, $problem:expr) => {
        #[test]
        fn $test() {
            let prior =
                Executor::from_start(($problem)(2), $solver, vec![2.0; 2])
                    .max_iter(4)
                    .run_with_solver()
                    .unwrap();
            assert_eq!(prior.iter(), 4);
            let fresh = Executor::new(($problem)(2), prior.solver, prior.state)
                .max_iter(0)
                .run_with_solver()
                .unwrap();
            assert_eq!(fresh.iter(), 0);
            assert_eq!(fresh.counts.cost_evals, 1);
        }
    };
}
fresh!(unbounded_fresh_bookkeeping, Mads::new(), Bowl::new);
fresh!(bounded_fresh_bookkeeping, Mads::new().bounded(), bounded);
fresh!(
    constrained_fresh_bookkeeping,
    Mads::new().constrained(),
    constrained
);

#[test]
fn constraints_are_counted_as_residual_calls() {
    let mut problem = Problem::new(constrained(2));
    let mut solver = Mads::new().constrained();
    let _ = solver
        .init(&mut problem, basin::SelectedState::new(vec![0.0; 2]))
        .unwrap();
    assert_eq!(problem.counts().cost_evals, 1);
    assert_eq!(problem.counts().residual_evals, 1);
}

macro_rules! lifecycle {
    ($module:ident, $solver:expr, $mode:ty, $state:ident, $problem:expr) => {
        mod $module {
            use super::*;
            #[test]
            fn exact_checkpoints_keep_mesh_and_halton_history() {
                let make = || {
                    Executor::from_start(
                        ($problem)(2),
                        $solver.with_minimum_poll_size(1e-12),
                        vec![2.0; 2],
                    )
                };
                let expected = make().max_iter(20).run_with_solver().unwrap();
                for split in [0, 1, 5, 13] {
                    let checkpoint = make()
                        .max_iter(split)
                        .run_with_solver()
                        .unwrap()
                        .into_checkpoint();
                    let actual = Executor::resume_from_checkpoint(
                        ($problem)(2),
                        checkpoint,
                    )
                    .require_evaluated_state()
                    .max_iter(20)
                    .run_with_solver()
                    .unwrap();
                    assert_eq!(actual.state, expected.state);
                    assert_eq!(actual.counts, expected.counts);
                    assert_eq!(
                        actual.solver.poll_size(),
                        expected.solver.poll_size()
                    );
                    assert_eq!(
                        actual.solver.mesh_index(),
                        expected.solver.mesh_index()
                    );
                }
            }

            #[test]
            fn fresh_runs_rebuild_for_a_new_dimension() {
                let used =
                    Executor::from_start(($problem)(2), $solver, vec![2.0; 2])
                        .max_iter(4)
                        .run_with_solver()
                        .unwrap();
                let actual = Executor::new(
                    ($problem)(3),
                    used.solver,
                    $state::new(vec![1.5; 3]),
                )
                .max_iter(10)
                .run_with_solver()
                .unwrap();
                let expected =
                    Executor::from_start(($problem)(3), $solver, vec![1.5; 3])
                        .max_iter(10)
                        .run_with_solver()
                        .unwrap();
                assert_eq!(actual.state, expected.state);
                assert_eq!(actual.counts, expected.counts);
                assert_eq!(
                    actual.solver.mesh_index(),
                    expected.solver.mesh_index()
                );
            }

            #[cfg(feature = "serde")]
            #[test]
            fn serialized_models_resume_exactly_and_reset_on_a_fresh_run() {
                type Checkpoint =
                    basin::ExactCheckpoint<Mads<$mode>, $state<Vec<f64>>>;
                let make = || {
                    Executor::from_start(
                        ($problem)(2),
                        $solver.with_minimum_poll_size(1e-12),
                        vec![2.0; 2],
                    )
                };
                let expected = make().max_iter(20).run_with_solver().unwrap();
                let checkpoint = make()
                    .max_iter(7)
                    .run_with_solver()
                    .unwrap()
                    .into_checkpoint();
                let bytes = postcard::to_allocvec(&checkpoint).unwrap();
                let restored: Checkpoint =
                    postcard::from_bytes(&bytes).unwrap();
                let actual =
                    Executor::resume_from_checkpoint(($problem)(2), restored)
                        .max_iter(20)
                        .run_with_solver()
                        .unwrap();
                assert_eq!(actual.state, expected.state);
                assert_eq!(actual.counts, expected.counts);
                assert_eq!(
                    postcard::to_allocvec(&actual.solver).unwrap(),
                    postcard::to_allocvec(&expected.solver).unwrap()
                );
                let fresh = Executor::new(
                    ($problem)(2),
                    actual.solver,
                    $state::new(vec![2.0; 2]),
                )
                .max_iter(0)
                .run_with_solver()
                .unwrap();
                let initialized = make().max_iter(0).run_with_solver().unwrap();
                assert_eq!(fresh.state, initialized.state);
                assert_eq!(
                    postcard::to_allocvec(&fresh.solver).unwrap(),
                    postcard::to_allocvec(&initialized.solver).unwrap()
                );
            }
        }
    };
}
lifecycle!(
    unbounded,
    Mads::new(),
    basin::solver::nelder_mead::Unbounded,
    PointState,
    Bowl::new
);
lifecycle!(
    bounded,
    Mads::new().bounded(),
    basin::solver::mads::Bounded,
    PointState,
    bounded
);
lifecycle!(
    constrained,
    Mads::new().constrained(),
    basin::solver::mads::Constrained,
    SelectedState,
    constrained
);

#[test]
fn constrained_selection_preserves_feasibility_and_publication_counts() {
    let mut stepper = Executor::from_start(
        constrained(2),
        Mads::new().constrained(),
        vec![0.0; 2],
    )
    .require_evaluated_state()
    .max_iter(20)
    .into_stepper()
    .unwrap();
    let initial_cost = stepper.state().cost();
    while stepper.finished().is_none() {
        let previous = stepper.state().clone();
        stepper.step().unwrap();
        let state = stepper.state();
        let (x, cost, violation) = state.current().unwrap();
        assert_eq!(cost, x.iter().map(|x| x * x).sum::<f64>());
        assert_eq!(violation.value, (1.0 - x[0]).max(0.0).powi(2));
        assert_eq!(*violation.measure(), basin::ViolationMeasure::SquaredL2);
        assert_eq!(*violation.scaling(), basin::ViolationScaling::Unscaled);
        assert_eq!(
            *violation.transformation(),
            basin::ViolationTransformation::Identity
        );
        assert_eq!(state.current(), state.best());
        assert_eq!(state.counts(), stepper.counts());
        assert_eq!(state.counts().residual_evals, state.cost_evals());
        if state.current() == previous.current() {
            assert_eq!(state.best_iter(), previous.best_iter());
            assert_eq!(state.best_counts(), previous.best_counts());
        } else {
            assert_eq!(state.best_iter(), state.iter());
            assert_eq!(state.best_counts(), Some(state.counts()));
        }
    }
    assert!(stepper.state().cost() > initial_cost);
    assert_eq!(stepper.state().current().unwrap().2.value, 0.0);
}