solverforge_solver/phase/localsearch/acceptor/
simulated_annealing.rs1use std::fmt::Debug;
4
5use rand::rngs::SmallRng;
6use rand::{RngExt, SeedableRng};
7use solverforge_core::domain::PlanningSolution;
8use solverforge_core::score::Score;
9
10use super::Acceptor;
11
12#[derive(Debug, Clone)]
30pub struct SimulatedAnnealingAcceptor {
31 starting_temperature: f64,
33 current_temperature: f64,
35 decay_rate: f64,
37 rng: SmallRng,
39 level_count: usize,
41}
42
43impl SimulatedAnnealingAcceptor {
44 pub fn new(starting_temperature: f64, decay_rate: f64) -> Self {
52 Self {
53 starting_temperature,
54 current_temperature: starting_temperature,
55 decay_rate,
56 rng: SmallRng::from_rng(&mut rand::rng()),
57 level_count: 0,
58 }
59 }
60
61 pub fn with_seed(starting_temperature: f64, decay_rate: f64, seed: u64) -> Self {
62 Self {
63 starting_temperature,
64 current_temperature: starting_temperature,
65 decay_rate,
66 rng: SmallRng::seed_from_u64(seed),
67 level_count: 0,
68 }
69 }
70
71 pub fn auto_calibrate(decay_rate: f64) -> Self {
76 Self {
77 starting_temperature: 0.0, current_temperature: 0.0,
79 decay_rate,
80 rng: SmallRng::from_rng(&mut rand::rng()),
81 level_count: 0,
82 }
83 }
84}
85
86impl Default for SimulatedAnnealingAcceptor {
87 fn default() -> Self {
88 Self::auto_calibrate(0.999985)
91 }
92}
93
94fn score_delta_to_scalar(levels: &[i64]) -> f64 {
103 if levels.is_empty() {
104 return 0.0;
105 }
106 if levels.len() == 1 {
107 return levels[0] as f64;
108 }
109 let n = levels.len();
110 let mut scalar = 0.0f64;
111 for (i, &level) in levels.iter().enumerate() {
112 let weight = 10.0f64.powi(6 * (n - 1 - i) as i32);
113 scalar += level as f64 * weight;
114 }
115 scalar
116}
117
118impl<S: PlanningSolution> Acceptor<S> for SimulatedAnnealingAcceptor
119where
120 S::Score: Score,
121{
122 fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
123 if *move_score >= *last_step_score {
125 return true;
126 }
127
128 if self.current_temperature <= 0.0 {
129 return false;
130 }
131
132 let delta = move_score.to_scalar() - last_step_score.to_scalar();
135
136 let probability = (delta / self.current_temperature).exp();
138 let roll: f64 = self.rng.random::<f64>();
139
140 roll < probability
141 }
142
143 fn phase_started(&mut self, initial_score: &S::Score) {
144 self.level_count = S::Score::levels_count();
145
146 if self.starting_temperature == 0.0 {
148 let levels = initial_score.to_level_numbers();
149 let magnitude = score_delta_to_scalar(&levels).abs();
150 self.starting_temperature = if magnitude > 0.0 {
155 magnitude * 0.02
156 } else {
157 1.0
158 };
159 }
160
161 self.current_temperature = self.starting_temperature;
162 }
163
164 fn step_ended(&mut self, _step_score: &S::Score) {
165 self.current_temperature *= self.decay_rate;
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use solverforge_core::domain::PlanningSolution;
173 use solverforge_core::score::{HardSoftScore, SoftScore};
174
175 #[derive(Clone, Debug)]
176 struct SimpleSol {
177 score: Option<SoftScore>,
178 }
179 impl PlanningSolution for SimpleSol {
180 type Score = SoftScore;
181 fn score(&self) -> Option<Self::Score> {
182 self.score
183 }
184 fn set_score(&mut self, score: Option<Self::Score>) {
185 self.score = score;
186 }
187 }
188
189 #[derive(Clone, Debug)]
190 struct HardSoftSol {
191 score: Option<HardSoftScore>,
192 }
193 impl PlanningSolution for HardSoftSol {
194 type Score = HardSoftScore;
195 fn score(&self) -> Option<Self::Score> {
196 self.score
197 }
198 fn set_score(&mut self, score: Option<Self::Score>) {
199 self.score = score;
200 }
201 }
202
203 #[test]
204 fn accepts_improving_moves() {
205 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
206 let last = SoftScore::of(-10);
207 let better = SoftScore::of(-5);
208 assert!(Acceptor::<SimpleSol>::is_accepted(
209 &mut acceptor,
210 &last,
211 &better
212 ));
213 }
214
215 #[test]
216 fn accepts_equal_moves() {
217 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
218 let score = SoftScore::of(-10);
219 assert!(Acceptor::<SimpleSol>::is_accepted(
220 &mut acceptor,
221 &score,
222 &score
223 ));
224 }
225
226 #[test]
227 fn rejects_at_zero_temperature() {
228 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.0, 0.99, 42);
229 acceptor.current_temperature = 0.0;
230 let last = SoftScore::of(-10);
231 let worse = SoftScore::of(-20);
232 assert!(!Acceptor::<SimpleSol>::is_accepted(
233 &mut acceptor,
234 &last,
235 &worse
236 ));
237 }
238
239 #[test]
240 fn high_temperature_accepts_most() {
241 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1_000_000.0, 0.99, 42);
242 let last = SoftScore::of(-10);
243 let worse = SoftScore::of(-11);
244 let mut accepted = 0;
245 for _ in 0..100 {
246 if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse) {
247 accepted += 1;
248 }
249 }
250 assert!(accepted > 90);
251 }
252
253 #[test]
254 fn low_temperature_rejects_most() {
255 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.001, 0.99, 42);
256 let last = SoftScore::of(-10);
257 let worse = SoftScore::of(-20);
258 let mut accepted = 0;
259 for _ in 0..100 {
260 if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse) {
261 accepted += 1;
262 }
263 }
264 assert!(accepted < 5);
265 }
266
267 #[test]
268 fn temperature_decays_each_step() {
269 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(100.0, 0.5, 42);
270 let score = SoftScore::of(0);
271 Acceptor::<SimpleSol>::phase_started(&mut acceptor, &score);
272 assert!((acceptor.current_temperature - 100.0).abs() < f64::EPSILON);
273 Acceptor::<SimpleSol>::step_ended(&mut acceptor, &score);
274 assert!((acceptor.current_temperature - 50.0).abs() < f64::EPSILON);
275 Acceptor::<SimpleSol>::step_ended(&mut acceptor, &score);
276 assert!((acceptor.current_temperature - 25.0).abs() < f64::EPSILON);
277 }
278
279 #[test]
280 fn auto_calibrate_sets_temperature_from_initial_score() {
281 let mut acceptor = SimulatedAnnealingAcceptor::auto_calibrate(0.999);
282 let initial = HardSoftScore::of(-576, 0);
283 Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &initial);
284 assert!(acceptor.current_temperature > 10_000_000.0);
286 assert!(acceptor.current_temperature < 20_000_000.0);
287 }
288
289 #[test]
290 fn score_delta_to_scalar_simple() {
291 assert!((score_delta_to_scalar(&[-5]) - -5.0).abs() < f64::EPSILON);
292 }
293
294 #[test]
295 fn score_delta_to_scalar_hard_soft() {
296 let scalar = score_delta_to_scalar(&[-1, -50]);
297 assert!((scalar - -1_000_050.0).abs() < f64::EPSILON);
298 }
299}