Skip to main content

solverforge_solver/builder/
acceptor.rs

1// Acceptor builder and `AnyAcceptor` enum.
2
3use std::fmt::Debug;
4
5use solverforge_config::{AcceptorConfig, TabuSearchConfig};
6use solverforge_core::domain::PlanningSolution;
7use solverforge_core::score::{ParseableScore, Score};
8
9use crate::heuristic::r#move::MoveTabuSignature;
10use crate::phase::localsearch::{
11    Acceptor, DiversifiedLateAcceptanceAcceptor, GreatDelugeAcceptor, HillClimbingAcceptor,
12    LateAcceptanceAcceptor, SimulatedAnnealingAcceptor, StepCountingHillClimbingAcceptor,
13    TabuSearchAcceptor, TabuSearchPolicy,
14};
15
16/* A concrete enum over all built-in acceptor types.
17
18Returned by [`AcceptorBuilder::build`] to avoid `Box<dyn Acceptor<S>>`.
19Dispatches to the inner acceptor via `match` — fully monomorphized.
20*/
21#[allow(clippy::large_enum_variant)]
22pub enum AnyAcceptor<S: PlanningSolution> {
23    // Hill climbing acceptor.
24    HillClimbing(HillClimbingAcceptor),
25    // Step counting hill climbing acceptor.
26    StepCountingHillClimbing(StepCountingHillClimbingAcceptor<S>),
27    // Tabu search acceptor.
28    TabuSearch(TabuSearchAcceptor<S>),
29    // Simulated annealing acceptor.
30    SimulatedAnnealing(SimulatedAnnealingAcceptor),
31    // Late acceptance acceptor.
32    LateAcceptance(LateAcceptanceAcceptor<S>),
33    // Diversified late acceptance acceptor.
34    DiversifiedLateAcceptance(DiversifiedLateAcceptanceAcceptor<S>),
35    // Great deluge acceptor.
36    GreatDeluge(GreatDelugeAcceptor<S>),
37}
38
39impl<S: PlanningSolution> Debug for AnyAcceptor<S> {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::HillClimbing(a) => write!(f, "AnyAcceptor::HillClimbing({a:?})"),
43            Self::StepCountingHillClimbing(a) => {
44                write!(f, "AnyAcceptor::StepCountingHillClimbing({a:?})")
45            }
46            Self::TabuSearch(a) => write!(f, "AnyAcceptor::TabuSearch({a:?})"),
47            Self::SimulatedAnnealing(a) => write!(f, "AnyAcceptor::SimulatedAnnealing({a:?})"),
48            Self::LateAcceptance(a) => write!(f, "AnyAcceptor::LateAcceptance({a:?})"),
49            Self::DiversifiedLateAcceptance(a) => {
50                write!(f, "AnyAcceptor::DiversifiedLateAcceptance({a:?})")
51            }
52            Self::GreatDeluge(a) => write!(f, "AnyAcceptor::GreatDeluge({a:?})"),
53        }
54    }
55}
56
57impl<S: PlanningSolution> Clone for AnyAcceptor<S>
58where
59    S::Score: Clone,
60{
61    fn clone(&self) -> Self {
62        match self {
63            Self::HillClimbing(a) => Self::HillClimbing(a.clone()),
64            Self::StepCountingHillClimbing(a) => Self::StepCountingHillClimbing(a.clone()),
65            Self::TabuSearch(a) => Self::TabuSearch(a.clone()),
66            Self::SimulatedAnnealing(a) => Self::SimulatedAnnealing(a.clone()),
67            Self::LateAcceptance(a) => Self::LateAcceptance(a.clone()),
68            Self::DiversifiedLateAcceptance(a) => Self::DiversifiedLateAcceptance(a.clone()),
69            Self::GreatDeluge(a) => Self::GreatDeluge(a.clone()),
70        }
71    }
72}
73
74impl<S: PlanningSolution> Acceptor<S> for AnyAcceptor<S>
75where
76    S::Score: Score,
77{
78    fn requires_move_signatures(&self) -> bool {
79        match self {
80            Self::HillClimbing(a) => Acceptor::<S>::requires_move_signatures(a),
81            Self::StepCountingHillClimbing(a) => Acceptor::<S>::requires_move_signatures(a),
82            Self::TabuSearch(a) => Acceptor::<S>::requires_move_signatures(a),
83            Self::SimulatedAnnealing(a) => Acceptor::<S>::requires_move_signatures(a),
84            Self::LateAcceptance(a) => Acceptor::<S>::requires_move_signatures(a),
85            Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::requires_move_signatures(a),
86            Self::GreatDeluge(a) => Acceptor::<S>::requires_move_signatures(a),
87        }
88    }
89
90    fn is_accepted(
91        &mut self,
92        last_step_score: &S::Score,
93        move_score: &S::Score,
94        move_signature: Option<&MoveTabuSignature>,
95    ) -> bool {
96        match self {
97            Self::HillClimbing(a) => {
98                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
99            }
100            Self::StepCountingHillClimbing(a) => {
101                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
102            }
103            Self::TabuSearch(a) => {
104                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
105            }
106            Self::SimulatedAnnealing(a) => {
107                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
108            }
109            Self::LateAcceptance(a) => {
110                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
111            }
112            Self::DiversifiedLateAcceptance(a) => {
113                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
114            }
115            Self::GreatDeluge(a) => {
116                Acceptor::<S>::is_accepted(a, last_step_score, move_score, move_signature)
117            }
118        }
119    }
120
121    fn phase_started(&mut self, initial_score: &S::Score) {
122        match self {
123            Self::HillClimbing(a) => Acceptor::<S>::phase_started(a, initial_score),
124            Self::StepCountingHillClimbing(a) => Acceptor::<S>::phase_started(a, initial_score),
125            Self::TabuSearch(a) => Acceptor::<S>::phase_started(a, initial_score),
126            Self::SimulatedAnnealing(a) => Acceptor::<S>::phase_started(a, initial_score),
127            Self::LateAcceptance(a) => Acceptor::<S>::phase_started(a, initial_score),
128            Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::phase_started(a, initial_score),
129            Self::GreatDeluge(a) => Acceptor::<S>::phase_started(a, initial_score),
130        }
131    }
132
133    fn phase_ended(&mut self) {
134        match self {
135            Self::HillClimbing(a) => Acceptor::<S>::phase_ended(a),
136            Self::StepCountingHillClimbing(a) => Acceptor::<S>::phase_ended(a),
137            Self::TabuSearch(a) => Acceptor::<S>::phase_ended(a),
138            Self::SimulatedAnnealing(a) => Acceptor::<S>::phase_ended(a),
139            Self::LateAcceptance(a) => Acceptor::<S>::phase_ended(a),
140            Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::phase_ended(a),
141            Self::GreatDeluge(a) => Acceptor::<S>::phase_ended(a),
142        }
143    }
144
145    fn step_started(&mut self) {
146        match self {
147            Self::HillClimbing(a) => Acceptor::<S>::step_started(a),
148            Self::StepCountingHillClimbing(a) => Acceptor::<S>::step_started(a),
149            Self::TabuSearch(a) => Acceptor::<S>::step_started(a),
150            Self::SimulatedAnnealing(a) => Acceptor::<S>::step_started(a),
151            Self::LateAcceptance(a) => Acceptor::<S>::step_started(a),
152            Self::DiversifiedLateAcceptance(a) => Acceptor::<S>::step_started(a),
153            Self::GreatDeluge(a) => Acceptor::<S>::step_started(a),
154        }
155    }
156
157    fn step_ended(
158        &mut self,
159        step_score: &S::Score,
160        accepted_move_signature: Option<&MoveTabuSignature>,
161    ) {
162        match self {
163            Self::HillClimbing(a) => {
164                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
165            }
166            Self::StepCountingHillClimbing(a) => {
167                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
168            }
169            Self::TabuSearch(a) => {
170                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
171            }
172            Self::SimulatedAnnealing(a) => {
173                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
174            }
175            Self::LateAcceptance(a) => {
176                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
177            }
178            Self::DiversifiedLateAcceptance(a) => {
179                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
180            }
181            Self::GreatDeluge(a) => {
182                Acceptor::<S>::step_ended(a, step_score, accepted_move_signature)
183            }
184        }
185    }
186}
187
188/// Builder for constructing acceptors from configuration.
189pub struct AcceptorBuilder;
190
191impl AcceptorBuilder {
192    /// Builds a concrete [`AnyAcceptor`] from configuration.
193    pub fn build<S: PlanningSolution>(config: &AcceptorConfig) -> AnyAcceptor<S>
194    where
195        S::Score: Score + ParseableScore,
196    {
197        Self::build_with_seed(config, None)
198    }
199
200    /// Builds a concrete [`AnyAcceptor`] from configuration with an optional deterministic seed.
201    pub fn build_with_seed<S: PlanningSolution>(
202        config: &AcceptorConfig,
203        random_seed: Option<u64>,
204    ) -> AnyAcceptor<S>
205    where
206        S::Score: Score + ParseableScore,
207    {
208        match config {
209            AcceptorConfig::HillClimbing => AnyAcceptor::HillClimbing(HillClimbingAcceptor::new()),
210
211            AcceptorConfig::StepCountingHillClimbing(step_counting_config) => {
212                AnyAcceptor::StepCountingHillClimbing(StepCountingHillClimbingAcceptor::new(
213                    step_counting_config.step_count_limit.unwrap_or(100),
214                ))
215            }
216
217            AcceptorConfig::TabuSearch(tabu_config) => AnyAcceptor::TabuSearch(
218                TabuSearchAcceptor::<S>::new(normalize_tabu_search_policy(tabu_config)),
219            ),
220
221            AcceptorConfig::SimulatedAnnealing(sa_config) => {
222                let starting_temp = sa_config.starting_temperature.as_ref().map(|s| {
223                    s.parse::<f64>()
224                        .ok()
225                        .or_else(|| S::Score::parse(s).ok().map(|score| score.to_scalar().abs()))
226                        .unwrap_or_else(|| {
227                            panic!("Invalid starting_temperature '{}': expected scalar or score string", s)
228                        })
229                });
230                let decay_rate = sa_config.decay_rate.unwrap_or(0.999985);
231                AnyAcceptor::SimulatedAnnealing(match (starting_temp, random_seed) {
232                    (Some(temp), Some(seed)) => {
233                        SimulatedAnnealingAcceptor::with_seed(temp, decay_rate, seed)
234                    }
235                    (Some(temp), None) => SimulatedAnnealingAcceptor::new(temp, decay_rate),
236                    (None, Some(seed)) => {
237                        SimulatedAnnealingAcceptor::auto_calibrate_with_seed(decay_rate, seed)
238                    }
239                    (None, None) => SimulatedAnnealingAcceptor::auto_calibrate(decay_rate),
240                })
241            }
242
243            AcceptorConfig::LateAcceptance(la_config) => {
244                let size = la_config.late_acceptance_size.unwrap_or(400);
245                AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(size))
246            }
247
248            AcceptorConfig::DiversifiedLateAcceptance(dla_config) => {
249                let size = dla_config.late_acceptance_size.unwrap_or(400);
250                let tolerance = dla_config.tolerance.unwrap_or(0.01);
251                AnyAcceptor::DiversifiedLateAcceptance(DiversifiedLateAcceptanceAcceptor::<S>::new(
252                    size, tolerance,
253                ))
254            }
255
256            AcceptorConfig::GreatDeluge(gd_config) => {
257                let rain_speed = gd_config.water_level_increase_ratio.unwrap_or(0.001);
258                AnyAcceptor::GreatDeluge(GreatDelugeAcceptor::<S>::new(rain_speed))
259            }
260        }
261    }
262
263    pub fn hill_climbing<S: PlanningSolution>() -> HillClimbingAcceptor {
264        HillClimbingAcceptor::new()
265    }
266
267    pub fn tabu_search<S: PlanningSolution>(tabu_size: usize) -> TabuSearchAcceptor<S> {
268        TabuSearchAcceptor::<S>::new(TabuSearchPolicy::move_only(tabu_size))
269    }
270
271    pub fn simulated_annealing(starting_temp: f64, decay_rate: f64) -> SimulatedAnnealingAcceptor {
272        SimulatedAnnealingAcceptor::new(starting_temp, decay_rate)
273    }
274
275    pub fn late_acceptance<S: PlanningSolution>(size: usize) -> LateAcceptanceAcceptor<S> {
276        LateAcceptanceAcceptor::<S>::new(size)
277    }
278}
279
280fn normalize_tabu_search_policy(config: &TabuSearchConfig) -> TabuSearchPolicy {
281    let aspiration_enabled = config.aspiration_enabled.unwrap_or(true);
282
283    match (
284        config.entity_tabu_size,
285        config.value_tabu_size,
286        config.move_tabu_size,
287        config.undo_move_tabu_size,
288    ) {
289        (None, None, None, None) => TabuSearchPolicy {
290            aspiration_enabled,
291            ..TabuSearchPolicy::move_only(10)
292        },
293        (entity_tabu_size, value_tabu_size, move_tabu_size, undo_move_tabu_size) => {
294            TabuSearchPolicy {
295                entity_tabu_size,
296                value_tabu_size,
297                move_tabu_size,
298                undo_move_tabu_size,
299                aspiration_enabled,
300            }
301            .validated()
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests;