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 pub(crate) fn auto_calibrate_with_seed(decay_rate: f64, seed: u64) -> Self {
86 Self {
87 starting_temperature: 0.0, current_temperature: 0.0,
89 decay_rate,
90 rng: SmallRng::seed_from_u64(seed),
91 level_count: 0,
92 }
93 }
94}
95
96impl Default for SimulatedAnnealingAcceptor {
97 fn default() -> Self {
98 Self::auto_calibrate(0.999985)
101 }
102}
103
104fn score_delta_to_scalar(levels: &[i64]) -> f64 {
113 if levels.is_empty() {
114 return 0.0;
115 }
116 if levels.len() == 1 {
117 return levels[0] as f64;
118 }
119 let n = levels.len();
120 let mut scalar = 0.0f64;
121 for (i, &level) in levels.iter().enumerate() {
122 let weight = 10.0f64.powi(6 * (n - 1 - i) as i32);
123 scalar += level as f64 * weight;
124 }
125 scalar
126}
127
128impl<S: PlanningSolution> Acceptor<S> for SimulatedAnnealingAcceptor
129where
130 S::Score: Score,
131{
132 fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
133 if *move_score >= *last_step_score {
135 return true;
136 }
137
138 if self.current_temperature <= 0.0 {
139 return false;
140 }
141
142 let delta = move_score.to_scalar() - last_step_score.to_scalar();
145
146 let probability = (delta / self.current_temperature).exp();
148 let roll: f64 = self.rng.random::<f64>();
149
150 roll < probability
151 }
152
153 fn phase_started(&mut self, initial_score: &S::Score) {
154 self.level_count = S::Score::levels_count();
155
156 if self.starting_temperature == 0.0 {
158 let levels = initial_score.to_level_numbers();
159 let magnitude = score_delta_to_scalar(&levels).abs();
160 self.starting_temperature = if magnitude > 0.0 {
165 magnitude * 0.02
166 } else {
167 1.0
168 };
169 }
170
171 self.current_temperature = self.starting_temperature;
172 }
173
174 fn step_ended(&mut self, _step_score: &S::Score) {
175 self.current_temperature *= self.decay_rate;
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use solverforge_core::domain::PlanningSolution;
183 use solverforge_core::score::{HardSoftScore, SoftScore};
184
185 #[derive(Clone, Debug)]
186 struct SimpleSol {
187 score: Option<SoftScore>,
188 }
189 impl PlanningSolution for SimpleSol {
190 type Score = SoftScore;
191 fn score(&self) -> Option<Self::Score> {
192 self.score
193 }
194 fn set_score(&mut self, score: Option<Self::Score>) {
195 self.score = score;
196 }
197 }
198
199 #[derive(Clone, Debug)]
200 struct HardSoftSol {
201 score: Option<HardSoftScore>,
202 }
203 impl PlanningSolution for HardSoftSol {
204 type Score = HardSoftScore;
205 fn score(&self) -> Option<Self::Score> {
206 self.score
207 }
208 fn set_score(&mut self, score: Option<Self::Score>) {
209 self.score = score;
210 }
211 }
212
213 #[test]
214 fn accepts_improving_moves() {
215 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
216 let last = SoftScore::of(-10);
217 let better = SoftScore::of(-5);
218 assert!(Acceptor::<SimpleSol>::is_accepted(
219 &mut acceptor,
220 &last,
221 &better
222 ));
223 }
224
225 #[test]
226 fn accepts_equal_moves() {
227 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
228 let score = SoftScore::of(-10);
229 assert!(Acceptor::<SimpleSol>::is_accepted(
230 &mut acceptor,
231 &score,
232 &score
233 ));
234 }
235
236 #[test]
237 fn rejects_at_zero_temperature() {
238 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.0, 0.99, 42);
239 acceptor.current_temperature = 0.0;
240 let last = SoftScore::of(-10);
241 let worse = SoftScore::of(-20);
242 assert!(!Acceptor::<SimpleSol>::is_accepted(
243 &mut acceptor,
244 &last,
245 &worse
246 ));
247 }
248
249 #[test]
250 fn high_temperature_accepts_most() {
251 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1_000_000.0, 0.99, 42);
252 let last = SoftScore::of(-10);
253 let worse = SoftScore::of(-11);
254 let mut accepted = 0;
255 for _ in 0..100 {
256 if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse) {
257 accepted += 1;
258 }
259 }
260 assert!(accepted > 90);
261 }
262
263 #[test]
264 fn low_temperature_rejects_most() {
265 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.001, 0.99, 42);
266 let last = SoftScore::of(-10);
267 let worse = SoftScore::of(-20);
268 let mut accepted = 0;
269 for _ in 0..100 {
270 if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse) {
271 accepted += 1;
272 }
273 }
274 assert!(accepted < 5);
275 }
276
277 #[test]
278 fn temperature_decays_each_step() {
279 let mut acceptor = SimulatedAnnealingAcceptor::with_seed(100.0, 0.5, 42);
280 let score = SoftScore::of(0);
281 Acceptor::<SimpleSol>::phase_started(&mut acceptor, &score);
282 assert!((acceptor.current_temperature - 100.0).abs() < f64::EPSILON);
283 Acceptor::<SimpleSol>::step_ended(&mut acceptor, &score);
284 assert!((acceptor.current_temperature - 50.0).abs() < f64::EPSILON);
285 Acceptor::<SimpleSol>::step_ended(&mut acceptor, &score);
286 assert!((acceptor.current_temperature - 25.0).abs() < f64::EPSILON);
287 }
288
289 #[test]
290 fn auto_calibrate_sets_temperature_from_initial_score() {
291 let mut acceptor = SimulatedAnnealingAcceptor::auto_calibrate(0.999);
292 let initial = HardSoftScore::of(-576, 0);
293 Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &initial);
294 assert!(acceptor.current_temperature > 10_000_000.0);
296 assert!(acceptor.current_temperature < 20_000_000.0);
297 }
298
299 #[test]
300 fn seeded_auto_calibrate_matches_unseeded_temperature() {
301 let initial = HardSoftScore::of(-576, 0);
302 let mut seeded = SimulatedAnnealingAcceptor::auto_calibrate_with_seed(0.999, 42);
303 let mut unseeded = SimulatedAnnealingAcceptor::auto_calibrate(0.999);
304
305 Acceptor::<HardSoftSol>::phase_started(&mut seeded, &initial);
306 Acceptor::<HardSoftSol>::phase_started(&mut unseeded, &initial);
307
308 assert_eq!(seeded.starting_temperature, unseeded.starting_temperature);
309 assert_eq!(seeded.current_temperature, unseeded.current_temperature);
310 }
311
312 #[test]
313 fn score_delta_to_scalar_simple() {
314 assert!((score_delta_to_scalar(&[-5]) - -5.0).abs() < f64::EPSILON);
315 }
316
317 #[test]
318 fn score_delta_to_scalar_hard_soft() {
319 let scalar = score_delta_to_scalar(&[-1, -50]);
320 assert!((scalar - -1_000_050.0).abs() < f64::EPSILON);
321 }
322}