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                    config,
290                    time_limit,
291                    progress_callback,
292                    ..
293                } = self;
294
295                let mut solver_scope = SolverScope::new_with_callback(
296                    score_director,
297                    progress_callback,
298                    terminate,
299                    runtime,
300                );
301                if let Some(seed) = config.as_ref().and_then(|cfg| cfg.random_seed) {
302                    solver_scope = solver_scope.with_seed(seed);
303                }
304                if let Some(limit) = time_limit {
305                    solver_scope.set_time_limit(limit);
306                }
307                solver_scope.start_solving();
308                let initial_score = solver_scope.calculate_score();
309                let initial_solution = solver_scope.score_director().clone_working_solution();
310                solver_scope.set_best_solution(initial_solution, initial_score);
311                solver_scope.report_best_solution();
312                solver_scope.pause_if_requested();
313
314                // Install in-phase termination limits so phases can check them
315                // inside their step loops (T1: StepCountTermination, MoveCountTermination, etc.)
316                termination.install_inphase_limits(&mut solver_scope);
317
318                // Execute phases with termination checking
319                $(
320                    solver_scope.pause_if_requested();
321                    if !check_termination(&termination, &mut solver_scope) {
322                        tracing::debug!(
323                            "Starting phase {} ({})",
324                            $idx,
325                            phases.$idx.phase_type_name()
326                        );
327                        phases.$idx.solve(&mut solver_scope);
328                        solver_scope.pause_if_requested();
329                        tracing::debug!(
330                            "Finished phase {} ({}) with score {:?}",
331                            $idx,
332                            phases.$idx.phase_type_name(),
333                            solver_scope.best_score()
334                        );
335                    }
336                )+
337
338                // Extract solution and stats before consuming scope
339                let (solution, current_score, best_score, stats, terminal_reason) =
340                    solver_scope.take_solution_and_stats();
341                SolveResult {
342                    solution,
343                    current_score,
344                    best_score,
345                    terminal_reason,
346                    stats,
347                }
348            }
349        }
350    };
351}
352
353fn check_termination<S, D, ProgressCb, T>(
354    termination: &T,
355    solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
356) -> bool
357where
358    S: PlanningSolution,
359    D: Director<S>,
360    ProgressCb: ProgressCallback<S>,
361    T: MaybeTermination<S, D, ProgressCb>,
362{
363    if solver_scope.is_terminate_early() {
364        solver_scope.mark_cancelled();
365        return true;
366    }
367    if termination.should_terminate(solver_scope) {
368        solver_scope.mark_terminated_by_config();
369        true
370    } else {
371        false
372    }
373}
374
375macro_rules! impl_solver_with_director {
376    ($($idx:tt: $P:ident),+) => {
377        impl<'t, S, T, ProgressCb, $($P),+> Solver<'t, ($($P,)+), T, S, (), ProgressCb>
378        where
379            S: PlanningSolution,
380            T: Send,
381            ProgressCb: Send + Sync,
382        {
383            /// Solves using a provided score director.
384            pub fn solve_with_director<D>(self, director: D) -> SolveResult<S>
385            where
386                D: Director<S>,
387                ProgressCb: ProgressCallback<S>,
388                T: MaybeTermination<S, D, ProgressCb>,
389                $($P: Phase<S, D, ProgressCb>,)+
390            {
391                let solver: Solver<'t, ($($P,)+), T, S, D, ProgressCb> = Solver {
392                    phases: self.phases,
393                    termination: self.termination,
394                    terminate: self.terminate,
395                    runtime: self.runtime,
396                    config: self.config,
397                    time_limit: self.time_limit,
398                    progress_callback: self.progress_callback,
399                    _phantom: PhantomData,
400                };
401                solver.solve(director)
402            }
403        }
404    };
405}
406
407impl_solver_with_director!(0: P0);
408impl_solver_with_director!(0: P0, 1: P1);
409impl_solver_with_director!(0: P0, 1: P1, 2: P2);
410impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3);
411impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
412impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
413impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
414impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);
415
416impl_solver!(0: P0);
417impl_solver!(0: P0, 1: P1);
418impl_solver!(0: P0, 1: P1, 2: P2);
419impl_solver!(0: P0, 1: P1, 2: P2, 3: P3);
420impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
421impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
422impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
423impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);