use basin::problems::BoothBoxed;
use basin::{
BasicPopulationState, Executor, MaxIter, PopulationState, RandomSearch, State, StepOutcome,
};
#[test]
fn same_seed_yields_identical_trajectory() {
let lower = vec![-1.0, -1.0];
let upper = vec![1.0, 1.0];
let result_a = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower.clone(), upper.clone()),
RandomSearch::new(16, 42),
BasicPopulationState::<Vec<f64>>::with_size(16),
)
.max_iter(20)
.run();
let result_b = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower, upper),
RandomSearch::new(16, 42),
BasicPopulationState::<Vec<f64>>::with_size(16),
)
.max_iter(20)
.run();
assert_eq!(result_a.cost(), result_b.cost());
assert_eq!(result_a.param(), result_b.param());
}
#[test]
fn different_seeds_yield_different_trajectories() {
let lower = vec![-5.0, -5.0];
let upper = vec![5.0, 5.0];
let result_a = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower.clone(), upper.clone()),
RandomSearch::new(8, 1),
BasicPopulationState::<Vec<f64>>::with_size(8),
)
.max_iter(5)
.run();
let result_b = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower, upper),
RandomSearch::new(8, 2),
BasicPopulationState::<Vec<f64>>::with_size(8),
)
.max_iter(5)
.run();
assert_ne!(result_a.param(), result_b.param());
}
#[test]
fn converges_to_box_corner_on_tight_booth() {
let lower = vec![-1.0, -1.0];
let upper = vec![1.0, 1.0];
let result = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower, upper),
RandomSearch::new(64, 7),
BasicPopulationState::<Vec<f64>>::with_size(64),
)
.max_iter(200)
.run();
assert!(
(result.param()[0] - 1.0).abs() < 0.05,
"x[0] = {} (expected near upper bound 1)",
result.param()[0]
);
assert!(
(result.param()[1] - 1.0).abs() < 0.05,
"x[1] = {} (expected near upper bound 1)",
result.param()[1]
);
}
#[test]
fn elite_keeps_cost_monotone_across_iterations() {
let lower = vec![-3.0, -3.0];
let upper = vec![3.0, 3.0];
let problem = BoothBoxed::<Vec<f64>>::new(lower, upper);
let mut stepper = Executor::new(
problem,
RandomSearch::new(8, 99),
BasicPopulationState::<Vec<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 = {prev}, current = {current}"
);
prev = current;
}
}
#[test]
fn population_invariants_hold_after_iteration() {
let lower = vec![-2.0, -2.0];
let upper = vec![2.0, 2.0];
let lambda = 12;
let mut stepper = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower, upper),
RandomSearch::new(lambda, 1234),
BasicPopulationState::<Vec<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],
"costs not sorted: {} > {}",
window[0],
window[1]
);
}
}
}
#[test]
fn max_iter_zero_returns_initial_population_best() {
let lower = vec![0.0, 0.0];
let upper = vec![1.0, 1.0];
let result = Executor::new(
BoothBoxed::<Vec<f64>>::new(lower.clone(), upper.clone()),
RandomSearch::new(4, 555),
BasicPopulationState::<Vec<f64>>::with_size(4),
)
.max_iter(0)
.terminate_on(MaxIter(0))
.run();
let p = result.param();
assert_eq!(p.len(), 2);
for (i, &v) in p.iter().enumerate() {
assert!(v >= lower[i] - 1e-12 && v <= upper[i] + 1e-12);
}
}