basin 2.0.0

Numerical optimization in pure Rust, with pluggable linear-algebra backends and WASM support.
Documentation
//! Distribution ownership, coherent CMA progress, and restart contracts.
use basin::{
    BoundedCmaEs, CmaEs, CostFunction, CountsMirror, DenseMatrix, EvalCounts,
    Executor, PopulationProgress, State,
};
use std::convert::Infallible;
struct Sphere {
    lower: Vec<f64>,
    upper: Vec<f64>,
}
impl Sphere {
    fn new(n: usize) -> Self {
        Self {
            lower: vec![-0.5; n],
            upper: vec![0.5; n],
        }
    }
}

impl basin::ProblemConstraints for Sphere {
    type Constraints = basin::Constraints<basin::BoxBounds<Vec<f64>>>;
    fn constraint_set(
        &self,
    ) -> <Self::Constraints as basin::ConstraintDescription>::Borrowed<'_> {
        basin::Constraints::new().with_bounds(basin::BoxBounds::new(
            Some(&self.lower),
            Some(&self.upper),
        ))
    }
}
impl CostFunction for Sphere {
    type Param = Vec<f64>;
    type Output = f64;
    type Error = Infallible;
    fn cost(&self, x: &Vec<f64>) -> Result<f64, Infallible> {
        assert_eq!(x.len(), self.lower.len());
        Ok(x.iter().map(|v| v * v).sum())
    }
}

#[test]
fn fresh_run_reevaluates_and_resets_distribution() {
    let prior = Executor::new(
        basin::SamplingDomain::new(Sphere::new(2)),
        CmaEs::<_, DenseMatrix>::new(91, 0.7).with_lambda(6),
        PopulationProgress::from_point(vec![0.4; 2]),
    )
    .max_iter(4)
    .run_with_solver()
    .unwrap();
    let fresh = Executor::new(
        basin::SamplingDomain::new(Sphere::new(2)),
        prior.solver,
        prior.state,
    )
    .max_iter(0)
    .run_with_solver()
    .unwrap();
    assert_eq!(fresh.iter(), 0);
    assert_eq!(fresh.counts.cost_evals, 7);
}
#[test]
fn categories_are_raw() {
    let mut state = PopulationProgress::<Vec<f64>>::from_point(vec![0.4; 2]);
    let mut counts = EvalCounts::default();
    counts.cost_evals = 2;
    counts.gradient_evals = 3;
    counts.residual_evals = 4;
    counts.jacobian_evals = 5;
    counts.hessian_evals = 6;
    counts.hessian_product_evals = 7;
    state.mirror(&counts);
    assert_eq!(state.cost_evals(), 2);
}
#[test]
fn bounded_members_are_evaluated_points_with_raw_costs() {
    let result = Executor::new(
        Sphere::new(2),
        BoundedCmaEs::<_, DenseMatrix>::new(91, 2.0).with_lambda(6),
        PopulationProgress::from_point(vec![0.4; 2]),
    )
    .max_iter(3)
    .run()
    .unwrap();
    for (x, &f) in result.state.candidates().iter().zip(result.state.costs()) {
        assert!(x.iter().all(|v| (-0.5..=0.5).contains(v)));
        assert_eq!(f, x.iter().map(|v| v * v).sum::<f64>());
    }
}

fn coherent(state: &PopulationProgress<Vec<f64>>, bounded: bool) {
    assert_eq!(state.candidates().len(), state.costs().len());
    for (x, f) in state
        .candidates()
        .iter()
        .zip(state.costs().iter().copied())
        .chain(state.current())
        .chain(state.best())
    {
        if bounded {
            assert!(x.iter().all(|v| (-0.5..=0.5).contains(v)));
        }
        assert_eq!(f, x.iter().map(|v| v * v).sum::<f64>());
    }
}

