basin 2.0.0

Numerical optimization in pure Rust, with pluggable linear-algebra backends and WASM support.
Documentation
use super::*;
use crate::{CountsMirror, FirstOrderState, PointState};

fn point(cost: f64) -> PointState<f64> {
    let mut state = PointState::new(cost);
    state.replace(cost, cost);
    state.update_best();
    state
}

#[test]
fn raw_categories_read_the_state_without_legacy_folds() {
    let mut state = point(1.0);
    state.mirror(&EvalCounts {
        cost_evals: 2,
        gradient_evals: 3,
        residual_evals: 5,
        jacobian_evals: 7,
        hessian_evals: 11,
        hessian_product_evals: 13,
    });
    for (kind, count) in EvaluationKind::ALL
        .into_iter()
        .zip([2, 3, 5, 7, 11, 13, 41])
    {
        let mut control = RunControl::new().max_evaluations(kind, count + 1);
        assert_eq!(control.check(&state, &EvalCounts::default()), None);
        control = control.max_evaluations(kind, count);
        assert_eq!(
            control
                .check(&state, &EvalCounts::default())
                .as_ref()
                .map(|stop| stop.code()),
            Some(crate::TerminationCode::MaxEvaluations)
        );
    }
}

#[test]
fn raw_setters_replace_and_preserve_budget_precedence() {
    let mut state = FirstOrderState::new(vec![1.0]);
    state.replace(vec![1.0], 1.0, vec![2.0]).unwrap();
    let counts = EvalCounts {
        cost_evals: 2,
        gradient_evals: 3,
        ..EvalCounts::default()
    };
    state.mirror(&counts);
    state.update_best();
    let mut control = RunControl::new()
        .max_evaluations(EvaluationKind::Cost, 1)
        .max_evaluations(EvaluationKind::Cost, 3);
    assert_eq!(control.check(&state, &counts), None);
    control = control
        .max_evaluations(EvaluationKind::Gradient, 2)
        .max_time(Duration::ZERO)
        .target_objective(1.0);
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::MaxEvaluations)
    );
    control = control.max_evaluations(EvaluationKind::Cost, 2);
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::MaxEvaluations)
    );
    control = control.max_gradient_evals(1);
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::MaxGradientEvals)
    );
    control = control.max_cost_evals(1);
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::MaxCostEvals)
    );
    control = control.max_iter(0);
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::MaxIter)
    );
}

#[test]
fn repeated_checks_and_mid_step_publications_do_not_invent_stall_age() {
    for delta in [0.0, 1.0] {
        let mut state = point(10.0);
        let counts = EvalCounts::default();
        let mut control = RunControl::new().no_objective_improvement(2, delta);
        for _ in 0..4 {
            assert_eq!(control.check(&state, &counts), None);
        }
        state.increment_iter();
        state.replace(8.0, 8.0);
        state.update_best();
        assert_eq!(control.check(&state, &counts), None);
        state.increment_iter();
        for _ in 0..4 {
            assert_eq!(control.check(&state, &counts), None);
        }
        // A clean mid-step improvement retains the iteration number but
        // still resets a positive-delta anchor before further checks.
        state.replace(6.0, 6.0);
        state.update_best();
        assert_eq!(control.check(&state, &counts), None);
        state.increment_iter();
        assert_eq!(control.check(&state, &counts), None);
        state.increment_iter();
        assert_eq!(
            control
                .check(&state, &counts)
                .as_ref()
                .map(|stop| stop.code()),
            Some(crate::TerminationCode::NoImprovement)
        );
    }
}

#[test]
fn objective_anchor_handles_large_decreases_and_negative_infinity() {
    for initial in [f64::MAX, -f64::MAX] {
        let mut state = point(initial);
        let counts = EvalCounts::default();
        let mut control =
            RunControl::new().no_objective_improvement(2, f64::MAX);
        assert_eq!(control.check(&state, &counts), None);
        state.increment_iter();
        let improved = if initial > 0.0 {
            -f64::MAX
        } else {
            f64::NEG_INFINITY
        };
        state.replace(improved, improved);
        state.update_best();
        assert_eq!(control.check(&state, &counts), None);
        state.increment_iter();
        assert_eq!(control.check(&state, &counts), None);
        state.increment_iter();
        assert_eq!(
            control
                .check(&state, &counts)
                .as_ref()
                .map(|stop| stop.code()),
            Some(crate::TerminationCode::NoImprovement)
        );
    }
}

#[test]
fn invalid_objective_control_settings_are_rejected() {
    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        assert!(
            std::panic::catch_unwind(
                || RunControl::<PointState<f64>>::new().target_objective(value)
            )
            .is_err()
        );
    }
    for value in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        assert!(
            std::panic::catch_unwind(|| RunControl::<PointState<f64>>::new()
                .no_objective_improvement(1, value))
            .is_err()
        );
    }
    assert!(
        std::panic::catch_unwind(|| RunControl::<PointState<f64>>::new()
            .no_objective_improvement(0, 0.0))
        .is_err()
    );
}

#[test]
fn reused_controls_reset_clocks_and_stall_anchors() {
    let counts = EvalCounts::default();
    let mut control = RunControl::new()
        .max_time(Duration::from_secs(60))
        .no_objective_improvement(2, 1.0);
    let mut state = point(1.0);
    assert_eq!(control.check(&state, &counts), None);
    for _ in 0..2 {
        state.increment_iter();
        control.check(&state, &counts);
    }
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::NoImprovement)
    );
    control.start = Some(Instant::now() - Duration::from_secs(120));
    assert_eq!(
        control
            .check(&state, &counts)
            .as_ref()
            .map(|stop| stop.code()),
        Some(crate::TerminationCode::MaxTime)
    );
    control.reset();
    assert_eq!(control.start, None);
    let fresh = point(100.0);
    assert_eq!(control.check(&fresh, &counts), None);
}

#[test]
fn direct_hooks_keep_captures_while_factories_reset_history() {
    use std::{cell::RefCell, rc::Rc};
    let direct = Rc::new(RefCell::new(Vec::new()));
    let factory = Rc::new(RefCell::new(Vec::new()));
    let mut control = RunControl::new()
        .stop_when({
            let direct = Rc::clone(&direct);
            let mut calls = 0;
            move |_| {
                calls += 1;
                direct.borrow_mut().push(calls);
                None
            }
        })
        .stop_when_factory({
            let factory = Rc::clone(&factory);
            move || {
                let factory = Rc::clone(&factory);
                let mut calls = 0;
                move |_| {
                    calls += 1;
                    factory.borrow_mut().push(calls);
                    None
                }
            }
        });
    for _ in 0..2 {
        control.reset();
        assert_eq!(control.check(&point(1.0), &EvalCounts::default()), None);
    }
    assert_eq!(*direct.borrow(), [1, 2]);
    assert_eq!(*factory.borrow(), [1, 1]);
}