#![cfg(feature = "ndarray")]
use basin::problems::BoothBoxed;
use basin::{
BasicPopulationState, BoundedCmaEs, Executor, PopulationState, StepOutcome, TerminationReason,
};
use ndarray::{Array1, Array2};
#[test]
fn same_seed_yields_identical_trajectory() {
let lower = Array1::from_vec(vec![-5.0, -5.0]);
let upper = Array1::from_vec(vec![5.0, 5.0]);
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let result_a = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower.clone(), upper.clone()),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0.clone(), 0.5, 42),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(30)
.run();
let result_b = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.5, 42),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(30)
.run();
assert_eq!(result_a.cost(), result_b.cost());
assert_eq!(result_a.param(), result_b.param());
}
#[test]
fn with_stds_ones_matches_default() {
let lower = Array1::from_vec(vec![-5.0, -5.0]);
let upper = Array1::from_vec(vec![5.0, 5.0]);
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let ones = Array1::from_elem(2, 1.0);
let default = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower.clone(), upper.clone()),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0.clone(), 0.5, 42),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(40)
.run();
let with_ones = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.5, 42).with_stds(ones),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(40)
.run();
assert_eq!(default.cost(), with_ones.cost());
assert_eq!(default.param(), with_ones.param());
assert_eq!(default.reason, with_ones.reason);
}
#[test]
fn with_stds_anisotropic_recovers_minimum() {
let lower = Array1::from_vec(vec![-5.0, -5.0]);
let upper = Array1::from_vec(vec![5.0, 5.0]);
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let stds = Array1::from_vec(vec![0.5, 2.0]);
let result = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.5, 7).with_stds(stds),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(400)
.run();
let p = result.param();
assert!(
(p[0] - 1.0).abs() < 1e-2 && (p[1] - 3.0).abs() < 1e-2,
"iterate = ({}, {}), expected ≈ (1, 3)",
p[0],
p[1]
);
}
#[test]
#[should_panic(expected = "stds.len() == initial_mean.len()")]
fn with_stds_panics_on_length_mismatch() {
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let _ = BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.5, 42)
.with_stds(Array1::from_vec(vec![1.0]));
}
#[test]
fn slack_bounds_recover_unconstrained_minimum() {
let lower = Array1::from_vec(vec![-5.0, -5.0]);
let upper = Array1::from_vec(vec![5.0, 5.0]);
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let result = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.5, 7),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(400)
.run();
let p = result.param();
assert!(
(p[0] - 1.0).abs() < 1e-2 && (p[1] - 3.0).abs() < 1e-2,
"iterate = ({}, {}), expected ≈ (1, 3)",
p[0],
p[1]
);
}
#[test]
fn tight_bounds_converge_to_box_corner() {
let lower = Array1::from_vec(vec![-1.0, -1.0]);
let upper = Array1::from_vec(vec![1.0, 1.0]);
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let result = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.3, 11),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(800)
.run();
let p = result.param();
assert!(
(p[0] - 1.0).abs() < 1e-2 && (p[1] - 1.0).abs() < 1e-2,
"iterate = ({}, {}), expected ≈ (1, 1) box corner",
p[0],
p[1]
);
}
#[test]
fn infeasible_initial_mean_converges_to_box_corner() {
let lower = Array1::from_vec(vec![-1.0, -1.0]);
let upper = Array1::from_vec(vec![1.0, 1.0]);
let m0 = Array1::from_vec(vec![10.0, 10.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let result = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.3, 5),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(800)
.run();
let p = result.param();
assert!(
(p[0] - 1.0).abs() < 1e-2 && (p[1] - 1.0).abs() < 1e-2,
"iterate = ({}, {}), expected ≈ (1, 1) box corner from infeasible start",
p[0],
p[1]
);
}
#[test]
fn slack_bounds_terminate_solver_converged_on_tol_x() {
let lower = Array1::from_vec(vec![-10.0, -10.0]);
let upper = Array1::from_vec(vec![10.0, 10.0]);
let m0 = Array1::from_vec(vec![0.0, 0.0]);
let lambda = BoundedCmaEs::<Array1<f64>, Array2<f64>>::default_lambda(2);
let result = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.3, 11),
BasicPopulationState::<Array1<f64>>::with_size(lambda),
)
.max_iter(2000)
.run();
assert_eq!(result.reason, TerminationReason::SolverConverged);
}
#[test]
fn population_invariants_hold_after_iteration() {
let lower = Array1::from_vec(vec![-1.0, -1.0]);
let upper = Array1::from_vec(vec![1.0, 1.0]);
let m0 = Array1::from_vec(vec![0.3, 0.4]);
let lambda = 12;
let mut stepper = Executor::new(
BoothBoxed::<Array1<f64>>::new(lower, upper),
BoundedCmaEs::<Array1<f64>, Array2<f64>>::new(m0, 0.5, 1234).with_lambda(lambda),
BasicPopulationState::<Array1<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]);
}
}
}