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)]
26pub 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 let tolerance = dla_config.tolerance.unwrap_or(0.01);
237 AnyAcceptor::DiversifiedLateAcceptance(DiversifiedLateAcceptanceAcceptor::<S>::new(
238 size, tolerance,
239 ))
240 }
241
242 AcceptorConfig::GreatDeluge(gd_config) => {
243 let rain_speed = gd_config.water_level_increase_ratio.unwrap_or(0.001);
244 AnyAcceptor::GreatDeluge(GreatDelugeAcceptor::<S>::new(rain_speed))
245 }
246 }
247 }
248
249 pub fn hill_climbing<S: PlanningSolution>() -> HillClimbingAcceptor {
250 HillClimbingAcceptor::new()
251 }
252
253 pub fn tabu_search<S: PlanningSolution>(tabu_size: usize) -> TabuSearchAcceptor<S> {
254 TabuSearchAcceptor::<S>::new(TabuSearchPolicy::move_only(tabu_size))
255 }
256
257 pub fn simulated_annealing(starting_temp: f64, decay_rate: f64) -> SimulatedAnnealingAcceptor {
258 SimulatedAnnealingAcceptor::new(starting_temp, decay_rate)
259 }
260
261 pub fn late_acceptance<S: PlanningSolution>(size: usize) -> LateAcceptanceAcceptor<S> {
262 LateAcceptanceAcceptor::<S>::new(size)
263 }
264}
265
266fn build_simulated_annealing<S>(
267 config: &SimulatedAnnealingConfig,
268 random_seed: Option<u64>,
269) -> SimulatedAnnealingAcceptor
270where
271 S: PlanningSolution,
272 S::Score: Score,
273{
274 let level_count = S::Score::levels_count();
275 let decay_rate = config.decay_rate.unwrap_or(0.999985);
276 assert!(
277 decay_rate.is_finite() && decay_rate > 0.0 && decay_rate <= 1.0,
278 "simulated_annealing decay_rate must be finite and in (0, 1]"
279 );
280 let hill_climbing_temperature = config.hill_climbing_temperature.unwrap_or(1.0e-9);
281 assert!(
282 hill_climbing_temperature.is_finite() && hill_climbing_temperature >= 0.0,
283 "simulated_annealing hill_climbing_temperature must be finite and non-negative"
284 );
285 let hard_regression_policy = match config
286 .hard_regression_policy
287 .unwrap_or(HardRegressionPolicyConfig::TemperatureControlled)
288 {
289 HardRegressionPolicyConfig::TemperatureControlled => {
290 HardRegressionPolicy::TemperatureControlled
291 }
292 HardRegressionPolicyConfig::NeverAcceptHardRegression => {
293 HardRegressionPolicy::NeverAcceptHardRegression
294 }
295 };
296
297 if let Some(level_temperatures) = &config.level_temperatures {
298 validate_level_temperatures(level_temperatures, level_count);
299 return match random_seed {
300 Some(seed) => SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
301 level_temperatures.clone(),
302 decay_rate,
303 hill_climbing_temperature,
304 hard_regression_policy,
305 seed,
306 ),
307 None => SimulatedAnnealingAcceptor::with_level_temperatures_and_rng(
308 level_temperatures.clone(),
309 decay_rate,
310 hill_climbing_temperature,
311 hard_regression_policy,
312 ),
313 };
314 }
315
316 let calibration = normalize_simulated_annealing_calibration(config.calibration.as_ref());
317 validate_simulated_annealing_calibration(calibration);
318 match random_seed {
319 Some(seed) => SimulatedAnnealingAcceptor::with_calibration_and_seed(
320 decay_rate,
321 hill_climbing_temperature,
322 hard_regression_policy,
323 calibration,
324 seed,
325 ),
326 None => SimulatedAnnealingAcceptor::with_calibration(
327 decay_rate,
328 hill_climbing_temperature,
329 hard_regression_policy,
330 calibration,
331 ),
332 }
333}
334
335fn validate_level_temperatures(level_temperatures: &[f64], level_count: usize) {
336 assert_eq!(
337 level_temperatures.len(),
338 level_count,
339 "simulated_annealing level_temperatures length must match score level count"
340 );
341 for temperature in level_temperatures {
342 assert!(
343 temperature.is_finite() && *temperature >= 0.0,
344 "simulated_annealing level_temperatures must be finite and non-negative"
345 );
346 }
347}
348
349fn normalize_simulated_annealing_calibration(
350 config: Option<&SimulatedAnnealingCalibrationConfig>,
351) -> SimulatedAnnealingCalibration {
352 let defaults = SimulatedAnnealingCalibration::default();
353 SimulatedAnnealingCalibration {
354 sample_size: config
355 .and_then(|config| config.sample_size)
356 .unwrap_or(defaults.sample_size),
357 target_acceptance_probability: config
358 .and_then(|config| config.target_acceptance_probability)
359 .unwrap_or(defaults.target_acceptance_probability),
360 fallback_temperature: config
361 .and_then(|config| config.fallback_temperature)
362 .unwrap_or(defaults.fallback_temperature),
363 }
364}
365
366fn validate_simulated_annealing_calibration(calibration: SimulatedAnnealingCalibration) {
367 assert!(
368 calibration.sample_size > 0,
369 "simulated_annealing calibration sample_size must be greater than 0"
370 );
371 assert!(
372 calibration.target_acceptance_probability.is_finite()
373 && calibration.target_acceptance_probability > 0.0
374 && calibration.target_acceptance_probability < 1.0,
375 "simulated_annealing calibration target_acceptance_probability must be in (0, 1)"
376 );
377 assert!(
378 calibration.fallback_temperature.is_finite() && calibration.fallback_temperature >= 0.0,
379 "simulated_annealing calibration fallback_temperature must be finite and non-negative"
380 );
381}
382
383fn normalize_tabu_search_policy(config: &TabuSearchConfig) -> TabuSearchPolicy {
384 let aspiration_enabled = config.aspiration_enabled.unwrap_or(true);
385
386 match (
387 config.entity_tabu_size,
388 config.value_tabu_size,
389 config.move_tabu_size,
390 config.undo_move_tabu_size,
391 ) {
392 (None, None, None, None) => TabuSearchPolicy {
393 aspiration_enabled,
394 ..TabuSearchPolicy::move_only(10)
395 },
396 (entity_tabu_size, value_tabu_size, move_tabu_size, undo_move_tabu_size) => {
397 TabuSearchPolicy {
398 entity_tabu_size,
399 value_tabu_size,
400 move_tabu_size,
401 undo_move_tabu_size,
402 aspiration_enabled,
403 }
404 .validated()
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests;