Skip to main content

solverforge_solver/scope/
solver.rs

1// Solver-level scope.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::{Duration, Instant};
5
6use rand::rngs::StdRng;
7use rand::SeedableRng;
8
9use solverforge_core::domain::PlanningSolution;
10use solverforge_scoring::Director;
11
12use crate::manager::SolverStatus;
13use crate::stats::SolverStats;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SolverProgressKind {
17    Progress,
18    BestSolution,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct SolverProgressRef<'a, S: PlanningSolution> {
23    pub kind: SolverProgressKind,
24    pub status: SolverStatus,
25    pub solution: Option<&'a S>,
26    pub current_score: Option<&'a S::Score>,
27    pub best_score: Option<&'a S::Score>,
28    pub telemetry: crate::stats::SolverTelemetry,
29}
30
31/// Sealed trait for invoking an optional progress callback.
32///
33/// Implemented for `()` (no-op) and for any `F: for<'a> Fn(SolverProgressRef<'a, S>) + Send + Sync`.
34pub trait ProgressCallback<S: PlanningSolution>: Send + Sync {
35    fn invoke(&self, progress: SolverProgressRef<'_, S>);
36}
37
38impl<S: PlanningSolution> ProgressCallback<S> for () {
39    fn invoke(&self, _progress: SolverProgressRef<'_, S>) {}
40}
41
42impl<S, F> ProgressCallback<S> for F
43where
44    S: PlanningSolution,
45    F: for<'a> Fn(SolverProgressRef<'a, S>) + Send + Sync,
46{
47    fn invoke(&self, progress: SolverProgressRef<'_, S>) {
48        self(progress);
49    }
50}
51
52/// Top-level scope for the entire solving process.
53///
54/// Holds the working solution, score director, and tracks the best solution found.
55///
56/// # Type Parameters
57/// * `'t` - Lifetime of the termination flag reference
58/// * `S` - The planning solution type
59/// * `D` - The score director type
60/// * `ProgressCb` - The progress callback type (default `()` means no callback)
61pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
62    // The score director managing the working solution.
63    score_director: D,
64    // The best solution found so far.
65    best_solution: Option<S>,
66    // The score of the current working solution.
67    current_score: Option<S::Score>,
68    // The score of the best solution.
69    best_score: Option<S::Score>,
70    // Random number generator for stochastic algorithms.
71    rng: StdRng,
72    // When solving started.
73    start_time: Option<Instant>,
74    // Total number of steps across all phases.
75    total_step_count: u64,
76    // Flag for early termination requests.
77    terminate: Option<&'t AtomicBool>,
78    // Solver statistics.
79    stats: SolverStats,
80    // Time limit for solving (checked by phases).
81    time_limit: Option<Duration>,
82    // Callback invoked when the solver should publish progress.
83    progress_callback: ProgressCb,
84    // Optional maximum total step count for in-phase termination (T1).
85    pub inphase_step_count_limit: Option<u64>,
86    // Optional maximum total move count for in-phase termination (T1).
87    pub inphase_move_count_limit: Option<u64>,
88    // Optional maximum total score calculation count for in-phase termination (T1).
89    pub inphase_score_calc_count_limit: Option<u64>,
90}
91
92impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
93    pub fn new(score_director: D) -> Self {
94        Self {
95            score_director,
96            best_solution: None,
97            current_score: None,
98            best_score: None,
99            rng: StdRng::from_rng(&mut rand::rng()),
100            start_time: None,
101            total_step_count: 0,
102            terminate: None,
103            stats: SolverStats::default(),
104            time_limit: None,
105            progress_callback: (),
106            inphase_step_count_limit: None,
107            inphase_move_count_limit: None,
108            inphase_score_calc_count_limit: None,
109        }
110    }
111}
112
113impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
114    SolverScope<'t, S, D, ProgressCb>
115{
116    pub fn new_with_callback(
117        score_director: D,
118        callback: ProgressCb,
119        terminate: Option<&'t std::sync::atomic::AtomicBool>,
120    ) -> Self {
121        Self {
122            score_director,
123            best_solution: None,
124            current_score: None,
125            best_score: None,
126            rng: StdRng::from_rng(&mut rand::rng()),
127            start_time: None,
128            total_step_count: 0,
129            terminate,
130            stats: SolverStats::default(),
131            time_limit: None,
132            progress_callback: callback,
133            inphase_step_count_limit: None,
134            inphase_move_count_limit: None,
135            inphase_score_calc_count_limit: None,
136        }
137    }
138}
139
140impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
141    SolverScope<'t, S, D, ProgressCb>
142{
143    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
144        self.terminate = terminate;
145        self
146    }
147
148    pub fn with_seed(mut self, seed: u64) -> Self {
149        self.rng = StdRng::seed_from_u64(seed);
150        self
151    }
152
153    /// Sets the progress callback, transitioning to a typed callback scope.
154    ///
155    /// The callback is invoked for exact progress updates and best-solution updates.
156    pub fn with_progress_callback<F: ProgressCallback<S>>(
157        self,
158        callback: F,
159    ) -> SolverScope<'t, S, D, F> {
160        SolverScope {
161            score_director: self.score_director,
162            best_solution: self.best_solution,
163            current_score: self.current_score,
164            best_score: self.best_score,
165            rng: self.rng,
166            start_time: self.start_time,
167            total_step_count: self.total_step_count,
168            terminate: self.terminate,
169            stats: self.stats,
170            time_limit: self.time_limit,
171            progress_callback: callback,
172            inphase_step_count_limit: self.inphase_step_count_limit,
173            inphase_move_count_limit: self.inphase_move_count_limit,
174            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
175        }
176    }
177
178    /// Marks the start of solving.
179    pub fn start_solving(&mut self) {
180        self.start_time = Some(Instant::now());
181        self.total_step_count = 0;
182        self.stats.start();
183    }
184
185    pub fn elapsed(&self) -> Option<std::time::Duration> {
186        self.start_time.map(|t| t.elapsed())
187    }
188
189    pub fn score_director(&self) -> &D {
190        &self.score_director
191    }
192
193    pub fn score_director_mut(&mut self) -> &mut D {
194        &mut self.score_director
195    }
196
197    pub fn working_solution(&self) -> &S {
198        self.score_director.working_solution()
199    }
200
201    pub fn working_solution_mut(&mut self) -> &mut S {
202        self.score_director.working_solution_mut()
203    }
204
205    /// Calculates and returns the current score.
206    ///
207    /// Also records the score calculation in solver statistics.
208    pub fn calculate_score(&mut self) -> S::Score {
209        self.stats.record_score_calculation();
210        let score = self.score_director.calculate_score();
211        self.current_score = Some(score);
212        score
213    }
214
215    pub fn best_solution(&self) -> Option<&S> {
216        self.best_solution.as_ref()
217    }
218
219    pub fn best_score(&self) -> Option<&S::Score> {
220        self.best_score.as_ref()
221    }
222
223    pub fn current_score(&self) -> Option<&S::Score> {
224        self.current_score.as_ref()
225    }
226
227    pub fn set_current_score(&mut self, score: S::Score) {
228        self.current_score = Some(score);
229    }
230
231    pub fn report_progress(&self) {
232        self.progress_callback.invoke(SolverProgressRef {
233            kind: SolverProgressKind::Progress,
234            status: SolverStatus::Solving,
235            solution: None,
236            current_score: self.current_score.as_ref(),
237            best_score: self.best_score.as_ref(),
238            telemetry: self.stats.snapshot(),
239        });
240    }
241
242    pub fn report_best_solution(&self) {
243        self.progress_callback.invoke(SolverProgressRef {
244            kind: SolverProgressKind::BestSolution,
245            status: SolverStatus::Solving,
246            solution: self.best_solution.as_ref(),
247            current_score: self.current_score.as_ref(),
248            best_score: self.best_score.as_ref(),
249            telemetry: self.stats.snapshot(),
250        });
251    }
252
253    /// Updates the best solution if the current solution is better.
254    pub fn update_best_solution(&mut self) {
255        let current_score = self.score_director.calculate_score();
256        self.current_score = Some(current_score);
257        let is_better = match &self.best_score {
258            None => true,
259            Some(best) => current_score > *best,
260        };
261
262        if is_better {
263            self.best_solution = Some(self.score_director.clone_working_solution());
264            self.best_score = Some(current_score);
265
266            self.report_best_solution();
267        }
268    }
269
270    /// Forces an update of the best solution regardless of score comparison.
271    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
272        self.current_score = Some(score);
273        self.best_solution = Some(solution);
274        self.best_score = Some(score);
275    }
276
277    pub fn rng(&mut self) -> &mut StdRng {
278        &mut self.rng
279    }
280
281    /// Increments and returns the total step count.
282    pub fn increment_step_count(&mut self) -> u64 {
283        self.total_step_count += 1;
284        self.stats.record_step();
285        self.total_step_count
286    }
287
288    pub fn total_step_count(&self) -> u64 {
289        self.total_step_count
290    }
291
292    /// Extracts the best solution, consuming this scope.
293    pub fn take_best_solution(self) -> Option<S> {
294        self.best_solution
295    }
296
297    pub fn take_best_or_working_solution(self) -> S {
298        self.best_solution
299            .unwrap_or_else(|| self.score_director.clone_working_solution())
300    }
301
302    /// Extracts both the solution and stats, consuming this scope.
303    ///
304    /// Returns the best solution (or working solution if none) along with
305    /// the accumulated solver statistics.
306    pub fn take_solution_and_stats(self) -> (S, S::Score, SolverStats) {
307        let solution = self
308            .best_solution
309            .unwrap_or_else(|| self.score_director.clone_working_solution());
310        let score = self
311            .best_score
312            .or(self.current_score)
313            .expect("solver finished without a canonical score");
314        (solution, score, self.stats)
315    }
316
317    pub fn is_terminate_early(&self) -> bool {
318        self.terminate
319            .is_some_and(|flag| flag.load(Ordering::SeqCst))
320    }
321
322    pub fn set_time_limit(&mut self, limit: Duration) {
323        self.time_limit = Some(limit);
324    }
325
326    pub fn should_terminate_construction(&self) -> bool {
327        // Check external termination flag
328        if self.is_terminate_early() {
329            return true;
330        }
331        // Check time limit only
332        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
333            if start.elapsed() >= limit {
334                return true;
335            }
336        }
337        false
338    }
339
340    pub fn should_terminate(&self) -> bool {
341        // Check external termination flag
342        if self.is_terminate_early() {
343            return true;
344        }
345        // Check time limit
346        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
347            if start.elapsed() >= limit {
348                return true;
349            }
350        }
351        // Check in-phase step count limit (T1: StepCountTermination fires inside phase loop)
352        if let Some(limit) = self.inphase_step_count_limit {
353            if self.total_step_count >= limit {
354                return true;
355            }
356        }
357        // Check in-phase move count limit (T1: MoveCountTermination fires inside phase loop)
358        if let Some(limit) = self.inphase_move_count_limit {
359            if self.stats.moves_evaluated >= limit {
360                return true;
361            }
362        }
363        // Check in-phase score calculation count limit
364        if let Some(limit) = self.inphase_score_calc_count_limit {
365            if self.stats.score_calculations >= limit {
366                return true;
367            }
368        }
369        false
370    }
371
372    pub fn stats(&self) -> &SolverStats {
373        &self.stats
374    }
375
376    pub fn stats_mut(&mut self) -> &mut SolverStats {
377        &mut self.stats
378    }
379}