macro_rules! continuation {
    ($name:ident, $solver:expr, $bounded:expr, $prepare:expr) => {
        #[test]
        fn $name() {
            let make = || {
                Executor::new(
                    ($prepare)(Sphere::new(2)),
                    $solver,
                    PopulationProgress::from_point(vec![0.4; 2]),
                )
            };
            let expected = make().max_iter(18).run_with_solver().unwrap();
            coherent(&expected.state, $bounded);
            for split in [0, 1, 5, 17] {
                let checkpoint = make()
                    .max_iter(split)
                    .run_with_solver()
                    .unwrap()
                    .into_checkpoint();
                let resumed = Executor::resume_from_checkpoint(
                    ($prepare)(Sphere::new(2)),
                    checkpoint,
                )
                .require_evaluated_state()
                .max_iter(18)
                .run_with_solver()
                .unwrap();
                coherent(&resumed.state, $bounded);
                assert_eq!(resumed.state, expected.state);
                assert_eq!(resumed.counts, expected.counts);
                #[cfg(feature = "serde")]
                assert_eq!(
                    postcard::to_allocvec(&resumed.solver).unwrap(),
                    postcard::to_allocvec(&expected.solver).unwrap()
                );
            }
            #[cfg(feature = "serde")]
            {
                // Let inference recover the full inner solver type without erasing it.
                fn restore<
                    T: serde::Serialize + serde::de::DeserializeOwned,
                >(
                    value: T,
                ) -> T {
                    postcard::from_bytes(
                        &postcard::to_allocvec(&value).unwrap(),
                    )
                    .unwrap()
                }
                let checkpoint = restore(
                    make()
                        .max_iter(7)
                        .run_with_solver()
                        .unwrap()
                        .into_checkpoint(),
                );
                let resumed = Executor::resume_from_checkpoint(
                    ($prepare)(Sphere::new(2)),
                    checkpoint,
                )
                .require_evaluated_state()
                .max_iter(18)
                .run_with_solver()
                .unwrap();
                assert_eq!(resumed.state, expected.state);
                assert_eq!(resumed.counts, expected.counts);
                assert_eq!(
                    postcard::to_allocvec(&resumed.solver).unwrap(),
                    postcard::to_allocvec(&expected.solver).unwrap()
                );
            }
        }
    };
}
continuation!(
    unbounded_owned_and_serialized_continuation,
    CmaEs::<_, DenseMatrix>::new(91, 0.7)
        .with_lambda(6)
        .with_stds(vec![1.0, 0.3]),
    false,
    basin::SamplingDomain::new
);
continuation!(
    bounded_owned_and_serialized_continuation,
    BoundedCmaEs::<_, DenseMatrix>::new(91, 0.7)
        .with_lambda(6)
        .with_stds(vec![1.0, 0.3]),
    true,
    |p| p
);
continuation!(
    injected_owned_and_serialized_continuation,
    basin::CmaInject::with_inner_solver(
        CmaEs::<_, DenseMatrix>::new(91, 0.7).with_lambda(6),
        basin::NelderMead::adaptive()
    )
    .with_k(2)
    .with_inner_max_iter(3),
    false,
    basin::SamplingDomain::new
);
continuation!(
    bounded_injected_owned_and_serialized_continuation,
    basin::BoundedCmaInject::with_inner_solver(
        BoundedCmaEs::<_, DenseMatrix>::new(91, 0.7).with_lambda(6),
        basin::RelaxedInner::new(basin::NelderMead::adaptive())
    )
    .with_k(2)
    .with_inner_max_iter(3),
    true,
    |p| p
);

