use std::fmt::Debug;
use rand::rngs::SmallRng;
use rand::{RngExt, SeedableRng};
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::Score;
use super::Acceptor;
#[derive(Debug, Clone)]
pub struct SimulatedAnnealingAcceptor {
starting_temperature: f64,
current_temperature: f64,
decay_rate: f64,
rng: SmallRng,
level_count: usize,
}
impl SimulatedAnnealingAcceptor {
pub fn new(starting_temperature: f64, decay_rate: f64) -> Self {
Self {
starting_temperature,
current_temperature: starting_temperature,
decay_rate,
rng: SmallRng::from_rng(&mut rand::rng()),
level_count: 0,
}
}
pub fn with_seed(starting_temperature: f64, decay_rate: f64, seed: u64) -> Self {
Self {
starting_temperature,
current_temperature: starting_temperature,
decay_rate,
rng: SmallRng::seed_from_u64(seed),
level_count: 0,
}
}
pub fn auto_calibrate(decay_rate: f64) -> Self {
Self {
starting_temperature: 0.0, current_temperature: 0.0,
decay_rate,
rng: SmallRng::from_rng(&mut rand::rng()),
level_count: 0,
}
}
}
impl Default for SimulatedAnnealingAcceptor {
fn default() -> Self {
Self::auto_calibrate(0.999985)
}
}
fn score_delta_to_scalar(levels: &[i64]) -> f64 {
if levels.is_empty() {
return 0.0;
}
if levels.len() == 1 {
return levels[0] as f64;
}
let n = levels.len();
let mut scalar = 0.0f64;
for (i, &level) in levels.iter().enumerate() {
let weight = 10.0f64.powi(6 * (n - 1 - i) as i32);
scalar += level as f64 * weight;
}
scalar
}
impl<S: PlanningSolution> Acceptor<S> for SimulatedAnnealingAcceptor
where
S::Score: Score,
{
fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
if *move_score >= *last_step_score {
return true;
}
if self.current_temperature <= 0.0 {
return false;
}
let delta = move_score.to_scalar() - last_step_score.to_scalar();
let probability = (delta / self.current_temperature).exp();
let roll: f64 = self.rng.random::<f64>();
roll < probability
}
fn phase_started(&mut self, initial_score: &S::Score) {
self.level_count = S::Score::levels_count();
if self.starting_temperature == 0.0 {
let levels = initial_score.to_level_numbers();
let magnitude = score_delta_to_scalar(&levels).abs();
self.starting_temperature = if magnitude > 0.0 {
magnitude * 0.02
} else {
1.0
};
}
self.current_temperature = self.starting_temperature;
}
fn step_ended(&mut self, _step_score: &S::Score) {
self.current_temperature *= self.decay_rate;
}
}
#[cfg(test)]
mod tests {
use super::*;
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::{HardSoftScore, SoftScore};
#[derive(Clone, Debug)]
struct SimpleSol {
score: Option<SoftScore>,
}
impl PlanningSolution for SimpleSol {
type Score = SoftScore;
fn score(&self) -> Option<Self::Score> {
self.score
}
fn set_score(&mut self, score: Option<Self::Score>) {
self.score = score;
}
}
#[derive(Clone, Debug)]
struct HardSoftSol {
score: Option<HardSoftScore>,
}
impl PlanningSolution for HardSoftSol {
type Score = HardSoftScore;
fn score(&self) -> Option<Self::Score> {
self.score
}
fn set_score(&mut self, score: Option<Self::Score>) {
self.score = score;
}
}
#[test]
fn accepts_improving_moves() {
let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
let last = SoftScore::of(-10);
let better = SoftScore::of(-5);
assert!(Acceptor::<SimpleSol>::is_accepted(
&mut acceptor,
&last,
&better
));
}
#[test]
fn accepts_equal_moves() {
let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
let score = SoftScore::of(-10);
assert!(Acceptor::<SimpleSol>::is_accepted(
&mut acceptor,
&score,
&score
));
}
#[test]
fn rejects_at_zero_temperature() {
let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.0, 0.99, 42);
acceptor.current_temperature = 0.0;
let last = SoftScore::of(-10);
let worse = SoftScore::of(-20);
assert!(!Acceptor::<SimpleSol>::is_accepted(
&mut acceptor,
&last,
&worse
));
}
#[test]
fn high_temperature_accepts_most() {
let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1_000_000.0, 0.99, 42);
let last = SoftScore::of(-10);
let worse = SoftScore::of(-11);
let mut accepted = 0;
for _ in 0..100 {
if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse) {
accepted += 1;
}
}
assert!(accepted > 90);
}
#[test]
fn low_temperature_rejects_most() {
let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.001, 0.99, 42);
let last = SoftScore::of(-10);
let worse = SoftScore::of(-20);
let mut accepted = 0;
for _ in 0..100 {
if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse) {
accepted += 1;
}
}
assert!(accepted < 5);
}
#[test]
fn temperature_decays_each_step() {
let mut acceptor = SimulatedAnnealingAcceptor::with_seed(100.0, 0.5, 42);
let score = SoftScore::of(0);
Acceptor::<SimpleSol>::phase_started(&mut acceptor, &score);
assert!((acceptor.current_temperature - 100.0).abs() < f64::EPSILON);
Acceptor::<SimpleSol>::step_ended(&mut acceptor, &score);
assert!((acceptor.current_temperature - 50.0).abs() < f64::EPSILON);
Acceptor::<SimpleSol>::step_ended(&mut acceptor, &score);
assert!((acceptor.current_temperature - 25.0).abs() < f64::EPSILON);
}
#[test]
fn auto_calibrate_sets_temperature_from_initial_score() {
let mut acceptor = SimulatedAnnealingAcceptor::auto_calibrate(0.999);
let initial = HardSoftScore::of(-576, 0);
Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &initial);
assert!(acceptor.current_temperature > 10_000_000.0);
assert!(acceptor.current_temperature < 20_000_000.0);
}
#[test]
fn score_delta_to_scalar_simple() {
assert!((score_delta_to_scalar(&[-5]) - -5.0).abs() < f64::EPSILON);
}
#[test]
fn score_delta_to_scalar_hard_soft() {
let scalar = score_delta_to_scalar(&[-1, -50]);
assert!((scalar - -1_000_050.0).abs() < f64::EPSILON);
}
}