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::{ProposalState, State};
use crate::core::termination::Termination;
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: Scalar> {
Geometric { alpha: F },
Reciprocal,
Logarithmic,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug)]
pub struct TemperatureSchedule<F: Scalar = 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: Scalar = f64, R = ChaCha8Rng> {
neighbor: N,
initial_temperature: F,
schedule: TemperatureSchedule<F>,
reannealing: Option<Reannealing>,
rng: R,
live_neighbor: Option<N>,
live_rng: Option<R>,
cooling_age: u64,
reannealing_progress: ReannealingProgress,
reannealings: u64,
accepted_iterate: bool,
}
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,
live_neighbor: None,
live_rng: None,
cooling_age: 0,
reannealing_progress: ReannealingProgress::default(),
reannealings: 0,
accepted_iterate: true,
}
}
pub fn temperature(&self) -> F {
self.schedule
.temperature(self.initial_temperature, self.cooling_age)
}
pub fn reannealings(&self) -> u64 {
self.reannealings
}
pub fn neighbor(&self) -> Option<&N> {
self.live_neighbor.as_ref()
}
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<N: Clone, F: Scalar> SimulatedAnnealing<N, F, ChaCha8Rng> {
pub fn seed_chain(&self, seed: u64) -> Self {
let mut chain = Self::new(
self.neighbor.clone(),
self.initial_temperature,
self.schedule,
seed,
);
chain.reannealing = self.reannealing;
chain
}
}
impl<V: Clone, N, F: Scalar, R> InitialState<V>
for SimulatedAnnealing<N, F, R>
{
type State = ProposalState<V, F>;
fn seed(&self, x: &V) -> Self::State {
ProposalState::new(x.clone())
}
}
impl<P, V, N, F, R> Solver<P, ProposalState<V, F>>
for SimulatedAnnealing<N, F, R>
where
P: CostFunction<Param = V, Output = F>,
V: Clone,
N: Neighbor<V, F, R, Error = P::Error> + Clone,
F: Scalar,
R: Rng + Clone,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: ProposalState<V, F>,
) -> Result<ProposalState<V, F>, Self::Error> {
state.reset();
self.live_neighbor = Some(self.neighbor.clone());
self.live_rng = Some(self.rng.clone());
self.cooling_age = 0;
self.reannealing_progress = ReannealingProgress::default();
self.reannealings = 0;
self.accepted_iterate = true;
let cost = problem.cost(state.param())?;
state.replace(state.param().clone(), cost);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: ProposalState<V, F>,
) -> Result<crate::SolverStep<ProposalState<V, F>>, Self::Error> {
let incumbent_cost = state.cost();
let temperature = self.temperature();
let rng = self
.live_rng
.as_mut()
.expect("annealing must be initialized");
let candidate = self
.live_neighbor
.as_mut()
.expect("annealing must be initialized")
.propose(state.param(), temperature, 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(rng.random::<f64>()).unwrap();
draw < probability
};
self.accepted_iterate = accepted;
if accepted {
state.accept_proposal(candidate, candidate_cost);
} else {
state.reject_proposal();
}
self.cooling_age = self.cooling_age.saturating_add(1);
if let Some(reannealing) = self.reannealing {
reannealing.update_progress(
&mut self.reannealing_progress,
accepted,
new_best,
);
if reannealing.should_restart(self.reannealing_progress) {
self.cooling_age = 0;
self.reannealing_progress = ReannealingProgress::default();
self.reannealings = self.reannealings.saturating_add(1);
}
}
Ok(crate::SolverStep::from((state, None)))
}
fn should_check_iterate_change(&self) -> bool {
self.accepted_iterate
}
fn terminate(&self, state: &ProposalState<V, F>) -> Option<Termination<F>> {
let cost = state.cost();
if cost.is_nan() {
Some(Termination::numerical_failure(
"Simulated annealing has a NaN objective at its current point.",
))
} else if cost == F::neg_infinity() {
Some(Termination::custom(
"simulated_annealing.negative_infinity",
"The published incumbent objective equals negative infinity.",
vec![crate::Measurement {
name: "cost".into(),
value: cost,
}],
))
} 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);
}
}