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;
6use solverforge_core::domain::PlanningSolution;
7use solverforge_core::score::{ParseableScore, Score};
8
9use crate::phase::localsearch::{
10    Acceptor, GreatDelugeAcceptor, HillClimbingAcceptor, LateAcceptanceAcceptor,
11    SimulatedAnnealingAcceptor, TabuSearchAcceptor,
12};
13
14/* A concrete enum over all built-in acceptor types.
15
16Returned by [`AcceptorBuilder::build`] to avoid `Box<dyn Acceptor<S>>`.
17Dispatches to the inner acceptor via `match` — fully monomorphized.
18*/
19#[allow(clippy::large_enum_variant)]
20pub enum AnyAcceptor<S: PlanningSolution> {
21    // Hill climbing acceptor.
22    HillClimbing(HillClimbingAcceptor),
23    // Tabu search acceptor.
24    TabuSearch(TabuSearchAcceptor<S>),
25    // Simulated annealing acceptor.
26    SimulatedAnnealing(SimulatedAnnealingAcceptor),
27    // Late acceptance acceptor.
28    LateAcceptance(LateAcceptanceAcceptor<S>),
29    // Great deluge acceptor.
30    GreatDeluge(GreatDelugeAcceptor<S>),
31}
32
33impl<S: PlanningSolution> Debug for AnyAcceptor<S> {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            Self::HillClimbing(a) => write!(f, "AnyAcceptor::HillClimbing({a:?})"),
37            Self::TabuSearch(a) => write!(f, "AnyAcceptor::TabuSearch({a:?})"),
38            Self::SimulatedAnnealing(a) => write!(f, "AnyAcceptor::SimulatedAnnealing({a:?})"),
39            Self::LateAcceptance(a) => write!(f, "AnyAcceptor::LateAcceptance({a:?})"),
40            Self::GreatDeluge(a) => write!(f, "AnyAcceptor::GreatDeluge({a:?})"),
41        }
42    }
43}
44
45impl<S: PlanningSolution> Clone for AnyAcceptor<S>
46where
47    S::Score: Clone,
48{
49    fn clone(&self) -> Self {
50        match self {
51            Self::HillClimbing(a) => Self::HillClimbing(a.clone()),
52            Self::TabuSearch(a) => Self::TabuSearch(a.clone()),
53            Self::SimulatedAnnealing(a) => Self::SimulatedAnnealing(a.clone()),
54            Self::LateAcceptance(a) => Self::LateAcceptance(a.clone()),
55            Self::GreatDeluge(a) => Self::GreatDeluge(a.clone()),
56        }
57    }
58}
59
60impl<S: PlanningSolution> Acceptor<S> for AnyAcceptor<S>
61where
62    S::Score: Score,
63{
64    fn is_accepted(&mut self, last_step_score: &S::Score, move_score: &S::Score) -> bool {
65        match self {
66            Self::HillClimbing(a) => Acceptor::<S>::is_accepted(a, last_step_score, move_score),
67            Self::TabuSearch(a) => Acceptor::<S>::is_accepted(a, last_step_score, move_score),
68            Self::SimulatedAnnealing(a) => {
69                Acceptor::<S>::is_accepted(a, last_step_score, move_score)
70            }
71            Self::LateAcceptance(a) => Acceptor::<S>::is_accepted(a, last_step_score, move_score),
72            Self::GreatDeluge(a) => Acceptor::<S>::is_accepted(a, last_step_score, move_score),
73        }
74    }
75
76    fn phase_started(&mut self, initial_score: &S::Score) {
77        match self {
78            Self::HillClimbing(a) => Acceptor::<S>::phase_started(a, initial_score),
79            Self::TabuSearch(a) => Acceptor::<S>::phase_started(a, initial_score),
80            Self::SimulatedAnnealing(a) => Acceptor::<S>::phase_started(a, initial_score),
81            Self::LateAcceptance(a) => Acceptor::<S>::phase_started(a, initial_score),
82            Self::GreatDeluge(a) => Acceptor::<S>::phase_started(a, initial_score),
83        }
84    }
85
86    fn phase_ended(&mut self) {
87        match self {
88            Self::HillClimbing(a) => Acceptor::<S>::phase_ended(a),
89            Self::TabuSearch(a) => Acceptor::<S>::phase_ended(a),
90            Self::SimulatedAnnealing(a) => Acceptor::<S>::phase_ended(a),
91            Self::LateAcceptance(a) => Acceptor::<S>::phase_ended(a),
92            Self::GreatDeluge(a) => Acceptor::<S>::phase_ended(a),
93        }
94    }
95
96    fn step_started(&mut self) {
97        match self {
98            Self::HillClimbing(a) => Acceptor::<S>::step_started(a),
99            Self::TabuSearch(a) => Acceptor::<S>::step_started(a),
100            Self::SimulatedAnnealing(a) => Acceptor::<S>::step_started(a),
101            Self::LateAcceptance(a) => Acceptor::<S>::step_started(a),
102            Self::GreatDeluge(a) => Acceptor::<S>::step_started(a),
103        }
104    }
105
106    fn step_ended(&mut self, step_score: &S::Score) {
107        match self {
108            Self::HillClimbing(a) => Acceptor::<S>::step_ended(a, step_score),
109            Self::TabuSearch(a) => Acceptor::<S>::step_ended(a, step_score),
110            Self::SimulatedAnnealing(a) => Acceptor::<S>::step_ended(a, step_score),
111            Self::LateAcceptance(a) => Acceptor::<S>::step_ended(a, step_score),
112            Self::GreatDeluge(a) => Acceptor::<S>::step_ended(a, step_score),
113        }
114    }
115}
116
117/// Builder for constructing acceptors from configuration.
118pub struct AcceptorBuilder;
119
120impl AcceptorBuilder {
121    /// Builds a concrete [`AnyAcceptor`] from configuration.
122    pub fn build<S: PlanningSolution>(config: &AcceptorConfig) -> AnyAcceptor<S>
123    where
124        S::Score: Score + ParseableScore,
125    {
126        match config {
127            AcceptorConfig::HillClimbing => AnyAcceptor::HillClimbing(HillClimbingAcceptor::new()),
128
129            AcceptorConfig::TabuSearch(tabu_config) => {
130                let tabu_size = tabu_config
131                    .entity_tabu_size
132                    .or(tabu_config.move_tabu_size)
133                    .unwrap_or(7);
134                AnyAcceptor::TabuSearch(TabuSearchAcceptor::<S>::new(tabu_size))
135            }
136
137            AcceptorConfig::SimulatedAnnealing(sa_config) => {
138                let starting_temp = sa_config
139                    .starting_temperature
140                    .as_ref()
141                    .map(|s| {
142                        s.parse::<f64>()
143                            .ok()
144                            .or_else(|| S::Score::parse(s).ok().map(|score| score.to_scalar().abs()))
145                            .unwrap_or_else(|| {
146                                panic!("Invalid starting_temperature '{}': expected scalar or score string", s)
147                            })
148                    })
149                    .unwrap_or(0.0);
150                AnyAcceptor::SimulatedAnnealing(SimulatedAnnealingAcceptor::new(
151                    starting_temp,
152                    0.999985,
153                ))
154            }
155
156            AcceptorConfig::LateAcceptance(la_config) => {
157                let size = la_config.late_acceptance_size.unwrap_or(400);
158                AnyAcceptor::LateAcceptance(LateAcceptanceAcceptor::<S>::new(size))
159            }
160
161            AcceptorConfig::GreatDeluge(gd_config) => {
162                let rain_speed = gd_config.water_level_increase_ratio.unwrap_or(0.001);
163                AnyAcceptor::GreatDeluge(GreatDelugeAcceptor::<S>::new(rain_speed))
164            }
165        }
166    }
167
168    pub fn hill_climbing<S: PlanningSolution>() -> HillClimbingAcceptor {
169        HillClimbingAcceptor::new()
170    }
171
172    pub fn tabu_search<S: PlanningSolution>(tabu_size: usize) -> TabuSearchAcceptor<S> {
173        TabuSearchAcceptor::<S>::new(tabu_size)
174    }
175
176    pub fn simulated_annealing(starting_temp: f64, decay_rate: f64) -> SimulatedAnnealingAcceptor {
177        SimulatedAnnealingAcceptor::new(starting_temp, decay_rate)
178    }
179
180    pub fn late_acceptance<S: PlanningSolution>(size: usize) -> LateAcceptanceAcceptor<S> {
181        LateAcceptanceAcceptor::<S>::new(size)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use solverforge_config::{
189        AcceptorConfig, LateAcceptanceConfig, SimulatedAnnealingConfig, TabuSearchConfig,
190    };
191    use solverforge_core::score::SoftScore;
192
193    #[derive(Clone, Debug)]
194    struct TestSolution {
195        score: Option<SoftScore>,
196    }
197
198    impl PlanningSolution for TestSolution {
199        type Score = SoftScore;
200        fn score(&self) -> Option<Self::Score> {
201            self.score
202        }
203        fn set_score(&mut self, score: Option<Self::Score>) {
204            self.score = score;
205        }
206    }
207
208    #[test]
209    fn test_acceptor_builder_hill_climbing() {
210        let config = AcceptorConfig::HillClimbing;
211        let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
212    }
213
214    #[test]
215    fn test_acceptor_builder_tabu_search() {
216        let config = AcceptorConfig::TabuSearch(TabuSearchConfig {
217            entity_tabu_size: Some(10),
218            ..Default::default()
219        });
220        let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
221    }
222
223    #[test]
224    fn test_acceptor_builder_simulated_annealing() {
225        let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
226            starting_temperature: Some("2".to_string()),
227        });
228        let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
229    }
230
231    #[test]
232    fn test_acceptor_builder_simulated_annealing_accepts_fractional_scalar() {
233        let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
234            starting_temperature: Some("2.5".to_string()),
235        });
236        let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
237    }
238
239    #[test]
240    fn test_acceptor_builder_late_acceptance() {
241        let config = AcceptorConfig::LateAcceptance(LateAcceptanceConfig {
242            late_acceptance_size: Some(500),
243        });
244        let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
245    }
246}