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