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}