Skip to main content

solverforge_solver/
solver.rs

1//! Solver implementation.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::sync::atomic::AtomicBool;
6use std::time::Duration;
7
8use solverforge_config::SolverConfig;
9use solverforge_core::domain::PlanningSolution;
10use solverforge_scoring::ScoreDirector;
11
12use crate::phase::Phase;
13use crate::scope::SolverScope;
14use crate::stats::SolverStats;
15use crate::termination::Termination;
16
17/// Result of a solve operation containing solution and telemetry.
18///
19/// This is the canonical return type for `Solver::solve()`. It provides
20/// both the optimized solution and comprehensive statistics about the
21/// solving process.
22#[derive(Debug)]
23pub struct SolveResult<S: PlanningSolution> {
24    /// The best solution found during solving.
25    pub solution: S,
26    /// Solver statistics including steps, moves evaluated, and acceptance rates.
27    pub stats: SolverStats,
28}
29
30impl<S: PlanningSolution> SolveResult<S> {
31    /// Returns a reference to the solution.
32    pub fn solution(&self) -> &S {
33        &self.solution
34    }
35
36    /// Returns the solution, consuming this result.
37    pub fn into_solution(self) -> S {
38        self.solution
39    }
40
41    /// Returns a reference to the solver statistics.
42    pub fn stats(&self) -> &SolverStats {
43        &self.stats
44    }
45
46    /// Returns the total number of steps taken across all phases.
47    pub fn step_count(&self) -> u64 {
48        self.stats.step_count
49    }
50
51    /// Returns the total number of moves evaluated across all phases.
52    pub fn moves_evaluated(&self) -> u64 {
53        self.stats.moves_evaluated
54    }
55
56    /// Returns the total number of moves accepted across all phases.
57    pub fn moves_accepted(&self) -> u64 {
58        self.stats.moves_accepted
59    }
60}
61
62/// The main solver that optimizes planning solutions.
63///
64/// Uses macro-generated tuple implementations for phases, preserving
65/// concrete types through the entire pipeline (zero-erasure architecture).
66///
67/// # Type Parameters
68/// * `'t` - Lifetime of the termination flag reference
69/// * `P` - Tuple of phases to execute
70/// * `T` - Termination condition (use `Option<ConcreteTermination>`)
71/// * `S` - Solution type
72/// * `D` - Score director type
73#[allow(clippy::type_complexity)]
74pub struct Solver<'t, P, T, S, D> {
75    phases: P,
76    termination: T,
77    terminate: Option<&'t AtomicBool>,
78    config: Option<SolverConfig>,
79    time_limit: Option<Duration>,
80    /// Callback invoked when a better solution is found during solving.
81    best_solution_callback: Option<Box<dyn Fn(&S) + Send + Sync + 't>>,
82    _phantom: PhantomData<fn(S, D)>,
83}
84
85impl<P: Debug, T: Debug, S, D> Debug for Solver<'_, P, T, S, D> {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.debug_struct("Solver")
88            .field("phases", &self.phases)
89            .field("termination", &self.termination)
90            .finish()
91    }
92}
93
94impl<P, S, D> Solver<'static, P, NoTermination, S, D>
95where
96    S: PlanningSolution,
97{
98    /// Creates a new solver with the given phases tuple and no termination.
99    pub fn new(phases: P) -> Self {
100        Solver {
101            phases,
102            termination: NoTermination,
103            terminate: None,
104            config: None,
105            time_limit: None,
106            best_solution_callback: None,
107            _phantom: PhantomData,
108        }
109    }
110
111    /// Sets the termination condition.
112    pub fn with_termination<T>(self, termination: T) -> Solver<'static, P, Option<T>, S, D> {
113        Solver {
114            phases: self.phases,
115            termination: Some(termination),
116            terminate: self.terminate,
117            config: self.config,
118            time_limit: self.time_limit,
119            best_solution_callback: self.best_solution_callback,
120            _phantom: PhantomData,
121        }
122    }
123}
124
125impl<'t, P, T, S, D> Solver<'t, P, T, S, D>
126where
127    S: PlanningSolution,
128{
129    /// Sets the external termination flag.
130    ///
131    /// The solver will check this flag periodically and terminate early if set.
132    pub fn with_terminate(self, terminate: &'t AtomicBool) -> Solver<'t, P, T, S, D> {
133        Solver {
134            phases: self.phases,
135            termination: self.termination,
136            terminate: Some(terminate),
137            config: self.config,
138            time_limit: self.time_limit,
139            best_solution_callback: self.best_solution_callback,
140            _phantom: PhantomData,
141        }
142    }
143
144    /// Sets the time limit for solving.
145    pub fn with_time_limit(mut self, limit: Duration) -> Self {
146        self.time_limit = Some(limit);
147        self
148    }
149
150    /// Sets configuration.
151    pub fn with_config(mut self, config: SolverConfig) -> Self {
152        self.config = Some(config);
153        self
154    }
155
156    /// Sets a callback to be invoked when a better solution is found during solving.
157    pub fn with_best_solution_callback(
158        mut self,
159        callback: Box<dyn Fn(&S) + Send + Sync + 't>,
160    ) -> Self {
161        self.best_solution_callback = Some(callback);
162        self
163    }
164
165    /// Returns the configuration if set.
166    pub fn config(&self) -> Option<&SolverConfig> {
167        self.config.as_ref()
168    }
169}
170
171/// Marker type indicating no termination.
172#[derive(Debug, Clone, Copy, Default)]
173pub struct NoTermination;
174
175/// Marker trait for termination types that can be used in Solver.
176pub trait MaybeTermination<S: PlanningSolution, D: ScoreDirector<S>>: Send {
177    /// Checks if the solver should terminate.
178    fn should_terminate(&self, solver_scope: &SolverScope<'_, S, D>) -> bool;
179
180    /// Installs in-phase termination limits on the solver scope.
181    ///
182    /// This allows `Termination` conditions (step count, move count, etc.) to fire
183    /// inside the phase step loop, not only between phases (T1 fix).
184    ///
185    /// The default implementation is a no-op. Override for terminations that
186    /// express a concrete limit via a scope field.
187    fn install_inphase_limits(&self, _solver_scope: &mut SolverScope<'_, S, D>) {}
188}
189
190impl<S: PlanningSolution, D: ScoreDirector<S>, T: Termination<S, D>> MaybeTermination<S, D>
191    for Option<T>
192{
193    fn should_terminate(&self, solver_scope: &SolverScope<'_, S, D>) -> bool {
194        match self {
195            Some(t) => t.is_terminated(solver_scope),
196            None => false,
197        }
198    }
199
200    fn install_inphase_limits(&self, solver_scope: &mut SolverScope<'_, S, D>) {
201        if let Some(t) = self {
202            t.install_inphase_limits(solver_scope);
203        }
204    }
205}
206
207impl<S: PlanningSolution, D: ScoreDirector<S>> MaybeTermination<S, D> for NoTermination {
208    fn should_terminate(&self, _solver_scope: &SolverScope<'_, S, D>) -> bool {
209        false
210    }
211
212    // install_inphase_limits: no-op (default)
213}
214
215impl<S: PlanningSolution, D: ScoreDirector<S>> Termination<S, D> for NoTermination {
216    fn is_terminated(&self, _solver_scope: &SolverScope<'_, S, D>) -> bool {
217        false
218    }
219}
220
221macro_rules! impl_solver {
222    ($($idx:tt: $P:ident),+) => {
223        impl<'t, S, D, T, $($P),+> Solver<'t, ($($P,)+), T, S, D>
224        where
225            S: PlanningSolution,
226            D: ScoreDirector<S>,
227            T: MaybeTermination<S, D>,
228            $($P: Phase<S, D>,)+
229        {
230            /// Solves using the provided score director.
231            ///
232            /// Returns a `SolveResult` containing the best solution found
233            /// and comprehensive solver statistics.
234            pub fn solve(&mut self, score_director: D) -> SolveResult<S> {
235                let mut solver_scope = SolverScope::with_terminate(score_director, self.terminate);
236                if let Some(limit) = self.time_limit {
237                    solver_scope.set_time_limit(limit);
238                }
239                if let Some(callback) = self.best_solution_callback.take() {
240                    solver_scope = solver_scope.with_best_solution_callback(callback);
241                }
242                solver_scope.start_solving();
243
244                // Install in-phase termination limits so phases can check them
245                // inside their step loops (T1: StepCountTermination, MoveCountTermination, etc.)
246                self.termination.install_inphase_limits(&mut solver_scope);
247
248                // Execute phases with termination checking
249                $(
250                    if !self.check_termination(&solver_scope) {
251                        tracing::debug!(
252                            "Starting phase {} ({})",
253                            $idx,
254                            self.phases.$idx.phase_type_name()
255                        );
256                        self.phases.$idx.solve(&mut solver_scope);
257                        tracing::debug!(
258                            "Finished phase {} ({}) with score {:?}",
259                            $idx,
260                            self.phases.$idx.phase_type_name(),
261                            solver_scope.best_score()
262                        );
263                    }
264                )+
265
266                // Extract solution and stats before consuming scope
267                let (solution, stats) = solver_scope.take_solution_and_stats();
268                SolveResult { solution, stats }
269            }
270
271            fn check_termination(&self, solver_scope: &SolverScope<'_, S, D>) -> bool {
272                // Check external termination flag first
273                if solver_scope.is_terminate_early() {
274                    return true;
275                }
276                // Then check configured termination conditions
277                self.termination.should_terminate(solver_scope)
278            }
279
280
281        }
282    };
283}
284
285macro_rules! impl_solver_with_director {
286    ($($idx:tt: $P:ident),+) => {
287        impl<'t, S, T, $($P),+> Solver<'t, ($($P,)+), T, S, ()>
288        where
289            S: PlanningSolution,
290            T: Send,
291        {
292            /// Solves using a provided score director.
293            pub fn solve_with_director<D>(self, director: D) -> SolveResult<S>
294            where
295                D: ScoreDirector<S>,
296                T: MaybeTermination<S, D>,
297                $($P: Phase<S, D>,)+
298            {
299                let mut solver: Solver<'t, ($($P,)+), T, S, D> = Solver {
300                    phases: self.phases,
301                    termination: self.termination,
302                    terminate: self.terminate,
303                    config: self.config,
304                    time_limit: self.time_limit,
305                    best_solution_callback: self.best_solution_callback,
306                    _phantom: PhantomData,
307                };
308                solver.solve(director)
309            }
310        }
311    };
312}
313
314impl_solver_with_director!(0: P0);
315impl_solver_with_director!(0: P0, 1: P1);
316impl_solver_with_director!(0: P0, 1: P1, 2: P2);
317impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3);
318impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
319impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
320impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
321impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);
322
323impl_solver!(0: P0);
324impl_solver!(0: P0, 1: P1);
325impl_solver!(0: P0, 1: P1, 2: P2);
326impl_solver!(0: P0, 1: P1, 2: P2, 3: P3);
327impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
328impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
329impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
330impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);