solverforge_solver/phase/localsearch/acceptor/
simulated_annealing.rs

1//! Simulated annealing acceptor.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6
7use super::Acceptor;
8
9/// Simulated annealing acceptor - accepts moves with temperature-based probability.
10///
11/// Starts with high acceptance probability and gradually decreases it,
12/// allowing the search to escape local optima early on.
13///
14/// # Example
15///
16/// ```
17/// use solverforge_solver::phase::localsearch::SimulatedAnnealingAcceptor;
18///
19/// let acceptor = SimulatedAnnealingAcceptor::new(1.0, 0.99);
20/// ```
21#[derive(Debug, Clone)]
22pub struct SimulatedAnnealingAcceptor {
23    /// Initial temperature.
24    starting_temperature: f64,
25    /// Current temperature.
26    current_temperature: f64,
27    /// Temperature decay rate per step.
28    decay_rate: f64,
29}
30
31impl SimulatedAnnealingAcceptor {
32    /// Creates a new simulated annealing acceptor.
33    ///
34    /// # Arguments
35    /// * `starting_temperature` - Initial temperature (higher = more exploration)
36    /// * `decay_rate` - Multiplicative decay per step (e.g., 0.99)
37    pub fn new(starting_temperature: f64, decay_rate: f64) -> Self {
38        Self {
39            starting_temperature,
40            current_temperature: starting_temperature,
41            decay_rate,
42        }
43    }
44}
45
46impl Default for SimulatedAnnealingAcceptor {
47    fn default() -> Self {
48        Self::new(1.0, 0.99)
49    }
50}
51
52impl<S: PlanningSolution> Acceptor<S> for SimulatedAnnealingAcceptor {
53    fn is_accepted(&self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
54        // Always accept improving moves
55        if move_score > last_step_score {
56            return true;
57        }
58
59        // For non-improving moves, accept with probability based on temperature
60        // P = exp(-delta / temperature) where delta is the score difference
61        // Since we can't easily compute the numeric difference, we use a simpler approach:
62        // Accept with probability proportional to the temperature
63
64        if self.current_temperature <= 0.0 {
65            return false;
66        }
67
68        // Simple probability: temperature directly (0.0 to 1.0)
69        // In a real implementation, we'd compute the actual score difference
70        let acceptance_probability = self.current_temperature.min(1.0);
71
72        // Use a deterministic acceptance for testing
73        // In production, this would use a random number
74        acceptance_probability > 0.5
75    }
76
77    fn phase_started(&mut self, _initial_score: &S::Score) {
78        self.current_temperature = self.starting_temperature;
79    }
80
81    fn step_ended(&mut self, _step_score: &S::Score) {
82        // Decay temperature
83        self.current_temperature *= self.decay_rate;
84    }
85}