Skip to main content

solverforge_solver/builder/
acceptor.rs

1// Acceptor builder and `AnyAcceptor` enum.
2
3use 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/* A concrete enum over all built-in acceptor types.
21
22Returned by [`AcceptorBuilder::build`] to avoid `Box<dyn Acceptor<S>>`.
23Dispatches to the inner acceptor via `match` — fully monomorphized.
24*/
25#[allow(clippy::large_enum_variant)]
26pub enum AnyAcceptor<S: PlanningSolution> {
27    // Hill climbing acceptor.
28    HillClimbing(HillClimbingAcceptor),
29    // Step counting hill climbing acceptor.
30    StepCountingHillClimbing(StepCountingHillClimbingAcceptor<S>),
31    // Tabu search acceptor.
32    TabuSearch(TabuSearchAcceptor<S>),
33    // Simulated annealing acceptor.
34    SimulatedAnnealing(SimulatedAnnealingAcceptor),
35    // Late acceptance acceptor.
36    LateAcceptance(LateAcceptanceAcceptor<S>),
37    // Diversified late acceptance acceptor.
38    DiversifiedLateAcceptance(DiversifiedLateAcceptanceAcceptor<S>),
39    // Great deluge acceptor.
40    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
192/// Builder for constructing acceptors from configuration.
193pub struct AcceptorBuilder;
194
195impl AcceptorBuilder {
196    /// Builds a concrete [`AnyAcceptor`] from configuration.
197    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    /// Builds a concrete [`AnyAcceptor`] from configuration with an optional deterministic seed.
205    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;