use crate::core::inner::InitialState;
use crate::core::math::Scalar;
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, Rng, RngExt, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::{SimulatedAnnealingState, State};
use crate::core::termination::TerminationReason;
pub trait Neighbor<P, F = f64, R = ChaCha8Rng> {
type Error;
fn propose(
&mut self,
current: &P,
temperature: F,
rng: &mut R,
) -> Result<P, Self::Error>;
}
impl<P, F, R, N> Neighbor<P, F, R> for N
where
N: FnMut(&P, F, &mut R) -> P,
{
type Error = std::convert::Infallible;
fn propose(
&mut self,
current: &P,
temperature: F,
rng: &mut R,
) -> Result<P, Self::Error> {
Ok(self(current, temperature, rng))
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug)]
enum Cooling<F> {
Geometric { alpha: F },
Reciprocal,
Logarithmic,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug)]
pub struct TemperatureSchedule<F = f64> {
cooling: Cooling<F>,
steps_per_temperature: u64,
}
impl<F: Scalar> TemperatureSchedule<F> {
pub fn geometric(alpha: F) -> Self {
assert!(
alpha.is_finite() && alpha > F::zero() && alpha < F::one(),
"geometric cooling requires finite 0 < alpha < 1, got {alpha:?}"
);
Self {
cooling: Cooling::Geometric { alpha },
steps_per_temperature: 1,
}
}
pub fn reciprocal() -> Self {
Self {
cooling: Cooling::Reciprocal,
steps_per_temperature: 1,
}
}
pub fn logarithmic() -> Self {
Self {
cooling: Cooling::Logarithmic,
steps_per_temperature: 1,
}
}
pub fn with_steps_per_temperature(mut self, steps: u64) -> Self {
assert!(
steps > 0,
"temperature schedule requires steps_per_temperature > 0"
);
self.steps_per_temperature = steps;
self
}
pub fn steps_per_temperature(&self) -> u64 {
self.steps_per_temperature
}
pub fn temperature(&self, initial_temperature: F, proposal_age: u64) -> F {
assert!(
initial_temperature.is_finite() && initial_temperature > F::zero(),
"temperature schedule requires a finite initial temperature > 0"
);
let level = proposal_age / self.steps_per_temperature;
let level_f = F::from_u64(level).unwrap_or_else(F::infinity);
let temperature = match self.cooling {
Cooling::Geometric { alpha } => {
initial_temperature * alpha.powf(level_f)
}
Cooling::Reciprocal => initial_temperature / (level_f + F::one()),
Cooling::Logarithmic => {
let two = F::one() + F::one();
initial_temperature * two.ln() / (level_f + two).ln()
}
};
temperature.max(F::min_positive_value())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug)]
pub struct Reannealing {
fixed_interval: Option<u64>,
accepted_stall: Option<u64>,
best_stall: Option<u64>,
}
impl Reannealing {
fn validate_threshold(threshold: u64) {
assert!(threshold > 0, "reannealing threshold must be > 0");
}
fn none() -> Self {
Self {
fixed_interval: None,
accepted_stall: None,
best_stall: None,
}
}
fn with_fixed_interval(mut self, interval: u64) -> Self {
Self::validate_threshold(interval);
self.fixed_interval = Some(interval);
self
}
fn with_accepted_stall(mut self, iterations: u64) -> Self {
Self::validate_threshold(iterations);
self.accepted_stall = Some(iterations);
self
}
fn with_best_stall(mut self, iterations: u64) -> Self {
Self::validate_threshold(iterations);
self.best_stall = Some(iterations);
self
}
pub fn fixed_interval(interval: u64) -> Self {
Self::none().with_fixed_interval(interval)
}
pub fn after_rejections(rejections: u64) -> Self {
Self::none().with_accepted_stall(rejections)
}
pub fn after_no_best(iterations: u64) -> Self {
Self::none().with_best_stall(iterations)
}
fn update_progress(
self,
progress: &mut ReannealingProgress,
accepted: bool,
new_best: bool,
) {
progress.fixed_interval = progress.fixed_interval.saturating_add(1);
progress.accepted_stall = if accepted {
0
} else {
progress.accepted_stall.saturating_add(1)
};
progress.best_stall = if new_best {
0
} else {
progress.best_stall.saturating_add(1)
};
}
fn should_restart(self, progress: ReannealingProgress) -> bool {
self.fixed_interval
.is_some_and(|threshold| progress.fixed_interval >= threshold)
|| self
.accepted_stall
.is_some_and(|threshold| progress.accepted_stall >= threshold)
|| self
.best_stall
.is_some_and(|threshold| progress.best_stall >= threshold)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct ReannealingProgress {
fixed_interval: u64,
accepted_stall: u64,
best_stall: u64,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug)]
pub struct SimulatedAnnealing<N, F = f64, R = ChaCha8Rng> {
neighbor: N,
initial_temperature: F,
schedule: TemperatureSchedule<F>,
reannealing: Option<Reannealing>,
rng: R,
}
fn uphill_acceptance_probability<F: Scalar>(delta: F, temperature: F) -> F {
(-delta / temperature).exp()
}
impl<N, F> SimulatedAnnealing<N, F, ChaCha8Rng>
where
F: Scalar,
{
pub fn new(
neighbor: N,
initial_temperature: F,
schedule: TemperatureSchedule<F>,
seed: u64,
) -> Self {
Self::new_with_rng(
neighbor,
initial_temperature,
schedule,
ChaCha8Rng::seed_from_u64(seed),
)
}
}
impl<N, F, R> SimulatedAnnealing<N, F, R>
where
F: Scalar,
{
pub fn new_with_rng(
neighbor: N,
initial_temperature: F,
schedule: TemperatureSchedule<F>,
rng: R,
) -> Self {
assert!(
initial_temperature.is_finite() && initial_temperature > F::zero(),
"SimulatedAnnealing requires a finite initial temperature > 0"
);
Self {
neighbor,
initial_temperature,
schedule,
reannealing: None,
rng,
}
}
pub fn with_reannealing(mut self, reannealing: Reannealing) -> Self {
self.reannealing = Some(reannealing);
self
}
pub fn with_reannealing_fixed(mut self, iterations: u64) -> Self {
let reannealing = self
.reannealing
.unwrap_or_else(Reannealing::none)
.with_fixed_interval(iterations);
self.reannealing = Some(reannealing);
self
}
pub fn with_reannealing_accepted(mut self, iterations: u64) -> Self {
let reannealing = self
.reannealing
.unwrap_or_else(Reannealing::none)
.with_accepted_stall(iterations);
self.reannealing = Some(reannealing);
self
}
pub fn with_reannealing_best(mut self, iterations: u64) -> Self {
let reannealing = self
.reannealing
.unwrap_or_else(Reannealing::none)
.with_best_stall(iterations);
self.reannealing = Some(reannealing);
self
}
}
impl<V, N, F, R> InitialState<V> for SimulatedAnnealing<N, F, R>
where
V: Clone,
N: Clone,
F: Scalar,
R: Clone,
{
type State = SimulatedAnnealingState<V, N, F, R>;
fn seed(&self, x: &V) -> Self::State {
SimulatedAnnealingState::new(
x.clone(),
self.neighbor.clone(),
self.rng.clone(),
self.initial_temperature,
self.schedule,
self.reannealing,
)
}
}
impl<P, V, N, F, R> Solver<P, SimulatedAnnealingState<V, N, F, R>>
for SimulatedAnnealing<N, F, R>
where
P: CostFunction<Param = V, Output = F>,
V: Clone,
N: Neighbor<V, F, R, Error = P::Error>,
F: Scalar,
R: Rng,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: SimulatedAnnealingState<V, N, F, R>,
) -> Result<SimulatedAnnealingState<V, N, F, R>, Self::Error> {
if state.cost.is_none() {
state.cost = Some(problem.cost(&state.param)?);
}
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: SimulatedAnnealingState<V, N, F, R>,
) -> Result<
(
SimulatedAnnealingState<V, N, F, R>,
Option<TerminationReason>,
),
Self::Error,
> {
let incumbent_cost = state.cost.expect(
"SimulatedAnnealing::next_iter called before init evaluated the start point",
);
let temperature = state.temperature();
let candidate = state.neighbor.propose(
&state.param,
temperature,
&mut state.rng,
)?;
let candidate_cost = problem.cost(&candidate)?;
let new_best = candidate_cost < state.best_cost;
let accepted =
if candidate_cost.is_nan() || candidate_cost == F::infinity() {
false
} else if candidate_cost <= incumbent_cost {
true
} else {
let probability = uphill_acceptance_probability(
candidate_cost - incumbent_cost,
temperature,
);
let draw = F::from_f64(state.rng.random::<f64>()).unwrap();
draw < probability
};
if accepted {
state.param = candidate;
state.cost = Some(candidate_cost);
state.accepted_moves = state.accepted_moves.saturating_add(1);
state.last_accepted_iter = state.iter.saturating_add(1);
} else {
state.rejected_moves = state.rejected_moves.saturating_add(1);
}
state.cooling_age = state.cooling_age.saturating_add(1);
if let Some(reannealing) = state.reannealing {
reannealing.update_progress(
&mut state.reannealing_progress,
accepted,
new_best,
);
if reannealing.should_restart(state.reannealing_progress) {
state.cooling_age = 0;
state.reannealing_progress = ReannealingProgress::default();
state.reannealings = state.reannealings.saturating_add(1);
}
}
Ok((state, None))
}
fn terminate(
&self,
state: &SimulatedAnnealingState<V, N, F, R>,
) -> Option<TerminationReason> {
let cost = state.cost();
if cost.is_nan() {
Some(TerminationReason::SolverFailed)
} else if cost == F::neg_infinity() {
Some(TerminationReason::SolverConverged)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::uphill_acceptance_probability;
#[test]
fn metropolis_probability_matches_the_closed_form() {
let probability = uphill_acceptance_probability(2.0_f64, 4.0);
assert!((probability - (-0.5_f64).exp()).abs() < f64::EPSILON);
}
}