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