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, RecordingDirector};
11
12use crate::heuristic::r#move::Move;
13use crate::manager::{SolverLifecycleState, SolverRuntime, SolverTerminalReason};
14use crate::phase::construction::{
15    ConstructionFrontier, ConstructionListElementId, ConstructionSlotId,
16};
17use crate::stats::SolverStats;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SolverProgressKind {
21    Progress,
22    BestSolution,
23}
24
25#[derive(Debug, Clone, Copy)]
26pub struct SolverProgressRef<'a, S: PlanningSolution> {
27    pub kind: SolverProgressKind,
28    pub status: SolverLifecycleState,
29    pub solution: Option<&'a S>,
30    pub current_score: Option<&'a S::Score>,
31    pub best_score: Option<&'a S::Score>,
32    pub telemetry: crate::stats::SolverTelemetry,
33}
34
35pub trait ProgressCallback<S: PlanningSolution>: Send + Sync {
36    fn invoke(&self, progress: SolverProgressRef<'_, S>);
37}
38
39impl<S: PlanningSolution> ProgressCallback<S> for () {
40    fn invoke(&self, _progress: SolverProgressRef<'_, S>) {}
41}
42
43impl<S, F> ProgressCallback<S> for F
44where
45    S: PlanningSolution,
46    F: for<'a> Fn(SolverProgressRef<'a, S>) + Send + Sync,
47{
48    fn invoke(&self, progress: SolverProgressRef<'_, S>) {
49        self(progress);
50    }
51}
52
53pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
54    score_director: D,
55    best_solution: Option<S>,
56    current_score: Option<S::Score>,
57    best_score: Option<S::Score>,
58    rng: StdRng,
59    start_time: Option<Instant>,
60    paused_at: Option<Instant>,
61    total_step_count: u64,
62    terminate: Option<&'t AtomicBool>,
63    runtime: Option<SolverRuntime<S>>,
64    stats: SolverStats,
65    time_limit: Option<Duration>,
66    progress_callback: ProgressCb,
67    terminal_reason: Option<SolverTerminalReason>,
68    last_best_elapsed: Option<Duration>,
69    best_solution_revision: Option<u64>,
70    solution_revision: u64,
71    construction_frontier: ConstructionFrontier,
72    pub inphase_step_count_limit: Option<u64>,
73    pub inphase_move_count_limit: Option<u64>,
74    pub inphase_score_calc_count_limit: Option<u64>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) enum PendingControl {
79    Continue,
80    PauseRequested,
81    CancelRequested,
82    ConfigTerminationRequested,
83}
84
85impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
86    pub fn new(score_director: D) -> Self {
87        let construction_frontier = ConstructionFrontier::new();
88        Self {
89            score_director,
90            best_solution: None,
91            current_score: None,
92            best_score: None,
93            rng: StdRng::from_rng(&mut rand::rng()),
94            start_time: None,
95            paused_at: None,
96            total_step_count: 0,
97            terminate: None,
98            runtime: None,
99            stats: SolverStats::default(),
100            time_limit: None,
101            progress_callback: (),
102            terminal_reason: None,
103            last_best_elapsed: None,
104            best_solution_revision: None,
105            solution_revision: 1,
106            construction_frontier,
107            inphase_step_count_limit: None,
108            inphase_move_count_limit: None,
109            inphase_score_calc_count_limit: None,
110        }
111    }
112}
113
114impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
115    SolverScope<'t, S, D, ProgressCb>
116{
117    pub fn new_with_callback(
118        score_director: D,
119        callback: ProgressCb,
120        terminate: Option<&'t AtomicBool>,
121        runtime: Option<SolverRuntime<S>>,
122    ) -> Self {
123        let construction_frontier = ConstructionFrontier::new();
124        Self {
125            score_director,
126            best_solution: None,
127            current_score: None,
128            best_score: None,
129            rng: StdRng::from_rng(&mut rand::rng()),
130            start_time: None,
131            paused_at: None,
132            total_step_count: 0,
133            terminate,
134            runtime,
135            stats: SolverStats::default(),
136            time_limit: None,
137            progress_callback: callback,
138            terminal_reason: None,
139            last_best_elapsed: None,
140            best_solution_revision: None,
141            solution_revision: 1,
142            construction_frontier,
143            inphase_step_count_limit: None,
144            inphase_move_count_limit: None,
145            inphase_score_calc_count_limit: None,
146        }
147    }
148
149    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
150        self.terminate = terminate;
151        self
152    }
153
154    pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
155        self.runtime = runtime;
156        self
157    }
158
159    pub fn with_seed(mut self, seed: u64) -> Self {
160        self.rng = StdRng::seed_from_u64(seed);
161        self
162    }
163
164    pub fn with_progress_callback<F: ProgressCallback<S>>(
165        self,
166        callback: F,
167    ) -> SolverScope<'t, S, D, F> {
168        SolverScope {
169            score_director: self.score_director,
170            best_solution: self.best_solution,
171            current_score: self.current_score,
172            best_score: self.best_score,
173            rng: self.rng,
174            start_time: self.start_time,
175            paused_at: self.paused_at,
176            total_step_count: self.total_step_count,
177            terminate: self.terminate,
178            runtime: self.runtime,
179            stats: self.stats,
180            time_limit: self.time_limit,
181            progress_callback: callback,
182            terminal_reason: self.terminal_reason,
183            last_best_elapsed: self.last_best_elapsed,
184            best_solution_revision: self.best_solution_revision,
185            solution_revision: self.solution_revision,
186            construction_frontier: self.construction_frontier,
187            inphase_step_count_limit: self.inphase_step_count_limit,
188            inphase_move_count_limit: self.inphase_move_count_limit,
189            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
190        }
191    }
192
193    pub fn start_solving(&mut self) {
194        self.start_time = Some(Instant::now());
195        self.paused_at = None;
196        self.total_step_count = 0;
197        self.terminal_reason = None;
198        self.last_best_elapsed = None;
199        self.best_solution_revision = None;
200        self.solution_revision = 1;
201        self.construction_frontier.reset();
202        self.stats.start();
203    }
204
205    pub fn elapsed(&self) -> Option<Duration> {
206        match (self.start_time, self.paused_at) {
207            (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
208            (Some(start), None) => Some(start.elapsed()),
209            _ => None,
210        }
211    }
212
213    pub fn time_since_last_improvement(&self) -> Option<Duration> {
214        let elapsed = self.elapsed()?;
215        let last_best_elapsed = self.last_best_elapsed?;
216        Some(elapsed.saturating_sub(last_best_elapsed))
217    }
218
219    pub fn score_director(&self) -> &D {
220        &self.score_director
221    }
222
223    pub(crate) fn score_director_mut(&mut self) -> &mut D {
224        &mut self.score_director
225    }
226
227    pub fn working_solution(&self) -> &S {
228        self.score_director.working_solution()
229    }
230
231    pub fn trial<T, F>(&mut self, trial: F) -> T
232    where
233        F: FnOnce(&mut RecordingDirector<'_, S, D>) -> T,
234    {
235        let mut recording = RecordingDirector::new(&mut self.score_director);
236        let output = trial(&mut recording);
237        recording.undo_changes();
238        output
239    }
240
241    pub fn mutate<T, F>(&mut self, mutate: F) -> T
242    where
243        F: FnOnce(&mut D) -> T,
244    {
245        self.committed_mutation(mutate)
246    }
247
248    pub fn calculate_score(&mut self) -> S::Score {
249        self.stats.record_score_calculation();
250        let score = self.score_director.calculate_score();
251        self.current_score = Some(score);
252        score
253    }
254
255    pub fn initialize_working_solution_as_best(&mut self) -> S::Score {
256        if self.start_time.is_none() {
257            self.start_solving();
258        }
259        let score = self.calculate_score();
260        let solution = self.score_director.clone_working_solution();
261        self.set_best_solution(solution, score);
262        score
263    }
264
265    pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
266        *self.score_director.working_solution_mut() = solution;
267        self.score_director.reset();
268        self.current_score = None;
269        self.best_solution_revision = None;
270        self.solution_revision = 1;
271        self.construction_frontier.reset();
272        self.calculate_score()
273    }
274
275    pub fn best_solution(&self) -> Option<&S> {
276        self.best_solution.as_ref()
277    }
278
279    pub fn best_score(&self) -> Option<&S::Score> {
280        self.best_score.as_ref()
281    }
282
283    pub fn current_score(&self) -> Option<&S::Score> {
284        self.current_score.as_ref()
285    }
286
287    pub(crate) fn is_standard_slot_completed(&self, slot_id: ConstructionSlotId) -> bool {
288        self.construction_frontier
289            .is_standard_completed(slot_id, self.solution_revision)
290    }
291
292    pub(crate) fn mark_standard_slot_completed(&mut self, slot_id: ConstructionSlotId) {
293        self.construction_frontier
294            .mark_standard_completed(slot_id, self.solution_revision);
295    }
296
297    pub(crate) fn is_list_element_completed(&self, element_id: ConstructionListElementId) -> bool {
298        self.construction_frontier
299            .is_list_completed(element_id, self.solution_revision)
300    }
301
302    pub(crate) fn mark_list_element_completed(&mut self, element_id: ConstructionListElementId) {
303        self.construction_frontier
304            .mark_list_completed(element_id, self.solution_revision);
305    }
306
307    pub(crate) fn solution_revision(&self) -> u64 {
308        self.solution_revision
309    }
310
311    pub(crate) fn apply_committed_move<M>(&mut self, mov: &M)
312    where
313        M: Move<S>,
314    {
315        self.committed_mutation(|score_director| mov.do_move(score_director));
316    }
317
318    pub(crate) fn apply_committed_change<F>(&mut self, change: F)
319    where
320        F: FnOnce(&mut D),
321    {
322        self.committed_mutation(change);
323    }
324
325    pub(crate) fn construction_frontier(&self) -> &ConstructionFrontier {
326        &self.construction_frontier
327    }
328
329    pub fn terminal_reason(&self) -> SolverTerminalReason {
330        self.terminal_reason
331            .unwrap_or(SolverTerminalReason::Completed)
332    }
333
334    pub fn set_current_score(&mut self, score: S::Score) {
335        self.current_score = Some(score);
336    }
337
338    pub fn report_progress(&self) {
339        self.progress_callback.invoke(SolverProgressRef {
340            kind: SolverProgressKind::Progress,
341            status: self.progress_state(),
342            solution: None,
343            current_score: self.current_score.as_ref(),
344            best_score: self.best_score.as_ref(),
345            telemetry: self.stats.snapshot(),
346        });
347    }
348
349    pub fn report_best_solution(&self) {
350        self.progress_callback.invoke(SolverProgressRef {
351            kind: SolverProgressKind::BestSolution,
352            status: self.progress_state(),
353            solution: self.best_solution.as_ref(),
354            current_score: self.current_score.as_ref(),
355            best_score: self.best_score.as_ref(),
356            telemetry: self.stats.snapshot(),
357        });
358    }
359
360    pub fn update_best_solution(&mut self) {
361        let current_score = self.score_director.calculate_score();
362        self.current_score = Some(current_score);
363        let is_better = match &self.best_score {
364            None => true,
365            Some(best) => current_score > *best,
366        };
367
368        if is_better {
369            self.best_solution = Some(self.score_director.clone_working_solution());
370            self.best_score = Some(current_score);
371            self.last_best_elapsed = self.elapsed();
372            self.best_solution_revision = Some(self.solution_revision);
373            self.report_best_solution();
374        }
375    }
376
377    pub(crate) fn promote_current_solution_on_score_tie(&mut self) {
378        let Some(current_score) = self.current_score else {
379            return;
380        };
381        let Some(best_score) = self.best_score else {
382            return;
383        };
384
385        if current_score == best_score
386            && self.best_solution_revision != Some(self.solution_revision)
387        {
388            self.best_solution = Some(self.score_director.clone_working_solution());
389            self.best_solution_revision = Some(self.solution_revision);
390            self.report_best_solution();
391        }
392    }
393
394    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
395        if self.start_time.is_none() {
396            self.start_solving();
397        }
398        self.current_score = Some(score);
399        self.best_solution = Some(solution);
400        self.best_score = Some(score);
401        self.last_best_elapsed = self.elapsed();
402        self.best_solution_revision = Some(self.solution_revision);
403    }
404
405    pub fn rng(&mut self) -> &mut StdRng {
406        &mut self.rng
407    }
408
409    pub fn increment_step_count(&mut self) -> u64 {
410        self.total_step_count += 1;
411        self.stats.record_step();
412        self.total_step_count
413    }
414
415    pub fn total_step_count(&self) -> u64 {
416        self.total_step_count
417    }
418
419    pub fn take_best_solution(self) -> Option<S> {
420        self.best_solution
421    }
422
423    pub fn take_best_or_working_solution(self) -> S {
424        self.best_solution
425            .unwrap_or_else(|| self.score_director.clone_working_solution())
426    }
427
428    pub fn take_solution_and_stats(
429        self,
430    ) -> (
431        S,
432        Option<S::Score>,
433        S::Score,
434        SolverStats,
435        SolverTerminalReason,
436    ) {
437        let terminal_reason = self.terminal_reason();
438        let solution = self
439            .best_solution
440            .unwrap_or_else(|| self.score_director.clone_working_solution());
441        let best_score = self
442            .best_score
443            .or(self.current_score)
444            .expect("solver finished without a canonical score");
445        (
446            solution,
447            self.current_score,
448            best_score,
449            self.stats,
450            terminal_reason,
451        )
452    }
453
454    pub fn is_terminate_early(&self) -> bool {
455        self.terminate
456            .is_some_and(|flag| flag.load(Ordering::SeqCst))
457            || self
458                .runtime
459                .is_some_and(|runtime| runtime.is_cancel_requested())
460    }
461
462    pub(crate) fn pending_control(&self) -> PendingControl {
463        if self.is_terminate_early() {
464            return PendingControl::CancelRequested;
465        }
466        if self
467            .runtime
468            .is_some_and(|runtime| runtime.is_pause_requested())
469        {
470            return PendingControl::PauseRequested;
471        }
472        if self.time_limit_reached() {
473            return PendingControl::ConfigTerminationRequested;
474        }
475        PendingControl::Continue
476    }
477
478    pub fn set_time_limit(&mut self, limit: Duration) {
479        self.time_limit = Some(limit);
480    }
481
482    pub fn pause_if_requested(&mut self) {
483        self.settle_pause_if_requested();
484    }
485
486    pub fn pause_timers(&mut self) {
487        if self.paused_at.is_none() {
488            self.paused_at = Some(Instant::now());
489            self.stats.pause();
490        }
491    }
492
493    pub fn resume_timers(&mut self) {
494        if let Some(paused_at) = self.paused_at.take() {
495            let paused_for = paused_at.elapsed();
496            if let Some(start) = self.start_time {
497                self.start_time = Some(start + paused_for);
498            }
499            self.stats.resume();
500        }
501    }
502
503    pub fn should_terminate_construction(&mut self) -> bool {
504        self.settle_pause_if_requested();
505        if self.is_terminate_early() {
506            self.mark_cancelled();
507            return true;
508        }
509        if self.time_limit_reached() {
510            self.mark_terminated_by_config();
511            return true;
512        }
513        false
514    }
515
516    pub fn should_terminate(&mut self) -> bool {
517        self.settle_pause_if_requested();
518        if self.is_terminate_early() {
519            self.mark_cancelled();
520            return true;
521        }
522        if self.time_limit_reached() {
523            self.mark_terminated_by_config();
524            return true;
525        }
526        if let Some(limit) = self.inphase_step_count_limit {
527            if self.total_step_count >= limit {
528                self.mark_terminated_by_config();
529                return true;
530            }
531        }
532        if let Some(limit) = self.inphase_move_count_limit {
533            if self.stats.moves_evaluated >= limit {
534                self.mark_terminated_by_config();
535                return true;
536            }
537        }
538        if let Some(limit) = self.inphase_score_calc_count_limit {
539            if self.stats.score_calculations >= limit {
540                self.mark_terminated_by_config();
541                return true;
542            }
543        }
544        false
545    }
546
547    pub fn mark_cancelled(&mut self) {
548        self.terminal_reason
549            .get_or_insert(SolverTerminalReason::Cancelled);
550    }
551
552    pub fn mark_terminated_by_config(&mut self) {
553        self.terminal_reason
554            .get_or_insert(SolverTerminalReason::TerminatedByConfig);
555    }
556
557    pub fn stats(&self) -> &SolverStats {
558        &self.stats
559    }
560
561    pub fn stats_mut(&mut self) -> &mut SolverStats {
562        &mut self.stats
563    }
564
565    fn progress_state(&self) -> SolverLifecycleState {
566        self.runtime
567            .map(|runtime| {
568                if runtime.is_terminal() {
569                    SolverLifecycleState::Completed
570                } else {
571                    SolverLifecycleState::Solving
572                }
573            })
574            .unwrap_or(SolverLifecycleState::Solving)
575    }
576
577    fn settle_pause_if_requested(&mut self) {
578        if let Some(runtime) = self.runtime {
579            runtime.pause_if_requested(self);
580        }
581    }
582
583    fn time_limit_reached(&self) -> bool {
584        self.time_limit
585            .zip(self.elapsed())
586            .is_some_and(|(limit, elapsed)| elapsed >= limit)
587    }
588
589    fn advance_solution_revision(&mut self) {
590        self.solution_revision = self.solution_revision.wrapping_add(1);
591        if self.solution_revision == 0 {
592            self.solution_revision = 1;
593            self.construction_frontier.reset();
594        }
595    }
596
597    fn committed_mutation<T, F>(&mut self, mutate: F) -> T
598    where
599        F: FnOnce(&mut D) -> T,
600    {
601        self.current_score = None;
602        let output = mutate(&mut self.score_director);
603        self.advance_solution_revision();
604        output
605    }
606}