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 initialize_working_solution_as_best(&mut self) -> S::Score {
222        if self.start_time.is_none() {
223            self.start_solving();
224        }
225        let score = self.calculate_score();
226        let solution = self.score_director.clone_working_solution();
227        self.set_best_solution(solution, score);
228        score
229    }
230
231    pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
232        *self.score_director.working_solution_mut() = solution;
233        self.score_director.reset();
234        self.current_score = None;
235        self.calculate_score()
236    }
237
238    pub fn best_solution(&self) -> Option<&S> {
239        self.best_solution.as_ref()
240    }
241
242    pub fn best_score(&self) -> Option<&S::Score> {
243        self.best_score.as_ref()
244    }
245
246    pub fn current_score(&self) -> Option<&S::Score> {
247        self.current_score.as_ref()
248    }
249
250    pub fn terminal_reason(&self) -> SolverTerminalReason {
251        self.terminal_reason
252            .unwrap_or(SolverTerminalReason::Completed)
253    }
254
255    pub fn set_current_score(&mut self, score: S::Score) {
256        self.current_score = Some(score);
257    }
258
259    pub fn report_progress(&self) {
260        self.progress_callback.invoke(SolverProgressRef {
261            kind: SolverProgressKind::Progress,
262            status: self.progress_state(),
263            solution: None,
264            current_score: self.current_score.as_ref(),
265            best_score: self.best_score.as_ref(),
266            telemetry: self.stats.snapshot(),
267        });
268    }
269
270    pub fn report_best_solution(&self) {
271        self.progress_callback.invoke(SolverProgressRef {
272            kind: SolverProgressKind::BestSolution,
273            status: self.progress_state(),
274            solution: self.best_solution.as_ref(),
275            current_score: self.current_score.as_ref(),
276            best_score: self.best_score.as_ref(),
277            telemetry: self.stats.snapshot(),
278        });
279    }
280
281    pub fn update_best_solution(&mut self) {
282        let current_score = self.score_director.calculate_score();
283        self.current_score = Some(current_score);
284        let is_better = match &self.best_score {
285            None => true,
286            Some(best) => current_score > *best,
287        };
288
289        if is_better {
290            self.best_solution = Some(self.score_director.clone_working_solution());
291            self.best_score = Some(current_score);
292            self.last_best_elapsed = self.elapsed();
293            self.report_best_solution();
294        }
295    }
296
297    pub(crate) fn promote_current_solution_on_score_tie(&mut self) {
298        let Some(current_score) = self.current_score else {
299            return;
300        };
301        let Some(best_score) = self.best_score else {
302            return;
303        };
304
305        if current_score == best_score {
306            self.best_solution = Some(self.score_director.clone_working_solution());
307            self.report_best_solution();
308        }
309    }
310
311    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
312        if self.start_time.is_none() {
313            self.start_solving();
314        }
315        self.current_score = Some(score);
316        self.best_solution = Some(solution);
317        self.best_score = Some(score);
318        self.last_best_elapsed = self.elapsed();
319    }
320
321    pub fn rng(&mut self) -> &mut StdRng {
322        &mut self.rng
323    }
324
325    pub fn increment_step_count(&mut self) -> u64 {
326        self.total_step_count += 1;
327        self.stats.record_step();
328        self.total_step_count
329    }
330
331    pub fn total_step_count(&self) -> u64 {
332        self.total_step_count
333    }
334
335    pub fn take_best_solution(self) -> Option<S> {
336        self.best_solution
337    }
338
339    pub fn take_best_or_working_solution(self) -> S {
340        self.best_solution
341            .unwrap_or_else(|| self.score_director.clone_working_solution())
342    }
343
344    pub fn take_solution_and_stats(
345        self,
346    ) -> (
347        S,
348        Option<S::Score>,
349        S::Score,
350        SolverStats,
351        SolverTerminalReason,
352    ) {
353        let terminal_reason = self.terminal_reason();
354        let solution = self
355            .best_solution
356            .unwrap_or_else(|| self.score_director.clone_working_solution());
357        let best_score = self
358            .best_score
359            .or(self.current_score)
360            .expect("solver finished without a canonical score");
361        (
362            solution,
363            self.current_score,
364            best_score,
365            self.stats,
366            terminal_reason,
367        )
368    }
369
370    pub fn is_terminate_early(&self) -> bool {
371        self.terminate
372            .is_some_and(|flag| flag.load(Ordering::SeqCst))
373            || self
374                .runtime
375                .is_some_and(|runtime| runtime.is_cancel_requested())
376    }
377
378    pub(crate) fn pending_control(&self) -> PendingControl {
379        if self.is_terminate_early() {
380            return PendingControl::CancelRequested;
381        }
382        if self
383            .runtime
384            .is_some_and(|runtime| runtime.is_pause_requested())
385        {
386            return PendingControl::PauseRequested;
387        }
388        if self.time_limit_reached() {
389            return PendingControl::ConfigTerminationRequested;
390        }
391        PendingControl::Continue
392    }
393
394    pub fn set_time_limit(&mut self, limit: Duration) {
395        self.time_limit = Some(limit);
396    }
397
398    pub fn pause_if_requested(&mut self) {
399        self.settle_pause_if_requested();
400    }
401
402    pub fn pause_timers(&mut self) {
403        if self.paused_at.is_none() {
404            self.paused_at = Some(Instant::now());
405            self.stats.pause();
406        }
407    }
408
409    pub fn resume_timers(&mut self) {
410        if let Some(paused_at) = self.paused_at.take() {
411            let paused_for = paused_at.elapsed();
412            if let Some(start) = self.start_time {
413                self.start_time = Some(start + paused_for);
414            }
415            self.stats.resume();
416        }
417    }
418
419    pub fn should_terminate_construction(&mut self) -> bool {
420        self.settle_pause_if_requested();
421        if self.is_terminate_early() {
422            self.mark_cancelled();
423            return true;
424        }
425        if self.time_limit_reached() {
426            self.mark_terminated_by_config();
427            return true;
428        }
429        false
430    }
431
432    pub fn should_terminate(&mut self) -> bool {
433        self.settle_pause_if_requested();
434        if self.is_terminate_early() {
435            self.mark_cancelled();
436            return true;
437        }
438        if self.time_limit_reached() {
439            self.mark_terminated_by_config();
440            return true;
441        }
442        if let Some(limit) = self.inphase_step_count_limit {
443            if self.total_step_count >= limit {
444                self.mark_terminated_by_config();
445                return true;
446            }
447        }
448        if let Some(limit) = self.inphase_move_count_limit {
449            if self.stats.moves_evaluated >= limit {
450                self.mark_terminated_by_config();
451                return true;
452            }
453        }
454        if let Some(limit) = self.inphase_score_calc_count_limit {
455            if self.stats.score_calculations >= limit {
456                self.mark_terminated_by_config();
457                return true;
458            }
459        }
460        false
461    }
462
463    pub fn mark_cancelled(&mut self) {
464        self.terminal_reason
465            .get_or_insert(SolverTerminalReason::Cancelled);
466    }
467
468    pub fn mark_terminated_by_config(&mut self) {
469        self.terminal_reason
470            .get_or_insert(SolverTerminalReason::TerminatedByConfig);
471    }
472
473    pub fn stats(&self) -> &SolverStats {
474        &self.stats
475    }
476
477    pub fn stats_mut(&mut self) -> &mut SolverStats {
478        &mut self.stats
479    }
480
481    fn progress_state(&self) -> SolverLifecycleState {
482        self.runtime
483            .map(|runtime| {
484                if runtime.is_terminal() {
485                    SolverLifecycleState::Completed
486                } else {
487                    SolverLifecycleState::Solving
488                }
489            })
490            .unwrap_or(SolverLifecycleState::Solving)
491    }
492
493    fn settle_pause_if_requested(&mut self) {
494        if let Some(runtime) = self.runtime {
495            runtime.pause_if_requested(self);
496        }
497    }
498
499    fn time_limit_reached(&self) -> bool {
500        self.time_limit
501            .zip(self.elapsed())
502            .is_some_and(|(limit, elapsed)| elapsed >= limit)
503    }
504}