use std::cell::RefCell;
use std::rc::Rc;
use basin::core::rng::Rng;
use basin::problems::{Ackley, Rastrigin};
use basin::{
BasicState, BasinHopping, Executor, InitialState, MaxIter, NelderMead, Problem,
SimplexTolerance, Solver, State, StepTaker, TerminationReason, WarmStart,
};
#[test]
fn same_seed_yields_identical_trajectory() {
let run = |seed: u64| {
Executor::new(
Ackley::<Vec<f64>>::new(),
BasinHopping::new(NelderMead::adaptive(), seed)
.inner_terminate_on(SimplexTolerance::new(1e-8, 1e-8)),
BasicState::new(vec![2.0, 2.0]),
)
.max_iter(40)
.run()
.unwrap()
};
let a = run(42);
let b = run(42);
assert_eq!(a.param(), b.param());
assert_eq!(a.cost(), b.cost());
assert_eq!(a.best_cost(), b.best_cost());
assert_eq!(a.cost_evals(), b.cost_evals());
}
#[test]
fn different_seeds_yield_different_trajectories() {
let run = |seed: u64| {
Executor::new(
Ackley::<Vec<f64>>::new(),
BasinHopping::new(NelderMead::adaptive(), seed)
.with_stepsize(1.5)
.inner_terminate_on(SimplexTolerance::new(1e-8, 1e-8)),
BasicState::new(vec![3.0, 3.0]),
)
.max_iter(20)
.run()
.unwrap()
};
let a = run(1);
let b = run(2);
assert_ne!(a.param(), b.param());
}
#[test]
fn converges_on_ackley_2d() {
let result = Executor::new(
Ackley::<Vec<f64>>::new(),
BasinHopping::new(NelderMead::adaptive(), 7)
.with_stepsize(1.0)
.inner_terminate_on(SimplexTolerance::new(1e-10, 1e-10)),
BasicState::new(vec![2.5, -2.5]),
)
.max_iter(200)
.run()
.unwrap();
assert!(
result.best_cost() < 1e-6,
"Ackley(2-D) best cost {} (expected < 1e-6)",
result.best_cost()
);
}
#[test]
fn success_rate_over_seeds_on_rastrigin_2d() {
let trials = 12;
let successes = (0..trials)
.filter(|&seed| {
let result = Executor::new(
Rastrigin::<Vec<f64>>::new(),
BasinHopping::new(NelderMead::adaptive(), seed)
.with_stepsize(1.5)
.inner_terminate_on(SimplexTolerance::new(1e-10, 1e-10)),
BasicState::new(vec![2.0, 3.0]),
)
.max_iter(150)
.run()
.unwrap();
result.best_cost() < 1.0
})
.count();
assert!(
successes * 2 >= trials as usize,
"basin-hopping reached Rastrigin's global basin in only {successes}/{trials} runs"
);
}
#[test]
fn aggregates_inner_cost_evals() {
let hops = 30_u64;
let n = 2_u64;
let result = Executor::new(
Ackley::<Vec<f64>>::new(),
BasinHopping::new(NelderMead::adaptive(), 3)
.inner_terminate_on(SimplexTolerance::new(1e-8, 1e-8)),
BasicState::new(vec![1.0, 1.0]),
)
.max_iter(hops)
.terminate_on(MaxIter(hops))
.run()
.unwrap();
let floor = (hops + 1) * (n + 1);
assert!(
result.cost_evals() >= floor,
"cost_evals {} below aggregation floor {} — inner evals not folded in",
result.cost_evals(),
floor
);
}
struct RecordingStep {
calls: Rc<RefCell<(u32, u32)>>,
}
impl StepTaker<Vec<f64>, f64> for RecordingStep {
fn take_step<R: Rng + ?Sized>(&mut self, x: &Vec<f64>, _rng: &mut R) -> Vec<f64> {
self.calls.borrow_mut().0 += 1;
x.clone()
}
fn adjust_scale(&mut self, _factor: f64) {
self.calls.borrow_mut().1 += 1;
}
}
#[test]
fn adaptive_step_fires_on_cumulative_interval_schedule() {
let calls = Rc::new(RefCell::new((0_u32, 0_u32)));
let step = RecordingStep {
calls: Rc::clone(&calls),
};
let hops = 35_u64;
let _ = Executor::new(
Ackley::<Vec<f64>>::new(),
BasinHopping::new(NelderMead::adaptive(), 1)
.with_step_taker(step)
.with_adaptive_interval(10)
.inner_terminate_on(SimplexTolerance::new(1e-8, 1e-8)),
BasicState::new(vec![0.5, 0.5]),
)
.max_iter(hops)
.terminate_on(MaxIter(hops))
.run()
.unwrap();
let (take_steps, adjusts) = *calls.borrow();
assert_eq!(take_steps, hops as u32, "one perturbation per hop");
assert_eq!(adjusts, 3, "adjust at cumulative hops 10, 20, 30");
}
struct FailingInner<I>(I);
impl<I, V> InitialState<V> for FailingInner<I>
where
I: InitialState<V>,
{
type State = I::State;
fn seed(&self, x: &V) -> Self::State {
self.0.seed(x)
}
}
impl<I, V> WarmStart<V> for FailingInner<I> where I: WarmStart<V> {}
impl<I, P, S> Solver<P, S> for FailingInner<I>
where
S: State,
I: Solver<P, S>,
{
type Error = I::Error;
fn init(&mut self, problem: &mut Problem<P>, state: S) -> Result<S, Self::Error> {
self.0.init(problem, state)
}
fn next_iter(
&mut self,
_problem: &mut Problem<P>,
state: S,
) -> Result<(S, Option<TerminationReason>), Self::Error> {
Ok((state, Some(TerminationReason::SolverFailed)))
}
}
#[test]
fn failed_inner_solve_does_not_terminate_the_walk() {
let hops = 5_u64;
let result = Executor::new(
Ackley::<Vec<f64>>::new(),
BasinHopping::new(FailingInner(NelderMead::adaptive()), 3)
.inner_terminate_on(SimplexTolerance::new(1e-8, 1e-8)),
BasicState::new(vec![2.0, 2.0]),
)
.max_iter(hops)
.terminate_on(MaxIter(hops))
.run()
.unwrap();
assert_eq!(
result.reason,
TerminationReason::MaxIter,
"walk should run to MaxIter, not stop on the inner's SolverFailed"
);
assert_eq!(result.iter(), hops, "all hops should execute");
}