macro_rules! fresh {
    ($name:ident, $kind:ident, $prepare:expr) => {
        #[test]
        fn $name() {
            let make = || $kind::<_, DenseMatrix>::new(91, 0.7).with_lambda(6);
            let prior = Executor::from_start(
                ($prepare)(Sphere::new(2)),
                make(),
                vec![0.4; 2],
            )
            .max_iter(9)
            .run_with_solver()
            .unwrap();
            let warm = prior.state.param().clone();
            let actual = Executor::new(
                ($prepare)(Sphere::new(2)),
                prior.solver,
                prior.state,
            )
            .max_iter(0)
            .run_with_solver()
            .unwrap();
            let expected =
                Executor::from_start(($prepare)(Sphere::new(2)), make(), warm)
                    .max_iter(0)
                    .run_with_solver()
                    .unwrap();
            assert_eq!(actual.state, expected.state);
            assert_eq!(actual.solver.sigma(), Some(0.7));
            assert_eq!(actual.counts.cost_evals, 7);
            #[cfg(feature = "serde")]
            assert_eq!(
                postcard::to_allocvec(&actual.solver).unwrap(),
                postcard::to_allocvec(&expected.solver).unwrap()
            );
            let resized = Executor::from_start(
                ($prepare)(Sphere::new(3)),
                actual.solver,
                vec![0.2; 3],
            )
            .max_iter(0)
            .run_with_solver()
            .unwrap();
            let expected = Executor::from_start(
                ($prepare)(Sphere::new(3)),
                make(),
                vec![0.2; 3],
            )
            .max_iter(0)
            .run_with_solver()
            .unwrap();
            assert_eq!(resized.state, expected.state);
            #[cfg(feature = "serde")]
            assert_eq!(
                postcard::to_allocvec(&resized.solver).unwrap(),
                postcard::to_allocvec(&expected.solver).unwrap()
            );
        }
    };
}
fresh!(
    unbounded_fresh_model_and_dimension_reset,
    CmaEs,
    basin::SamplingDomain::new
);
fresh!(bounded_fresh_model_and_dimension_reset, BoundedCmaEs, |p| p);

#[test]
fn unevaluated_mean_survives_reset_without_publishing_a_cost() {
    let mut state = PopulationProgress::<Vec<f64>>::from_point(vec![0.4; 2]);
    assert_eq!(state.current(), None);
    assert_eq!(state.best(), None);
    assert_eq!(state.evaluated_members(), None);
    state.reset();
    assert_eq!(state.param(), &vec![0.4; 2]);
    let mut state = Executor::new(
        basin::SamplingDomain::new(Sphere::new(2)),
        CmaEs::<_, DenseMatrix>::new(91, 0.7),
        state,
    )
    .max_iter(3)
    .run()
    .unwrap()
    .state;
    let mean = state.param().clone();
    let members = state.candidates().to_vec();
    state.reset();
    assert_eq!(state.param(), &mean);
    assert_eq!(state.candidates(), members);
    assert_eq!(state.current(), None);
    assert_eq!(state.best(), None);
    assert_eq!(state.counts(), &EvalCounts::default());
}

#[test]
fn nonfinite_population_has_no_objective_incumbent() {
    struct Nonfinite(f64);

    impl basin::ProblemConstraints for Nonfinite {
        type Constraints = basin::NoConstraints;
        fn constraint_set(
            &self,
        ) -> <Self::Constraints as basin::ConstraintDescription>::Borrowed<'_>
        {
            basin::Constraints::new()
        }
    }
    impl CostFunction for Nonfinite {
        type Param = Vec<f64>;
        type Output = f64;
        type Error = Infallible;
        fn cost(&self, _: &Vec<f64>) -> Result<f64, Infallible> {
            Ok(self.0)
        }
    }
    for cost in [f64::NAN, f64::INFINITY] {
        let state = Executor::from_start(
            Nonfinite(cost),
            CmaEs::<_, DenseMatrix>::new(91, 0.7),
            vec![0.4; 2],
        )
        .max_iter(1)
        .run()
        .unwrap()
        .state;
        assert!(state.current().is_some());
        assert_eq!(state.best(), None);
        assert_eq!(state.best_counts(), None);
    }
}

#[test]
fn initial_scale_must_be_finite_and_positive() {
    for sigma in [0.0, -1.0, f64::NAN, f64::INFINITY] {
        assert!(
            std::panic::catch_unwind(|| CmaEs::<Vec<f64>, DenseMatrix>::new(
                1, sigma
            ))
            .is_err()
        );
        assert!(
            std::panic::catch_unwind(|| {
                BoundedCmaEs::<Vec<f64>, DenseMatrix>::new(1, sigma)
            })
            .is_err()
        );
    }
}