use crate::core::inner::{InitialState, InnerExecutor, WarmStart};
use crate::core::math::{SampleUniformBox, Scalar, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, Rng, RngExt, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::{BasicState, CountsMirror, State};
use crate::core::termination::{TerminationCriterion, TerminationReason};
use core::ops::{Index, IndexMut};
pub trait StepTaker<V, F = f64> {
fn take_step<R: Rng + ?Sized>(&mut self, x: &V, rng: &mut R) -> V;
fn adjust_scale(&mut self, _factor: F) {}
}
pub trait AcceptanceTest<F = f64> {
fn accept<R: Rng + ?Sized>(&self, f_new: F, f_old: F, rng: &mut R) -> bool;
}
#[derive(Clone, Debug)]
pub struct RandomDisplacement<F = f64> {
stepsize: F,
}
impl<F: Scalar> RandomDisplacement<F> {
pub fn new(stepsize: F) -> Self {
assert!(
stepsize > F::zero(),
"RandomDisplacement requires stepsize > 0, got {stepsize:?}"
);
Self { stepsize }
}
pub fn stepsize(&self) -> F {
self.stepsize
}
}
impl<V, F> StepTaker<V, F> for RandomDisplacement<F>
where
F: Scalar,
V: VectorLen
+ Clone
+ Index<usize, Output = F>
+ IndexMut<usize, Output = F>
+ SampleUniformBox,
{
fn take_step<R: Rng + ?Sized>(&mut self, x: &V, rng: &mut R) -> V {
let mut lower = x.clone();
let mut upper = x.clone();
for i in 0..x.vec_len() {
lower[i] = x[i] - self.stepsize;
upper[i] = x[i] + self.stepsize;
}
V::sample_uniform_box(&lower, &upper, rng)
}
fn adjust_scale(&mut self, factor: F) {
self.stepsize = self.stepsize * factor;
}
}
#[derive(Clone, Debug)]
pub struct Metropolis<F = f64> {
beta: F,
}
impl<F: Scalar> Metropolis<F> {
pub fn new(temperature: F) -> Self {
assert!(
temperature > F::zero(),
"Metropolis requires temperature > 0, got {temperature:?}"
);
Self {
beta: F::one() / temperature,
}
}
}
impl<F: Scalar> AcceptanceTest<F> for Metropolis<F> {
fn accept<R: Rng + ?Sized>(&self, f_new: F, f_old: F, rng: &mut R) -> bool {
if f_new <= f_old {
return true;
}
let w = (-(f_new - f_old) * self.beta).exp();
let r = F::from_f64(rng.random::<f64>()).unwrap();
w >= r
}
}
fn accept_guard(new_success: bool, incumbent_success: bool) -> bool {
new_success || !incumbent_success
}
pub struct BasinHopping<I, V, F = f64, S = RandomDisplacement<F>, A = Metropolis<F>>
where
F: Scalar,
I: WarmStart<V>,
{
inner: InnerExecutor<<I as InitialState<V>>::State, I>,
step: S,
accept: A,
rng: ChaCha8Rng,
adaptive: bool,
interval: u64,
target_accept_rate: F,
stepwise_factor: F,
nstep: u64,
naccept: u64,
incumbent_success: bool,
}
impl<I, V, F> BasinHopping<I, V, F, RandomDisplacement<F>, Metropolis<F>>
where
F: Scalar,
I: WarmStart<V>,
<I as InitialState<V>>::State: State + CountsMirror,
{
pub fn new(inner: I, seed: u64) -> Self {
let half = F::from_f64(0.5).unwrap();
Self {
inner: InnerExecutor::new(inner),
step: RandomDisplacement::new(half),
accept: Metropolis::new(F::one()),
rng: ChaCha8Rng::seed_from_u64(seed),
adaptive: true,
interval: 50,
target_accept_rate: half,
stepwise_factor: F::from_f64(0.9).unwrap(),
nstep: 0,
naccept: 0,
incumbent_success: true,
}
}
pub fn with_stepsize(mut self, stepsize: F) -> Self {
self.step = RandomDisplacement::new(stepsize);
self
}
pub fn with_temperature(mut self, temperature: F) -> Self {
self.accept = Metropolis::new(temperature);
self
}
}
impl<I, V, F, S, A> BasinHopping<I, V, F, S, A>
where
F: Scalar,
I: WarmStart<V>,
<I as InitialState<V>>::State: State + CountsMirror,
{
pub fn with_step_taker<S2>(self, step: S2) -> BasinHopping<I, V, F, S2, A> {
BasinHopping {
inner: self.inner,
step,
accept: self.accept,
rng: self.rng,
adaptive: self.adaptive,
interval: self.interval,
target_accept_rate: self.target_accept_rate,
stepwise_factor: self.stepwise_factor,
nstep: self.nstep,
naccept: self.naccept,
incumbent_success: self.incumbent_success,
}
}
pub fn with_acceptance_test<A2>(self, accept: A2) -> BasinHopping<I, V, F, S, A2> {
BasinHopping {
inner: self.inner,
step: self.step,
accept,
rng: self.rng,
adaptive: self.adaptive,
interval: self.interval,
target_accept_rate: self.target_accept_rate,
stepwise_factor: self.stepwise_factor,
nstep: self.nstep,
naccept: self.naccept,
incumbent_success: self.incumbent_success,
}
}
pub fn with_adaptive(mut self, adaptive: bool) -> Self {
self.adaptive = adaptive;
self
}
pub fn with_adaptive_interval(mut self, interval: u64) -> Self {
assert!(interval >= 1, "BasinHopping requires interval >= 1");
self.interval = interval;
self
}
pub fn with_target_accept_rate(mut self, rate: F) -> Self {
assert!(
rate > F::zero() && rate < F::one(),
"BasinHopping requires 0 < target_accept_rate < 1, got {rate:?}"
);
self.target_accept_rate = rate;
self
}
pub fn with_stepwise_factor(mut self, factor: F) -> Self {
assert!(
factor > F::zero() && factor < F::one(),
"BasinHopping requires 0 < stepwise_factor < 1, got {factor:?}"
);
self.stepwise_factor = factor;
self
}
pub fn with_inner_max_iter(mut self, n: u64) -> Self {
self.inner = self.inner.max_iter(n);
self
}
pub fn inner_terminate_on<C>(mut self, criterion: C) -> Self
where
C: TerminationCriterion<<I as InitialState<V>>::State> + 'static,
{
self.inner = self.inner.terminate_on(criterion);
self
}
}
impl<P, I, V, F, S, A> Solver<P, BasicState<V, F>> for BasinHopping<I, V, F, S, A>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
I: WarmStart<V> + Solver<P, <I as InitialState<V>>::State, Error = P::Error>,
<I as InitialState<V>>::State: State<Param = V, Float = F> + CountsMirror,
V: Clone,
S: StepTaker<V, F>,
A: AcceptanceTest<F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: BasicState<V, F>,
) -> Result<BasicState<V, F>, Self::Error> {
let seeded = self.inner.solver().seed(&state.param);
let result = self.inner.run(problem, seeded)?;
state.param = result.state.param().clone();
state.cost = Some(result.state.cost());
self.incumbent_success = !result.reason.is_failure();
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: BasicState<V, F>,
) -> Result<(BasicState<V, F>, Option<TerminationReason>), Self::Error> {
let f_old = state.cost();
let x_trial = self.step.take_step(&state.param, &mut self.rng);
let seeded = self.inner.solver().seed(&x_trial);
let result = self.inner.run(problem, seeded)?;
let new_success = !result.reason.is_failure();
let f_new = result.state.cost();
let x_new = result.state.param().clone();
self.nstep += 1;
let accepted = self.accept.accept(f_new, f_old, &mut self.rng)
&& accept_guard(new_success, self.incumbent_success);
if accepted {
state.param = x_new;
state.cost = Some(f_new);
self.incumbent_success = new_success;
self.naccept += 1;
}
if self.adaptive && self.nstep % self.interval == 0 {
let rate = F::from_u64(self.naccept).unwrap() / F::from_u64(self.nstep).unwrap();
let factor = if rate > self.target_accept_rate {
F::one() / self.stepwise_factor
} else {
self.stepwise_factor
};
self.step.adjust_scale(factor);
}
Ok((state, None))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::rng::ChaCha8Rng;
#[test]
fn random_displacement_stays_within_cube_and_advances_rng() {
let mut step = RandomDisplacement::new(0.5_f64);
let mut rng = ChaCha8Rng::seed_from_u64(1);
let x = vec![1.0, -2.0, 3.0];
let y = step.take_step(&x, &mut rng);
assert_eq!(y.len(), x.len());
for (yi, xi) in y.iter().zip(&x) {
assert!(
(yi - xi).abs() <= 0.5 + 1e-12,
"component {yi} outside [{}, {}]",
xi - 0.5,
xi + 0.5
);
}
}
#[test]
fn random_displacement_adjust_scale_grows_and_shrinks() {
let mut step = RandomDisplacement::new(1.0_f64);
StepTaker::<Vec<f64>>::adjust_scale(&mut step, 2.0);
assert_eq!(step.stepsize(), 2.0);
StepTaker::<Vec<f64>>::adjust_scale(&mut step, 0.25);
assert_eq!(step.stepsize(), 0.5);
}
#[test]
fn accept_guard_matches_scipy_success_rule() {
assert!(accept_guard(true, true));
assert!(accept_guard(true, false));
assert!(!accept_guard(false, true));
assert!(accept_guard(false, false));
}
#[test]
fn metropolis_always_accepts_downhill() {
let test = Metropolis::new(1.0_f64);
let mut rng = ChaCha8Rng::seed_from_u64(7);
assert!(test.accept(1.0, 2.0, &mut rng));
assert!(test.accept(2.0, 2.0, &mut rng));
}
#[test]
fn metropolis_uphill_acceptance_rate_matches_boltzmann() {
let test = Metropolis::new(1.0_f64);
let mut rng = ChaCha8Rng::seed_from_u64(12345);
let trials = 20_000;
let accepts = (0..trials)
.filter(|_| test.accept(2.0, 1.0, &mut rng))
.count();
let rate = accepts as f64 / trials as f64;
let expected = (-1.0_f64).exp();
assert!(
(rate - expected).abs() < 0.02,
"empirical uphill acceptance {rate} vs Boltzmann {expected}"
);
}
#[test]
fn lower_temperature_accepts_fewer_uphill_moves() {
let mut rng = ChaCha8Rng::seed_from_u64(99);
let hot = Metropolis::new(5.0_f64);
let cold = Metropolis::new(0.1_f64);
let trials = 10_000;
let hot_accepts = (0..trials)
.filter(|_| hot.accept(2.0, 1.0, &mut rng))
.count();
let cold_accepts = (0..trials)
.filter(|_| cold.accept(2.0, 1.0, &mut rng))
.count();
assert!(
hot_accepts > cold_accepts,
"hot {hot_accepts} should accept more uphill than cold {cold_accepts}"
);
}
}