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::stats::SolverStats;
13
14/// Sealed trait for invoking an optional best-solution callback.
15///
16/// Implemented for `()` (no-op) and for any `F: Fn(&S) + Send + Sync`.
17pub trait BestSolutionCallback<S>: Send + Sync {
18    // Invokes the callback with the given solution, if one is registered.
19    fn invoke(&self, solution: &S);
20}
21
22impl<S> BestSolutionCallback<S> for () {
23    fn invoke(&self, _solution: &S) {}
24}
25
26impl<S, F: Fn(&S) + Send + Sync> BestSolutionCallback<S> for F {
27    fn invoke(&self, solution: &S) {
28        self(solution);
29    }
30}
31
32/// Top-level scope for the entire solving process.
33///
34/// Holds the working solution, score director, and tracks the best solution found.
35///
36/// # Type Parameters
37/// * `'t` - Lifetime of the termination flag reference
38/// * `S` - The planning solution type
39/// * `D` - The score director type
40/// * `BestCb` - The best-solution callback type (default `()` means no callback)
41pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, BestCb = ()> {
42    // The score director managing the working solution.
43    score_director: D,
44    // The best solution found so far.
45    best_solution: Option<S>,
46    // The score of the best solution.
47    best_score: Option<S::Score>,
48    // Random number generator for stochastic algorithms.
49    rng: StdRng,
50    // When solving started.
51    start_time: Option<Instant>,
52    // Total number of steps across all phases.
53    total_step_count: u64,
54    // Flag for early termination requests.
55    terminate: Option<&'t AtomicBool>,
56    // Solver statistics.
57    stats: SolverStats,
58    // Time limit for solving (checked by phases).
59    time_limit: Option<Duration>,
60    // Callback invoked when the best solution improves.
61    best_solution_callback: BestCb,
62    // Optional maximum total step count for in-phase termination (T1).
63    pub inphase_step_count_limit: Option<u64>,
64    // Optional maximum total move count for in-phase termination (T1).
65    pub inphase_move_count_limit: Option<u64>,
66    // Optional maximum total score calculation count for in-phase termination (T1).
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            best_score: None,
76            rng: StdRng::from_rng(&mut rand::rng()),
77            start_time: None,
78            total_step_count: 0,
79            terminate: None,
80            stats: SolverStats::default(),
81            time_limit: None,
82            best_solution_callback: (),
83            inphase_step_count_limit: None,
84            inphase_move_count_limit: None,
85            inphase_score_calc_count_limit: None,
86        }
87    }
88}
89
90impl<'t, S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>>
91    SolverScope<'t, S, D, BestCb>
92{
93    pub fn new_with_callback(
94        score_director: D,
95        callback: BestCb,
96        terminate: Option<&'t std::sync::atomic::AtomicBool>,
97    ) -> Self {
98        Self {
99            score_director,
100            best_solution: None,
101            best_score: None,
102            rng: StdRng::from_rng(&mut rand::rng()),
103            start_time: None,
104            total_step_count: 0,
105            terminate,
106            stats: SolverStats::default(),
107            time_limit: None,
108            best_solution_callback: callback,
109            inphase_step_count_limit: None,
110            inphase_move_count_limit: None,
111            inphase_score_calc_count_limit: None,
112        }
113    }
114}
115
116impl<'t, S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>>
117    SolverScope<'t, S, D, BestCb>
118{
119    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
120        self.terminate = terminate;
121        self
122    }
123
124    pub fn with_seed(mut self, seed: u64) -> Self {
125        self.rng = StdRng::seed_from_u64(seed);
126        self
127    }
128
129    /// Sets the best solution callback, transitioning to a typed callback scope.
130    ///
131    /// The callback is invoked whenever the best solution improves during solving.
132    pub fn with_best_solution_callback<F: Fn(&S) + Send + Sync>(
133        self,
134        callback: F,
135    ) -> SolverScope<'t, S, D, F> {
136        SolverScope {
137            score_director: self.score_director,
138            best_solution: self.best_solution,
139            best_score: self.best_score,
140            rng: self.rng,
141            start_time: self.start_time,
142            total_step_count: self.total_step_count,
143            terminate: self.terminate,
144            stats: self.stats,
145            time_limit: self.time_limit,
146            best_solution_callback: callback,
147            inphase_step_count_limit: self.inphase_step_count_limit,
148            inphase_move_count_limit: self.inphase_move_count_limit,
149            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
150        }
151    }
152
153    /// Marks the start of solving.
154    pub fn start_solving(&mut self) {
155        self.start_time = Some(Instant::now());
156        self.total_step_count = 0;
157        self.stats.start();
158    }
159
160    pub fn elapsed(&self) -> Option<std::time::Duration> {
161        self.start_time.map(|t| t.elapsed())
162    }
163
164    pub fn score_director(&self) -> &D {
165        &self.score_director
166    }
167
168    pub fn score_director_mut(&mut self) -> &mut D {
169        &mut self.score_director
170    }
171
172    pub fn working_solution(&self) -> &S {
173        self.score_director.working_solution()
174    }
175
176    pub fn working_solution_mut(&mut self) -> &mut S {
177        self.score_director.working_solution_mut()
178    }
179
180    /// Calculates and returns the current score.
181    ///
182    /// Also records the score calculation in solver statistics.
183    pub fn calculate_score(&mut self) -> S::Score {
184        self.stats.record_score_calculation();
185        self.score_director.calculate_score()
186    }
187
188    pub fn best_solution(&self) -> Option<&S> {
189        self.best_solution.as_ref()
190    }
191
192    pub fn best_score(&self) -> Option<&S::Score> {
193        self.best_score.as_ref()
194    }
195
196    /// Updates the best solution if the current solution is better.
197    pub fn update_best_solution(&mut self) {
198        let current_score = self.score_director.calculate_score();
199        let is_better = match &self.best_score {
200            None => true,
201            Some(best) => current_score > *best,
202        };
203
204        if is_better {
205            self.best_solution = Some(self.score_director.clone_working_solution());
206            self.best_score = Some(current_score);
207
208            // Invoke callback if registered
209            if let Some(ref solution) = self.best_solution {
210                self.best_solution_callback.invoke(solution);
211            }
212        }
213    }
214
215    /// Forces an update of the best solution regardless of score comparison.
216    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
217        self.best_solution = Some(solution);
218        self.best_score = Some(score);
219    }
220
221    pub fn rng(&mut self) -> &mut StdRng {
222        &mut self.rng
223    }
224
225    /// Increments and returns the total step count.
226    pub fn increment_step_count(&mut self) -> u64 {
227        self.total_step_count += 1;
228        self.stats.record_step();
229        self.total_step_count
230    }
231
232    pub fn total_step_count(&self) -> u64 {
233        self.total_step_count
234    }
235
236    /// Extracts the best solution, consuming this scope.
237    pub fn take_best_solution(self) -> Option<S> {
238        self.best_solution
239    }
240
241    pub fn take_best_or_working_solution(self) -> S {
242        self.best_solution
243            .unwrap_or_else(|| self.score_director.clone_working_solution())
244    }
245
246    /// Extracts both the solution and stats, consuming this scope.
247    ///
248    /// Returns the best solution (or working solution if none) along with
249    /// the accumulated solver statistics.
250    pub fn take_solution_and_stats(self) -> (S, SolverStats) {
251        let solution = self
252            .best_solution
253            .unwrap_or_else(|| self.score_director.clone_working_solution());
254        (solution, self.stats)
255    }
256
257    pub fn is_terminate_early(&self) -> bool {
258        self.terminate
259            .is_some_and(|flag| flag.load(Ordering::SeqCst))
260    }
261
262    pub fn set_time_limit(&mut self, limit: Duration) {
263        self.time_limit = Some(limit);
264    }
265
266    pub fn should_terminate_construction(&self) -> bool {
267        // Check external termination flag
268        if self.is_terminate_early() {
269            return true;
270        }
271        // Check time limit only
272        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
273            if start.elapsed() >= limit {
274                return true;
275            }
276        }
277        false
278    }
279
280    pub fn should_terminate(&self) -> bool {
281        // Check external termination flag
282        if self.is_terminate_early() {
283            return true;
284        }
285        // Check time limit
286        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
287            if start.elapsed() >= limit {
288                return true;
289            }
290        }
291        // Check in-phase step count limit (T1: StepCountTermination fires inside phase loop)
292        if let Some(limit) = self.inphase_step_count_limit {
293            if self.total_step_count >= limit {
294                return true;
295            }
296        }
297        // Check in-phase move count limit (T1: MoveCountTermination fires inside phase loop)
298        if let Some(limit) = self.inphase_move_count_limit {
299            if self.stats.moves_evaluated >= limit {
300                return true;
301            }
302        }
303        // Check in-phase score calculation count limit
304        if let Some(limit) = self.inphase_score_calc_count_limit {
305            if self.stats.score_calculations >= limit {
306                return true;
307            }
308        }
309        false
310    }
311
312    pub fn stats(&self) -> &SolverStats {
313        &self.stats
314    }
315
316    pub fn stats_mut(&mut self) -> &mut SolverStats {
317        &mut self.stats
318    }
319}