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::{SolverLifecycleState, SolverRuntime, SolverTerminalReason};
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: SolverLifecycleState,
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
31pub trait ProgressCallback<S: PlanningSolution>: Send + Sync {
32    fn invoke(&self, progress: SolverProgressRef<'_, S>);
33}
34
35impl<S: PlanningSolution> ProgressCallback<S> for () {
36    fn invoke(&self, _progress: SolverProgressRef<'_, S>) {}
37}
38
39impl<S, F> ProgressCallback<S> for F
40where
41    S: PlanningSolution,
42    F: for<'a> Fn(SolverProgressRef<'a, S>) + Send + Sync,
43{
44    fn invoke(&self, progress: SolverProgressRef<'_, S>) {
45        self(progress);
46    }
47}
48
49pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
50    score_director: D,
51    best_solution: Option<S>,
52    current_score: Option<S::Score>,
53    best_score: Option<S::Score>,
54    rng: StdRng,
55    start_time: Option<Instant>,
56    paused_at: Option<Instant>,
57    total_step_count: u64,
58    terminate: Option<&'t AtomicBool>,
59    runtime: Option<SolverRuntime<S>>,
60    stats: SolverStats,
61    time_limit: Option<Duration>,
62    progress_callback: ProgressCb,
63    terminal_reason: Option<SolverTerminalReason>,
64    last_best_elapsed: Option<Duration>,
65    pub inphase_step_count_limit: Option<u64>,
66    pub inphase_move_count_limit: Option<u64>,
67    pub inphase_score_calc_count_limit: Option<u64>,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) enum PendingControl {
72    Continue,
73    PauseRequested,
74    CancelRequested,
75    ConfigTerminationRequested,
76}
77
78impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
79    pub fn new(score_director: D) -> Self {
80        Self {
81            score_director,
82            best_solution: None,
83            current_score: None,
84            best_score: None,
85            rng: StdRng::from_rng(&mut rand::rng()),
86            start_time: None,
87            paused_at: None,
88            total_step_count: 0,
89            terminate: None,
90            runtime: None,
91            stats: SolverStats::default(),
92            time_limit: None,
93            progress_callback: (),
94            terminal_reason: None,
95            last_best_elapsed: None,
96            inphase_step_count_limit: None,
97            inphase_move_count_limit: None,
98            inphase_score_calc_count_limit: None,
99        }
100    }
101}
102
103impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
104    SolverScope<'t, S, D, ProgressCb>
105{
106    pub fn new_with_callback(
107        score_director: D,
108        callback: ProgressCb,
109        terminate: Option<&'t AtomicBool>,
110        runtime: Option<SolverRuntime<S>>,
111    ) -> Self {
112        Self {
113            score_director,
114            best_solution: None,
115            current_score: None,
116            best_score: None,
117            rng: StdRng::from_rng(&mut rand::rng()),
118            start_time: None,
119            paused_at: None,
120            total_step_count: 0,
121            terminate,
122            runtime,
123            stats: SolverStats::default(),
124            time_limit: None,
125            progress_callback: callback,
126            terminal_reason: None,
127            last_best_elapsed: None,
128            inphase_step_count_limit: None,
129            inphase_move_count_limit: None,
130            inphase_score_calc_count_limit: None,
131        }
132    }
133
134    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
135        self.terminate = terminate;
136        self
137    }
138
139    pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
140        self.runtime = runtime;
141        self
142    }
143
144    pub fn with_seed(mut self, seed: u64) -> Self {
145        self.rng = StdRng::seed_from_u64(seed);
146        self
147    }
148
149    pub fn with_progress_callback<F: ProgressCallback<S>>(
150        self,
151        callback: F,
152    ) -> SolverScope<'t, S, D, F> {
153        SolverScope {
154            score_director: self.score_director,
155            best_solution: self.best_solution,
156            current_score: self.current_score,
157            best_score: self.best_score,
158            rng: self.rng,
159            start_time: self.start_time,
160            paused_at: self.paused_at,
161            total_step_count: self.total_step_count,
162            terminate: self.terminate,
163            runtime: self.runtime,
164            stats: self.stats,
165            time_limit: self.time_limit,
166            progress_callback: callback,
167            terminal_reason: self.terminal_reason,
168            last_best_elapsed: self.last_best_elapsed,
169            inphase_step_count_limit: self.inphase_step_count_limit,
170            inphase_move_count_limit: self.inphase_move_count_limit,
171            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
172        }
173    }
174
175    pub fn start_solving(&mut self) {
176        self.start_time = Some(Instant::now());
177        self.paused_at = None;
178        self.total_step_count = 0;
179        self.terminal_reason = None;
180        self.last_best_elapsed = None;
181        self.stats.start();
182    }
183
184    pub fn elapsed(&self) -> Option<Duration> {
185        match (self.start_time, self.paused_at) {
186            (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
187            (Some(start), None) => Some(start.elapsed()),
188            _ => None,
189        }
190    }
191
192    pub fn time_since_last_improvement(&self) -> Option<Duration> {
193        let elapsed = self.elapsed()?;
194        let last_best_elapsed = self.last_best_elapsed?;
195        Some(elapsed.saturating_sub(last_best_elapsed))
196    }
197
198    pub fn score_director(&self) -> &D {
199        &self.score_director
200    }
201
202    pub fn score_director_mut(&mut self) -> &mut D {
203        &mut self.score_director
204    }
205
206    pub fn working_solution(&self) -> &S {
207        self.score_director.working_solution()
208    }
209
210    pub fn working_solution_mut(&mut self) -> &mut S {
211        self.score_director.working_solution_mut()
212    }
213
214    pub fn calculate_score(&mut self) -> S::Score {
215        self.stats.record_score_calculation();
216        let score = self.score_director.calculate_score();
217        self.current_score = Some(score);
218        score
219    }
220
221    pub fn best_solution(&self) -> Option<&S> {
222        self.best_solution.as_ref()
223    }
224
225    pub fn best_score(&self) -> Option<&S::Score> {
226        self.best_score.as_ref()
227    }
228
229    pub fn current_score(&self) -> Option<&S::Score> {
230        self.current_score.as_ref()
231    }
232
233    pub fn terminal_reason(&self) -> SolverTerminalReason {
234        self.terminal_reason
235            .unwrap_or(SolverTerminalReason::Completed)
236    }
237
238    pub fn set_current_score(&mut self, score: S::Score) {
239        self.current_score = Some(score);
240    }
241
242    pub fn report_progress(&self) {
243        self.progress_callback.invoke(SolverProgressRef {
244            kind: SolverProgressKind::Progress,
245            status: self.progress_state(),
246            solution: None,
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    pub fn report_best_solution(&self) {
254        self.progress_callback.invoke(SolverProgressRef {
255            kind: SolverProgressKind::BestSolution,
256            status: self.progress_state(),
257            solution: self.best_solution.as_ref(),
258            current_score: self.current_score.as_ref(),
259            best_score: self.best_score.as_ref(),
260            telemetry: self.stats.snapshot(),
261        });
262    }
263
264    pub fn update_best_solution(&mut self) {
265        let current_score = self.score_director.calculate_score();
266        self.current_score = Some(current_score);
267        let is_better = match &self.best_score {
268            None => true,
269            Some(best) => current_score > *best,
270        };
271
272        if is_better {
273            self.best_solution = Some(self.score_director.clone_working_solution());
274            self.best_score = Some(current_score);
275            self.last_best_elapsed = self.elapsed();
276            self.report_best_solution();
277        }
278    }
279
280    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
281        if self.start_time.is_none() {
282            self.start_solving();
283        }
284        self.current_score = Some(score);
285        self.best_solution = Some(solution);
286        self.best_score = Some(score);
287        self.last_best_elapsed = self.elapsed();
288    }
289
290    pub fn rng(&mut self) -> &mut StdRng {
291        &mut self.rng
292    }
293
294    pub fn increment_step_count(&mut self) -> u64 {
295        self.total_step_count += 1;
296        self.stats.record_step();
297        self.total_step_count
298    }
299
300    pub fn total_step_count(&self) -> u64 {
301        self.total_step_count
302    }
303
304    pub fn take_best_solution(self) -> Option<S> {
305        self.best_solution
306    }
307
308    pub fn take_best_or_working_solution(self) -> S {
309        self.best_solution
310            .unwrap_or_else(|| self.score_director.clone_working_solution())
311    }
312
313    pub fn take_solution_and_stats(
314        self,
315    ) -> (
316        S,
317        Option<S::Score>,
318        S::Score,
319        SolverStats,
320        SolverTerminalReason,
321    ) {
322        let terminal_reason = self.terminal_reason();
323        let solution = self
324            .best_solution
325            .unwrap_or_else(|| self.score_director.clone_working_solution());
326        let best_score = self
327            .best_score
328            .or(self.current_score)
329            .expect("solver finished without a canonical score");
330        (
331            solution,
332            self.current_score,
333            best_score,
334            self.stats,
335            terminal_reason,
336        )
337    }
338
339    pub fn is_terminate_early(&self) -> bool {
340        self.terminate
341            .is_some_and(|flag| flag.load(Ordering::SeqCst))
342            || self
343                .runtime
344                .is_some_and(|runtime| runtime.is_cancel_requested())
345    }
346
347    pub(crate) fn pending_control(&self) -> PendingControl {
348        if self.is_terminate_early() {
349            return PendingControl::CancelRequested;
350        }
351        if self
352            .runtime
353            .is_some_and(|runtime| runtime.is_pause_requested())
354        {
355            return PendingControl::PauseRequested;
356        }
357        if self.time_limit_reached() {
358            return PendingControl::ConfigTerminationRequested;
359        }
360        PendingControl::Continue
361    }
362
363    pub fn set_time_limit(&mut self, limit: Duration) {
364        self.time_limit = Some(limit);
365    }
366
367    pub fn pause_if_requested(&mut self) {
368        self.settle_pause_if_requested();
369    }
370
371    pub fn pause_timers(&mut self) {
372        if self.paused_at.is_none() {
373            self.paused_at = Some(Instant::now());
374            self.stats.pause();
375        }
376    }
377
378    pub fn resume_timers(&mut self) {
379        if let Some(paused_at) = self.paused_at.take() {
380            let paused_for = paused_at.elapsed();
381            if let Some(start) = self.start_time {
382                self.start_time = Some(start + paused_for);
383            }
384            self.stats.resume();
385        }
386    }
387
388    pub fn should_terminate_construction(&mut self) -> bool {
389        self.settle_pause_if_requested();
390        if self.is_terminate_early() {
391            self.mark_cancelled();
392            return true;
393        }
394        if self.time_limit_reached() {
395            self.mark_terminated_by_config();
396            return true;
397        }
398        false
399    }
400
401    pub fn should_terminate(&mut self) -> bool {
402        self.settle_pause_if_requested();
403        if self.is_terminate_early() {
404            self.mark_cancelled();
405            return true;
406        }
407        if self.time_limit_reached() {
408            self.mark_terminated_by_config();
409            return true;
410        }
411        if let Some(limit) = self.inphase_step_count_limit {
412            if self.total_step_count >= limit {
413                self.mark_terminated_by_config();
414                return true;
415            }
416        }
417        if let Some(limit) = self.inphase_move_count_limit {
418            if self.stats.moves_evaluated >= limit {
419                self.mark_terminated_by_config();
420                return true;
421            }
422        }
423        if let Some(limit) = self.inphase_score_calc_count_limit {
424            if self.stats.score_calculations >= limit {
425                self.mark_terminated_by_config();
426                return true;
427            }
428        }
429        false
430    }
431
432    pub fn mark_cancelled(&mut self) {
433        self.terminal_reason
434            .get_or_insert(SolverTerminalReason::Cancelled);
435    }
436
437    pub fn mark_terminated_by_config(&mut self) {
438        self.terminal_reason
439            .get_or_insert(SolverTerminalReason::TerminatedByConfig);
440    }
441
442    pub fn stats(&self) -> &SolverStats {
443        &self.stats
444    }
445
446    pub fn stats_mut(&mut self) -> &mut SolverStats {
447        &mut self.stats
448    }
449
450    fn progress_state(&self) -> SolverLifecycleState {
451        self.runtime
452            .map(|runtime| {
453                if runtime.is_terminal() {
454                    SolverLifecycleState::Completed
455                } else {
456                    SolverLifecycleState::Solving
457                }
458            })
459            .unwrap_or(SolverLifecycleState::Solving)
460    }
461
462    fn settle_pause_if_requested(&mut self) {
463        if let Some(runtime) = self.runtime {
464            runtime.pause_if_requested(self);
465        }
466    }
467
468    fn time_limit_reached(&self) -> bool {
469        self.time_limit
470            .zip(self.elapsed())
471            .is_some_and(|(limit, elapsed)| elapsed >= limit)
472    }
473}