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