basin 2.0.0

Numerical optimization in pure Rust, with pluggable linear-algebra backends and WASM support.
Documentation
//! Progress ownership and lifecycle for Powell's quadratic-model solvers.

use basin::{
    Bobyqa, CostFunction, CountsMirror, EvalCounts, Executor, Lincoa, Newuoa,
    PointState, 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::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 Bowl {
    type Param = Vec<f64>;
    type Output = f64;
    type Error = Infallible;
    fn cost(&self, x: &Vec<f64>) -> Result<f64, Infallible> {
        Ok(x.iter()
            .enumerate()
            .map(|(i, x)| (i + 1) as f64 * x.powi(2))
            .sum())
    }
}

macro_rules! lifecycle {
    ($module:ident, $solver:ident, $state:ident, $prepare:expr) => {
        mod $module {
            use super::*;
            #[test]
            fn fresh_run_resets_bookkeeping_and_reevaluates() {
                let prior = Executor::from_start(
                    ($prepare)(Bowl::new(2)),
                    $solver::new(),
                    vec![2.0; 2],
                )
                .max_iter(3)
                .run_with_solver()
                .unwrap();
                assert_eq!(prior.iter(), 3);
                let fresh = Executor::new(
                    ($prepare)(Bowl::new(2)),
                    prior.solver,
                    prior.state,
                )
                .max_iter(0)
                .run_with_solver()
                .unwrap();
                assert_eq!(fresh.iter(), 0);
                assert_eq!(fresh.state.best_iter(), 0);
                assert_eq!(fresh.counts.cost_evals, 5);
                assert_eq!(fresh.state.cost_evals(), 5);
                assert_eq!(fresh.solver.rho(), Some(1.0));
                assert_eq!(fresh.state.counts(), &fresh.counts);
            }
            #[test]
            fn cost_reader_reports_only_cost_calls() {
                let mut state = $state::<Vec<f64>>::new(vec![2.0; 2]);
                let mut counts = EvalCounts::default();
                counts.cost_evals = 2;
                counts.gradient_evals = 3;
                counts.residual_evals = 5;
                counts.jacobian_evals = 7;
                counts.hessian_evals = 11;
                counts.hessian_product_evals = 13;
                state.replace(vec![2.0; 2], 12.0);
                state.mirror(&counts);
                state.update_best();
                assert_eq!(state.cost_evals(), 2);
                assert_eq!(state.counts(), &counts);
                assert_eq!(state.best_counts(), Some(&counts));
                assert_eq!(state.best_cost_evals(), 2);
                state.increment_iter();
                state.mirror(&EvalCounts::default());
                state.update_best();
                assert_eq!(state.best_counts(), Some(&counts));
                assert_eq!(state.best_iter(), 0);
            }

            #[test]
            fn fresh_solver_rebuilds_model_for_a_different_dimension() {
                let used = Executor::from_start(
                    ($prepare)(Bowl::new(2)),
                    $solver::new(),
                    vec![2.0; 2],
                )
                .max_iter(4)
                .run_with_solver()
                .unwrap();
                let fresh = Executor::new(
                    ($prepare)(Bowl::new(3)),
                    used.solver,
                    PointState::new(vec![1.5; 3]),
                )
                .require_evaluated_state()
                .max_iter(10)
                .run_with_solver()
                .unwrap();
                let expected = Executor::from_start(
                    ($prepare)(Bowl::new(3)),
                    $solver::new(),
                    vec![1.5; 3],
                )
                .max_iter(10)
                .run_with_solver()
                .unwrap();
                assert_eq!(fresh.state, expected.state);
                assert_eq!(fresh.counts, expected.counts);
                assert_eq!(fresh.solver.rho(), expected.solver.rho());
            }

            #[test]
            fn owned_checkpoints_continue_without_reevaluating_or_resetting() {
                let make = || {
                    Executor::from_start(
                        ($prepare)(Bowl::new(2)),
                        $solver::new().with_final_radius(1e-12),
                        vec![2.0; 2],
                    )
                };
                let expected = make().max_iter(20).run_with_solver().unwrap();
                for split in [0, 1, 3, 7, 12] {
                    let checkpoint = make()
                        .max_iter(split)
                        .run_with_solver()
                        .unwrap()
                        .into_checkpoint();
                    let resumed = Executor::resume_from_checkpoint(
                        ($prepare)(Bowl::new(2)),
                        checkpoint,
                    )
                    .require_evaluated_state()
                    .max_iter(20)
                    .run_with_solver()
                    .unwrap();
                    assert_eq!(
                        resumed.state, expected.state,
                        "split at {split}"
                    );
                    assert_eq!(resumed.counts, expected.counts);
                    assert_eq!(resumed.solver.rho(), expected.solver.rho());
                }
            }

            #[cfg(feature = "serde")]
            #[test]
            fn serialized_checkpoints_preserve_the_model_and_radius_schedule() {
                type Checkpoint =
                    basin::ExactCheckpoint<$solver, PointState<Vec<f64>>>;
                let make = || {
                    Executor::from_start(
                        ($prepare)(Bowl::new(2)),
                        $solver::new().with_final_radius(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 resumed = Executor::resume_from_checkpoint(
                    ($prepare)(Bowl::new(2)),
                    restored,
                )
                .require_evaluated_state()
                .max_iter(20)
                .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()
                );
                let fresh = Executor::new(
                    ($prepare)(Bowl::new(2)),
                    resumed.solver,
                    PointState::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!(newuoa, Newuoa, PointState, basin::SamplingDomain::new);
lifecycle!(bobyqa, Bobyqa, PointState, |p| p);
lifecycle!(lincoa, Lincoa, PointState, |p| p);