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