basin 0.7.0

Numerical optimization in pure Rust, with pluggable linear-algebra backends and WASM support.
Documentation
#![cfg(feature = "faer")]

use basin::problems::BoothBoxed;
use basin::{BasicPopulationState, Executor, PopulationState, RandomSearch, State, StepOutcome};
use faer::Col;

fn col2(a: f64, b: f64) -> Col<f64> {
    Col::<f64>::from_fn(2, |i| if i == 0 { a } else { b })
}

#[test]
fn same_seed_yields_identical_trajectory() {
    let result_a = Executor::new(
        BoothBoxed::<Col<f64>>::new(col2(-1.0, -1.0), col2(1.0, 1.0)),
        RandomSearch::new(16, 42),
        BasicPopulationState::<Col<f64>>::with_size(16),
    )
    .max_iter(20)
    .run();

    let result_b = Executor::new(
        BoothBoxed::<Col<f64>>::new(col2(-1.0, -1.0), col2(1.0, 1.0)),
        RandomSearch::new(16, 42),
        BasicPopulationState::<Col<f64>>::with_size(16),
    )
    .max_iter(20)
    .run();

    assert_eq!(result_a.cost(), result_b.cost());
    let a = result_a.param();
    let b = result_b.param();
    assert_eq!(a.nrows(), b.nrows());
    for i in 0..a.nrows() {
        assert_eq!(a[i], b[i]);
    }
}

#[test]
fn converges_to_box_corner_on_tight_booth() {
    let result = Executor::new(
        BoothBoxed::<Col<f64>>::new(col2(-1.0, -1.0), col2(1.0, 1.0)),
        RandomSearch::new(64, 7),
        BasicPopulationState::<Col<f64>>::with_size(64),
    )
    .max_iter(200)
    .run();

    let p = result.param();
    assert!((p[0] - 1.0).abs() < 0.05, "x[0] = {}", p[0]);
    assert!((p[1] - 1.0).abs() < 0.05, "x[1] = {}", p[1]);
}

#[test]
fn elite_keeps_cost_monotone_across_iterations() {
    let mut stepper = Executor::new(
        BoothBoxed::<Col<f64>>::new(col2(-3.0, -3.0), col2(3.0, 3.0)),
        RandomSearch::new(8, 99),
        BasicPopulationState::<Col<f64>>::with_size(8),
    )
    .max_iter(50)
    .into_stepper();

    let mut prev = stepper.state().cost();
    while let StepOutcome::Continue = stepper.step() {
        let current = stepper.state().cost();
        assert!(current <= prev, "cost increased: {prev} → {current}");
        prev = current;
    }
}

#[test]
fn population_invariants_hold_after_iteration() {
    let lambda = 12;

    let mut stepper = Executor::new(
        BoothBoxed::<Col<f64>>::new(col2(-2.0, -2.0), col2(2.0, 2.0)),
        RandomSearch::new(lambda, 1234),
        BasicPopulationState::<Col<f64>>::with_size(lambda),
    )
    .max_iter(10)
    .into_stepper();

    for _ in 0..10 {
        let StepOutcome::Continue = stepper.step() else {
            break;
        };
        let state = stepper.state();
        assert_eq!(state.candidates().len(), lambda);
        assert_eq!(state.costs().len(), lambda);
        for window in state.costs().windows(2) {
            assert!(window[0] <= window[1]);
        }
    }
}