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