solverforge_solver/
builder.rs

1//! Builder module for constructing solver components from configuration
2//!
3//! This module provides the wiring between configuration types and
4//! the actual solver implementation.
5
6use std::time::Duration;
7
8use solverforge_config::{AcceptorConfig, TerminationConfig};
9use solverforge_core::domain::PlanningSolution;
10use solverforge_core::score::Score;
11
12use crate::phase::localsearch::{
13    Acceptor, HillClimbingAcceptor, LateAcceptanceAcceptor, SimulatedAnnealingAcceptor,
14    TabuSearchAcceptor,
15};
16use crate::phase::Phase;
17use crate::termination::{
18    AndCompositeTermination, BestScoreTermination, OrCompositeTermination, StepCountTermination,
19    Termination, TimeTermination, UnimprovedStepCountTermination, UnimprovedTimeTermination,
20};
21
22/// Builder for constructing termination conditions from configuration.
23pub struct TerminationBuilder;
24
25impl TerminationBuilder {
26    /// Builds a termination condition from configuration.
27    ///
28    /// Multiple termination conditions are combined with OR logic
29    /// (any condition being met will terminate solving).
30    pub fn build<S: PlanningSolution>(
31        config: &TerminationConfig,
32    ) -> Option<Box<dyn Termination<S>>> {
33        let mut terminations: Vec<Box<dyn Termination<S>>> = Vec::new();
34
35        // Time-based termination
36        if let Some(time_limit) = config.time_limit() {
37            terminations.push(Box::new(TimeTermination::new(time_limit)));
38        }
39
40        // Step count termination
41        if let Some(step_limit) = config.step_count_limit {
42            terminations.push(Box::new(StepCountTermination::new(step_limit as u64)));
43        }
44
45        // Unimproved step count termination
46        if let Some(unimproved_limit) = config.unimproved_step_count_limit {
47            terminations.push(Box::new(UnimprovedStepCountTermination::<S>::new(
48                unimproved_limit as u64,
49            )));
50        }
51
52        // Unimproved time termination
53        if let Some(unimproved_seconds) = config.unimproved_seconds_spent_limit {
54            terminations.push(Box::new(UnimprovedTimeTermination::<S>::new(
55                Duration::from_secs(unimproved_seconds),
56            )));
57        }
58
59        // Combine terminations
60        match terminations.len() {
61            0 => None,
62            1 => Some(terminations.remove(0)),
63            _ => Some(Box::new(OrCompositeTermination::new(terminations))),
64        }
65    }
66
67    /// Builds a termination condition that requires ALL conditions to be met.
68    pub fn build_and<S: PlanningSolution>(
69        config: &TerminationConfig,
70    ) -> Option<Box<dyn Termination<S>>> {
71        let mut terminations: Vec<Box<dyn Termination<S>>> = Vec::new();
72
73        if let Some(time_limit) = config.time_limit() {
74            terminations.push(Box::new(TimeTermination::new(time_limit)));
75        }
76
77        if let Some(step_limit) = config.step_count_limit {
78            terminations.push(Box::new(StepCountTermination::new(step_limit as u64)));
79        }
80
81        match terminations.len() {
82            0 => None,
83            1 => Some(terminations.remove(0)),
84            _ => Some(Box::new(AndCompositeTermination::new(terminations))),
85        }
86    }
87
88    /// Creates a termination that stops when best score reaches a target.
89    pub fn best_score<S, Sc>(target: Sc) -> Box<dyn Termination<S>>
90    where
91        S: PlanningSolution<Score = Sc>,
92        Sc: Score,
93    {
94        Box::new(BestScoreTermination::new(target))
95    }
96}
97
98/// Builder for constructing acceptors from configuration.
99pub struct AcceptorBuilder;
100
101impl AcceptorBuilder {
102    /// Builds an acceptor from configuration.
103    pub fn build<S: PlanningSolution>(config: &AcceptorConfig) -> Box<dyn Acceptor<S>> {
104        match config {
105            AcceptorConfig::HillClimbing => Box::new(HillClimbingAcceptor::new()),
106
107            AcceptorConfig::TabuSearch(tabu_config) => {
108                // Use entity tabu size if specified, otherwise default
109                let tabu_size = tabu_config
110                    .entity_tabu_size
111                    .or(tabu_config.move_tabu_size)
112                    .unwrap_or(7);
113                Box::new(TabuSearchAcceptor::<S>::new(tabu_size))
114            }
115
116            AcceptorConfig::SimulatedAnnealing(sa_config) => {
117                // Parse starting temperature (default to 1.0 if not specified)
118                let starting_temp = sa_config
119                    .starting_temperature
120                    .as_ref()
121                    .and_then(|s| s.parse::<f64>().ok())
122                    .unwrap_or(1.0);
123                Box::new(SimulatedAnnealingAcceptor::new(starting_temp, 0.99))
124            }
125
126            AcceptorConfig::LateAcceptance(la_config) => {
127                let size = la_config.late_acceptance_size.unwrap_or(400);
128                Box::new(LateAcceptanceAcceptor::<S>::new(size))
129            }
130
131            AcceptorConfig::GreatDeluge(_) => {
132                // Great deluge not yet implemented, fall back to hill climbing
133                tracing::warn!("Great deluge acceptor not yet implemented, using hill climbing");
134                Box::new(HillClimbingAcceptor::new())
135            }
136        }
137    }
138
139    /// Creates a default hill climbing acceptor.
140    pub fn hill_climbing<S: PlanningSolution>() -> Box<dyn Acceptor<S>> {
141        Box::new(HillClimbingAcceptor::new())
142    }
143
144    /// Creates a tabu search acceptor with the given size.
145    pub fn tabu_search<S: PlanningSolution>(tabu_size: usize) -> Box<dyn Acceptor<S>> {
146        Box::new(TabuSearchAcceptor::<S>::new(tabu_size))
147    }
148
149    /// Creates a simulated annealing acceptor.
150    pub fn simulated_annealing<S: PlanningSolution>(
151        starting_temp: f64,
152        decay_rate: f64,
153    ) -> Box<dyn Acceptor<S>> {
154        Box::new(SimulatedAnnealingAcceptor::new(starting_temp, decay_rate))
155    }
156
157    /// Creates a late acceptance acceptor.
158    pub fn late_acceptance<S: PlanningSolution>(size: usize) -> Box<dyn Acceptor<S>> {
159        Box::new(LateAcceptanceAcceptor::<S>::new(size))
160    }
161}
162
163/// Builder for constructing a complete solver from configuration.
164///
165/// Note: Phase building now requires typed move selectors, so phases
166/// must be constructed directly using the typed phase constructors.
167///
168/// # Example
169///
170/// ```
171/// use solverforge_solver::{SolverBuilder, StepCountTermination};
172/// use solverforge_core::domain::PlanningSolution;
173/// use solverforge_core::score::SimpleScore;
174///
175/// #[derive(Clone, Debug)]
176/// struct MySolution { score: Option<SimpleScore> }
177///
178/// impl PlanningSolution for MySolution {
179///     type Score = SimpleScore;
180///     fn score(&self) -> Option<Self::Score> { self.score }
181///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
182/// }
183///
184/// let solver = SolverBuilder::<MySolution>::new()
185///     .with_termination(Box::new(StepCountTermination::new(100)))
186///     .build();
187/// ```
188pub struct SolverBuilder<S: PlanningSolution> {
189    phases: Vec<Box<dyn Phase<S>>>,
190    termination: Option<Box<dyn Termination<S>>>,
191}
192
193impl<S: PlanningSolution> SolverBuilder<S> {
194    /// Creates a new solver builder.
195    pub fn new() -> Self {
196        SolverBuilder {
197            phases: Vec::new(),
198            termination: None,
199        }
200    }
201
202    /// Adds a phase to the builder.
203    pub fn with_phase(mut self, phase: Box<dyn Phase<S>>) -> Self {
204        self.phases.push(phase);
205        self
206    }
207
208    /// Adds multiple phases to the builder.
209    pub fn with_phases(mut self, phases: Vec<Box<dyn Phase<S>>>) -> Self {
210        self.phases.extend(phases);
211        self
212    }
213
214    /// Sets the termination condition.
215    pub fn with_termination(mut self, termination: Box<dyn Termination<S>>) -> Self {
216        self.termination = Some(termination);
217        self
218    }
219
220    /// Sets a time-based termination condition.
221    ///
222    /// # Example
223    ///
224    /// ```
225    /// use solverforge_solver::SolverBuilder;
226    /// use solverforge_core::domain::PlanningSolution;
227    /// use solverforge_core::score::SimpleScore;
228    /// use std::time::Duration;
229    ///
230    /// #[derive(Clone, Debug)]
231    /// struct MySolution { score: Option<SimpleScore> }
232    ///
233    /// impl PlanningSolution for MySolution {
234    ///     type Score = SimpleScore;
235    ///     fn score(&self) -> Option<Self::Score> { self.score }
236    ///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
237    /// }
238    ///
239    /// let solver = SolverBuilder::<MySolution>::new()
240    ///     .with_time_limit(Duration::from_secs(30))
241    ///     .build();
242    /// ```
243    pub fn with_time_limit(mut self, duration: Duration) -> Self {
244        self.termination = Some(Box::new(TimeTermination::new(duration)));
245        self
246    }
247
248    /// Sets a step count termination condition.
249    pub fn with_step_limit(mut self, steps: u64) -> Self {
250        self.termination = Some(Box::new(StepCountTermination::new(steps)));
251        self
252    }
253
254    /// Builds termination from the configuration.
255    pub fn with_termination_from_config(mut self, config: &TerminationConfig) -> Self {
256        self.termination = TerminationBuilder::build(config);
257        self
258    }
259
260    /// Builds the solver.
261    pub fn build(self) -> crate::solver::Solver<S> {
262        let mut solver = crate::solver::Solver::new(self.phases);
263        if let Some(termination) = self.termination {
264            solver = solver.with_termination(termination);
265        }
266        solver
267    }
268}
269
270impl<S: PlanningSolution> Default for SolverBuilder<S> {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use solverforge_config::{
280        AcceptorConfig, LateAcceptanceConfig, SimulatedAnnealingConfig, TabuSearchConfig,
281        TerminationConfig,
282    };
283    use solverforge_core::score::SimpleScore;
284
285    #[derive(Clone, Debug)]
286    struct TestSolution {
287        score: Option<SimpleScore>,
288    }
289
290    impl PlanningSolution for TestSolution {
291        type Score = SimpleScore;
292        fn score(&self) -> Option<Self::Score> {
293            self.score
294        }
295        fn set_score(&mut self, score: Option<Self::Score>) {
296            self.score = score;
297        }
298    }
299
300    #[test]
301    fn test_termination_builder_time_limit() {
302        let config = TerminationConfig {
303            seconds_spent_limit: Some(30),
304            ..Default::default()
305        };
306
307        let term = TerminationBuilder::build::<TestSolution>(&config);
308        assert!(term.is_some());
309    }
310
311    #[test]
312    fn test_termination_builder_step_limit() {
313        let config = TerminationConfig {
314            step_count_limit: Some(100),
315            ..Default::default()
316        };
317
318        let term = TerminationBuilder::build::<TestSolution>(&config);
319        assert!(term.is_some());
320    }
321
322    #[test]
323    fn test_termination_builder_multiple() {
324        let config = TerminationConfig {
325            seconds_spent_limit: Some(30),
326            step_count_limit: Some(100),
327            ..Default::default()
328        };
329
330        let term = TerminationBuilder::build::<TestSolution>(&config);
331        assert!(term.is_some());
332    }
333
334    #[test]
335    fn test_termination_builder_empty() {
336        let config = TerminationConfig::default();
337        let term = TerminationBuilder::build::<TestSolution>(&config);
338        assert!(term.is_none());
339    }
340
341    #[test]
342    fn test_termination_builder_unimproved() {
343        let config = TerminationConfig {
344            unimproved_step_count_limit: Some(50),
345            unimproved_seconds_spent_limit: Some(10),
346            ..Default::default()
347        };
348
349        let term = TerminationBuilder::build::<TestSolution>(&config);
350        assert!(term.is_some());
351    }
352
353    #[test]
354    fn test_acceptor_builder_hill_climbing() {
355        let config = AcceptorConfig::HillClimbing;
356        let _acceptor: Box<dyn Acceptor<TestSolution>> = AcceptorBuilder::build(&config);
357    }
358
359    #[test]
360    fn test_acceptor_builder_tabu_search() {
361        let config = AcceptorConfig::TabuSearch(TabuSearchConfig {
362            entity_tabu_size: Some(10),
363            ..Default::default()
364        });
365        let _acceptor: Box<dyn Acceptor<TestSolution>> = AcceptorBuilder::build(&config);
366    }
367
368    #[test]
369    fn test_acceptor_builder_simulated_annealing() {
370        let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
371            starting_temperature: Some("1.5".to_string()),
372        });
373        let _acceptor: Box<dyn Acceptor<TestSolution>> = AcceptorBuilder::build(&config);
374    }
375
376    #[test]
377    fn test_acceptor_builder_late_acceptance() {
378        let config = AcceptorConfig::LateAcceptance(LateAcceptanceConfig {
379            late_acceptance_size: Some(500),
380        });
381        let _acceptor: Box<dyn Acceptor<TestSolution>> = AcceptorBuilder::build(&config);
382    }
383
384    #[test]
385    fn test_solver_builder() {
386        let config = TerminationConfig {
387            seconds_spent_limit: Some(30),
388            ..Default::default()
389        };
390
391        let builder = SolverBuilder::<TestSolution>::new().with_termination_from_config(&config);
392
393        let solver = builder.build();
394        assert!(!solver.is_solving());
395    }
396}