1use std::fmt::Debug;
4
5use solverforge_config::{
6 AcceptorConfig, HardRegressionPolicyConfig, SimulatedAnnealingCalibrationConfig,
7 SimulatedAnnealingConfig, TabuSearchConfig,
8};
9use solverforge_core::domain::PlanningSolution;
10use solverforge_core::score::{ParseableScore, Score};
11
12use crate::heuristic::r#move::MoveTabuSignature;
13use crate::phase::localsearch::{
14 Acceptor, DiversifiedLateAcceptanceAcceptor, GreatDelugeAcceptor, HardRegressionPolicy,
15 HillClimbingAcceptor, LateAcceptanceAcceptor, SimulatedAnnealingAcceptor,
16 SimulatedAnnealingCalibration, StepCountingHillClimbingAcceptor, TabuSearchAcceptor,
17 TabuSearchPolicy,
18};
19
20#[allow(clippy::large_enum_variant)] pub enum AnyAcceptor<S: PlanningSolution> {
27 HillClimbing(HillClimbingAcceptor),
29 StepCountingHillClimbing(StepCountingHillClimbingAcceptor<S>),
31 TabuSearch(TabuSearchAcceptor<S>),
33 SimulatedAnnealing(SimulatedAnnealingAcceptor),
35 LateAcceptance(LateAcceptanceAcceptor<S>),
37 DiversifiedLateAcceptance(DiversifiedLateAcceptanceAcceptor<S>),
39 GreatDeluge(GreatDelugeAcceptor<S>),
41}
42
43impl<S: PlanningSolution> Debug for AnyAcceptor<S> {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::HillClimbing(a) => write!(f, "AnyAcceptor::HillClimbing({a:?})"),
47 Self::StepCountingHillClimbing(a) => {
48 write!(f, "AnyAcceptor::StepCountingHillClimbing({a:?})")
49 }
50 Self::TabuSearch(a) => write!(f, "AnyAcceptor::TabuSearch({a:?})"),
51 Self::SimulatedAnnealing(a) => write!(f, "AnyAcceptor::SimulatedAnnealing({a:?})"),
52 Self::LateAcceptance(a) => write!(f, "AnyAcceptor::LateAcceptance({a:?})"),
53 Self::DiversifiedLateAcceptance(a) => {
54 write!(f, "AnyAcceptor::DiversifiedLateAcceptance({a:?})")
55 }
56 Self::GreatDeluge(a) => write!(f, "AnyAcceptor::GreatDeluge({a:?})"),
57 }
58 }
59}
60
61impl<S: PlanningSolution> Clone for AnyAcceptor<S>
62where
63 S::Score: Clone,
64{
65 fn clone(&self) -> Self {
66 match self {
67 Self::HillClimbing(a) => Self::HillClimbing(a.clone()),
68 Self::StepCountingHillClimbing(a) => Self::StepCountingHillClimbing(a.clone()),
69 Self::TabuSearch(a) => Self::TabuSearch(a.clone()),
70 Self::SimulatedAnnealing(a) => Self::SimulatedAnnealing(a.clone()),
71 Self::LateAcceptance(a) => Self::LateAcceptance(a.clone()),
72 Self::DiversifiedLateAcceptance(a) => Self::DiversifiedLateAcceptance(a.clone()),
73 Self::GreatDeluge(a) => Self::GreatDeluge(a.clone()),
74 }
75 }
76}
77
78impl<S: PlanningSolution> Acceptor<S> for AnyAcceptor<S>
79where
80 S::Score: Score,
81{
82 fn requires_move_signatures(&self) -> bool {
83 match self {
84 Self::HillClimbing(a) => Acceptor::<S>::requires_move_signatures(a),
85 Self::StepCountingHillClimbing(a) => Acceptor::<S>::requires_move_signatures(a),
86 Self::TabuSearch(a) => Acceptor::<S>::requires_move_signatures(a),
87 Self::SimulatedAnnealing(a) => Acceptor::<S>::requires_move_signatures(a),
88 Self::LateAcceptance(a) => Acceptor::<S>::requires_move_signatures(a),
89 Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::requires_move_signatures(a),
90 Self::GreatDeluge(a) => Acceptor::<S>::requires_move_signatures(a),
91 }
92 }
93
94 fn is_accepted(
95 &mut self,
96 last_step_score: &S::Score,
97 move_score: &S::Score,
98 move_signature: Option<&MoveTabuSignature>,
99 ) -> bool {
100 match self {
101 Self::HillClimbing(a) => {
102 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
103 }
104 Self::StepCountingHillClimbing(a) => {
105 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
106 }
107 Self::TabuSearch(a) => {
108 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
109 }
110 Self::SimulatedAnnealing(a) => {
111 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
112 }
113 Self::LateAcceptance(a) => {
114 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
115 }
116 Self::DiversifiedLateAcceptance(a) => {
117 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
118 }
119 Self::GreatDeluge(a) => {
120 Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
121 }
122 }
123 }
124
125 fn phase_started(&mut self, initial_score: &S::Score) {
126 match self {
127 Self::HillClimbing(a) => Acceptor::<S>::phase_started(a, initial_score),
128 Self::StepCountingHillClimbing(a) => Acceptor::<S>::phase_started(a, initial_score),
129 Self::TabuSearch(a) => Acceptor::<S>::phase_started(a, initial_score),
130 Self::SimulatedAnnealing(a) => Acceptor::<S>::phase_started(a, initial_score),
131 Self::LateAcceptance(a) => Acceptor::<S>::phase_started(a, initial_score),
132 Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::phase_started(a, initial_score),
133 Self::GreatDeluge(a) => Acceptor::<S>::phase_started(a, initial_score),
134 }
135 }
136
137 fn phase_ended(&mut self) {
138 match self {
139 Self::HillClimbing(a) => Acceptor::<S>::phase_ended(a),
140 Self::StepCountingHillClimbing(a) => Acceptor::<S>::phase_ended(a),
141 Self::TabuSearch(a) => Acceptor::<S>::phase_ended(a),
142 Self::SimulatedAnnealing(a) => Acceptor::<S>::phase_ended(a),
143 Self::LateAcceptance(a) => Acceptor::<S>::phase_ended(a),
144 Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::phase_ended(a),
145 Self::GreatDeluge(a) => Acceptor::<S>::phase_ended(a),
146 }
147 }
148
149 fn step_started(&mut self) {
150 match self {
151 Self::HillClimbing(a) => Acceptor::<S>::step_started(a),
152 Self::StepCountingHillClimbing(a) => Acceptor::<S>::step_started(a),
153 Self::TabuSearch(a) => Acceptor::<S>::step_started(a),
154 Self::SimulatedAnnealing(a) => Acceptor::<S>::step_started(a),
155 Self::LateAcceptance(a) => Acceptor::<S>::step_started(a),
156 Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::step_started(a),
157 Self::GreatDeluge(a) => Acceptor::<S>::step_started(a),
158 }
159 }
160
161 fn step_ended(
162 &mut self,
163 step_score: &S::Score,
164 accepted_move_signature: Option<&MoveTabuSignature>,
165 ) {
166 match self {
167 Self::HillClimbing(a) => {
168 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
169 }
170 Self::StepCountingHillClimbing(a) => {
171 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
172 }
173 Self::TabuSearch(a) => {
174 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
175 }
176 Self::SimulatedAnnealing(a) => {
177 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
178 }
179 Self::LateAcceptance(a) => {
180 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
181 }
182 Self::DiversifiedLateAcceptance(a) => {
183 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
184 }
185 Self::GreatDeluge(a) => {
186 Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
187 }
188 }
189 }
190}
191
192pub struct AcceptorBuilder;
194
195impl AcceptorBuilder {
196 pub fn build<S: PlanningSolution>(config: &AcceptorConfig) -> AnyAcceptor<S>
198 where
199 S::Score: Score + ParseableScore,
200 {
201 Self::build_with_seed(config, None)
202 }
203
204 pub fn build_with_seed<S: PlanningSolution>(
206 config: &AcceptorConfig,
207 random_seed: Option<u64>,
208 ) -> AnyAcceptor<S>
209 where
210 S::Score: Score + ParseableScore,
211 {
212 match config {
213 AcceptorConfig::HillClimbing => AnyAcceptor::HillClimbing(HillClimbingAcceptor::new()),
214
215 AcceptorConfig::StepCountingHillClimbing(step_counting_config) => {
216 AnyAcceptor::StepCountingHillClimbing(StepCountingHillClimbingAcceptor::new(
217 step_counting_config.step_count_limit.unwrap_or(100),
218 ))
219 }
220
221 AcceptorConfig::TabuSearch(tabu_config) => AnyAcceptor::TabuSearch(
222 TabuSearchAcceptor::<S>::new(normalize_tabu_search_policy(tabu_config)),
223 ),
224
225 AcceptorConfig::SimulatedAnnealing(sa_config) => AnyAcceptor::SimulatedAnnealing(
226 build_simulated_annealing::<S>(sa_config, random_seed),
227 ),
228
229 AcceptorConfig::LateAcceptance(la_config) => {
230 let size = la_config.late_acceptance_size.unwrap_or(400);
231 AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(size))
232 }
233
234 AcceptorConfig::DiversifiedLateAcceptance(dla_config) => {
235 let size = dla_config.late_acceptance_size.unwrap_or(400);
236 AnyAcceptor::DiversifiedLateAcceptance(DiversifiedLateAcceptanceAcceptor::<S>::new(
237 size,
238 ))
239 }
240
241 AcceptorConfig::GreatDeluge(gd_config) => {
242 let rain_speed = gd_config.water_level_increase_ratio.unwrap_or(0.001);
243 AnyAcceptor::GreatDeluge(GreatDelugeAcceptor::<S>::new(rain_speed))
244 }
245 }
246 }
247
248 pub fn hill_climbing<S: PlanningSolution>() -> HillClimbingAcceptor {
249 HillClimbingAcceptor::new()
250 }
251
252 pub fn tabu_search<S: PlanningSolution>(tabu_size: usize) -> TabuSearchAcceptor<S> {
253 TabuSearchAcceptor::<S>::new(TabuSearchPolicy::move_only(tabu_size))
254 }
255
256 pub fn simulated_annealing(starting_temp: f64, decay_rate: f64) -> SimulatedAnnealingAcceptor {
257 SimulatedAnnealingAcceptor::new(starting_temp, decay_rate)
258 }
259
260 pub fn late_acceptance<S: PlanningSolution>(size: usize) -> LateAcceptanceAcceptor<S> {
261 LateAcceptanceAcceptor::<S>::new(size)
262 }
263}
264
265fn build_simulated_annealing<S>(
266 config: &SimulatedAnnealingConfig,
267 random_seed: Option<u64>,
268) -> SimulatedAnnealingAcceptor
269where
270 S: PlanningSolution,
271 S::Score: Score,
272{
273 let level_count = S::Score::levels_count();
274 let decay_rate = config.decay_rate.unwrap_or(0.999985);
275 assert!(
276 decay_rate.is_finite() && decay_rate > 0.0 && decay_rate <= 1.0,
277 "simulated_annealing decay_rate must be finite and in (0, 1]"
278 );
279 let hill_climbing_temperature = config.hill_climbing_temperature.unwrap_or(1.0e-9);
280 assert!(
281 hill_climbing_temperature.is_finite() && hill_climbing_temperature >= 0.0,
282 "simulated_annealing hill_climbing_temperature must be finite and non-negative"
283 );
284 let hard_regression_policy = match config
285 .hard_regression_policy
286 .unwrap_or(HardRegressionPolicyConfig::TemperatureControlled)
287 {
288 HardRegressionPolicyConfig::TemperatureControlled => {
289 HardRegressionPolicy::TemperatureControlled
290 }
291 HardRegressionPolicyConfig::NeverAcceptHardRegression => {
292 HardRegressionPolicy::NeverAcceptHardRegression
293 }
294 };
295
296 if let Some(level_temperatures) = &config.level_temperatures {
297 validate_level_temperatures(level_temperatures, level_count);
298 return match random_seed {
299 Some(seed) => SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
300 level_temperatures.clone(),
301 decay_rate,
302 hill_climbing_temperature,
303 hard_regression_policy,
304 seed,
305 ),
306 None => SimulatedAnnealingAcceptor::with_level_temperatures_and_rng(
307 level_temperatures.clone(),
308 decay_rate,
309 hill_climbing_temperature,
310 hard_regression_policy,
311 ),
312 };
313 }
314
315 let calibration = normalize_simulated_annealing_calibration(config.calibration.as_ref());
316 validate_simulated_annealing_calibration(calibration);
317 match random_seed {
318 Some(seed) => SimulatedAnnealingAcceptor::with_calibration_and_seed(
319 decay_rate,
320 hill_climbing_temperature,
321 hard_regression_policy,
322 calibration,
323 seed,
324 ),
325 None => SimulatedAnnealingAcceptor::with_calibration(
326 decay_rate,
327 hill_climbing_temperature,
328 hard_regression_policy,
329 calibration,
330 ),
331 }
332}
333
334fn validate_level_temperatures(level_temperatures: &[f64], level_count: usize) {
335 assert_eq!(
336 level_temperatures.len(),
337 level_count,
338 "simulated_annealing level_temperatures length must match score level count"
339 );
340 for temperature in level_temperatures {
341 assert!(
342 temperature.is_finite() && *temperature >= 0.0,
343 "simulated_annealing level_temperatures must be finite and non-negative"
344 );
345 }
346}
347
348fn normalize_simulated_annealing_calibration(
349 config: Option<&SimulatedAnnealingCalibrationConfig>,
350) -> SimulatedAnnealingCalibration {
351 let defaults = SimulatedAnnealingCalibration::default();
352 SimulatedAnnealingCalibration {
353 sample_size: config
354 .and_then(|config| config.sample_size)
355 .unwrap_or(defaults.sample_size),
356 target_acceptance_probability: config
357 .and_then(|config| config.target_acceptance_probability)
358 .unwrap_or(defaults.target_acceptance_probability),
359 fallback_temperature: config
360 .and_then(|config| config.fallback_temperature)
361 .unwrap_or(defaults.fallback_temperature),
362 }
363}
364
365fn validate_simulated_annealing_calibration(calibration: SimulatedAnnealingCalibration) {
366 assert!(
367 calibration.sample_size > 0,
368 "simulated_annealing calibration sample_size must be greater than 0"
369 );
370 assert!(
371 calibration.target_acceptance_probability.is_finite()
372 && calibration.target_acceptance_probability > 0.0
373 && calibration.target_acceptance_probability < 1.0,
374 "simulated_annealing calibration target_acceptance_probability must be in (0, 1)"
375 );
376 assert!(
377 calibration.fallback_temperature.is_finite() && calibration.fallback_temperature >= 0.0,
378 "simulated_annealing calibration fallback_temperature must be finite and non-negative"
379 );
380}
381
382fn normalize_tabu_search_policy(config: &TabuSearchConfig) -> TabuSearchPolicy {
383 let aspiration_enabled = config.aspiration_enabled.unwrap_or(true);
384
385 match (
386 config.entity_tabu_size,
387 config.value_tabu_size,
388 config.move_tabu_size,
389 config.undo_move_tabu_size,
390 ) {
391 (None, None, None, None) => TabuSearchPolicy {
392 aspiration_enabled,
393 ..TabuSearchPolicy::move_only(10)
394 },
395 (entity_tabu_size, value_tabu_size, move_tabu_size, undo_move_tabu_size) => {
396 TabuSearchPolicy {
397 entity_tabu_size,
398 value_tabu_size,
399 move_tabu_size,
400 undo_move_tabu_size,
401 aspiration_enabled,
402 }
403 .validated()
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests;