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    /// Creates a new solver scope with the given score director and no callback.
72    pub fn new(score_director: D) -> Self {
73        Self {
74            score_director,
75            best_solution: None,
76            best_score: None,
77            rng: StdRng::from_os_rng(),
78            start_time: None,
79            total_step_count: 0,
80            terminate: None,
81            stats: SolverStats::default(),
82            time_limit: None,
83            best_solution_callback: (),
84            inphase_step_count_limit: None,
85            inphase_move_count_limit: None,
86            inphase_score_calc_count_limit: None,
87        }
88    }
89}
90
91impl<'t, S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>>
92    SolverScope<'t, S, D, BestCb>
93{
94    /// Creates a new solver scope with the given score director and callback.
95    pub fn new_with_callback(
96        score_director: D,
97        callback: BestCb,
98        terminate: Option<&'t std::sync::atomic::AtomicBool>,
99    ) -> Self {
100        Self {
101            score_director,
102            best_solution: None,
103            best_score: None,
104            rng: StdRng::from_os_rng(),
105            start_time: None,
106            total_step_count: 0,
107            terminate,
108            stats: SolverStats::default(),
109            time_limit: None,
110            best_solution_callback: callback,
111            inphase_step_count_limit: None,
112            inphase_move_count_limit: None,
113            inphase_score_calc_count_limit: None,
114        }
115    }
116}
117
118impl<'t, S: PlanningSolution, D: Director<S>, BestCb: BestSolutionCallback<S>>
119    SolverScope<'t, S, D, BestCb>
120{
121    /// Sets the termination flag.
122    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
123        self.terminate = terminate;
124        self
125    }
126
127    /// Sets a specific random seed.
128    pub fn with_seed(mut self, seed: u64) -> Self {
129        self.rng = StdRng::seed_from_u64(seed);
130        self
131    }
132
133    /// Sets the best solution callback, transitioning to a typed callback scope.
134    ///
135    /// The callback is invoked whenever the best solution improves during solving.
136    pub fn with_best_solution_callback<F: Fn(&S) + Send + Sync>(
137        self,
138        callback: F,
139    ) -> SolverScope<'t, S, D, F> {
140        SolverScope {
141            score_director: self.score_director,
142            best_solution: self.best_solution,
143            best_score: self.best_score,
144            rng: self.rng,
145            start_time: self.start_time,
146            total_step_count: self.total_step_count,
147            terminate: self.terminate,
148            stats: self.stats,
149            time_limit: self.time_limit,
150            best_solution_callback: callback,
151            inphase_step_count_limit: self.inphase_step_count_limit,
152            inphase_move_count_limit: self.inphase_move_count_limit,
153            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
154        }
155    }
156
157    /// Marks the start of solving.
158    pub fn start_solving(&mut self) {
159        self.start_time = Some(Instant::now());
160        self.total_step_count = 0;
161        self.stats.start();
162    }
163
164    /// Returns the elapsed time since solving started.
165    pub fn elapsed(&self) -> Option<std::time::Duration> {
166        self.start_time.map(|t| t.elapsed())
167    }
168
169    /// Returns a reference to the score director.
170    pub fn score_director(&self) -> &D {
171        &self.score_director
172    }
173
174    /// Returns a mutable reference to the score director.
175    pub fn score_director_mut(&mut self) -> &mut D {
176        &mut self.score_director
177    }
178
179    /// Returns a reference to the working solution.
180    pub fn working_solution(&self) -> &S {
181        self.score_director.working_solution()
182    }
183
184    /// Returns a mutable reference to the working solution.
185    pub fn working_solution_mut(&mut self) -> &mut S {
186        self.score_director.working_solution_mut()
187    }
188
189    /// Calculates and returns the current score.
190    ///
191    /// Also records the score calculation in solver statistics.
192    pub fn calculate_score(&mut self) -> S::Score {
193        self.stats.record_score_calculation();
194        self.score_director.calculate_score()
195    }
196
197    /// Returns the best solution found so far.
198    pub fn best_solution(&self) -> Option<&S> {
199        self.best_solution.as_ref()
200    }
201
202    /// Returns the best score found so far.
203    pub fn best_score(&self) -> Option<&S::Score> {
204        self.best_score.as_ref()
205    }
206
207    /// Updates the best solution if the current solution is better.
208    pub fn update_best_solution(&mut self) {
209        let current_score = self.score_director.calculate_score();
210        let is_better = match &self.best_score {
211            None => true,
212            Some(best) => current_score > *best,
213        };
214
215        if is_better {
216            self.best_solution = Some(self.score_director.clone_working_solution());
217            self.best_score = Some(current_score);
218
219            // Invoke callback if registered
220            if let Some(ref solution) = self.best_solution {
221                self.best_solution_callback.invoke(solution);
222            }
223        }
224    }
225
226    /// Forces an update of the best solution regardless of score comparison.
227    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
228        self.best_solution = Some(solution);
229        self.best_score = Some(score);
230    }
231
232    /// Returns a reference to the RNG.
233    pub fn rng(&mut self) -> &mut StdRng {
234        &mut self.rng
235    }
236
237    /// Increments and returns the total step count.
238    pub fn increment_step_count(&mut self) -> u64 {
239        self.total_step_count += 1;
240        self.stats.record_step();
241        self.total_step_count
242    }
243
244    /// Returns the total step count.
245    pub fn total_step_count(&self) -> u64 {
246        self.total_step_count
247    }
248
249    /// Extracts the best solution, consuming this scope.
250    pub fn take_best_solution(self) -> Option<S> {
251        self.best_solution
252    }
253
254    /// Returns the best solution or the current working solution if no best was set.
255    pub fn take_best_or_working_solution(self) -> S {
256        self.best_solution
257            .unwrap_or_else(|| self.score_director.clone_working_solution())
258    }
259
260    /// Extracts both the solution and stats, consuming this scope.
261    ///
262    /// Returns the best solution (or working solution if none) along with
263    /// the accumulated solver statistics.
264    pub fn take_solution_and_stats(self) -> (S, SolverStats) {
265        let solution = self
266            .best_solution
267            .unwrap_or_else(|| self.score_director.clone_working_solution());
268        (solution, self.stats)
269    }
270
271    /// Checks if early termination was requested (external flag only).
272    pub fn is_terminate_early(&self) -> bool {
273        self.terminate
274            .is_some_and(|flag| flag.load(Ordering::SeqCst))
275    }
276
277    /// Sets the time limit for solving.
278    pub fn set_time_limit(&mut self, limit: Duration) {
279        self.time_limit = Some(limit);
280    }
281
282    /// Checks if construction heuristic should terminate.
283    ///
284    /// Construction phases must always complete — they build the initial solution.
285    /// Step/move/score-calculation count limits are for local search phases only,
286    /// so this method intentionally excludes those checks.
287    pub fn should_terminate_construction(&self) -> bool {
288        // Check external termination flag
289        if self.is_terminate_early() {
290            return true;
291        }
292        // Check time limit only
293        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
294            if start.elapsed() >= limit {
295                return true;
296            }
297        }
298        false
299    }
300
301    /// Checks if solving should terminate (external flag, time limit, OR any registered limits).
302    pub fn should_terminate(&self) -> bool {
303        // Check external termination flag
304        if self.is_terminate_early() {
305            return true;
306        }
307        // Check time limit
308        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
309            if start.elapsed() >= limit {
310                return true;
311            }
312        }
313        // Check in-phase step count limit (T1: StepCountTermination fires inside phase loop)
314        if let Some(limit) = self.inphase_step_count_limit {
315            if self.total_step_count >= limit {
316                return true;
317            }
318        }
319        // Check in-phase move count limit (T1: MoveCountTermination fires inside phase loop)
320        if let Some(limit) = self.inphase_move_count_limit {
321            if self.stats.moves_evaluated >= limit {
322                return true;
323            }
324        }
325        // Check in-phase score calculation count limit
326        if let Some(limit) = self.inphase_score_calc_count_limit {
327            if self.stats.score_calculations >= limit {
328                return true;
329            }
330        }
331        false
332    }
333
334    /// Returns a reference to the solver statistics.
335    pub fn stats(&self) -> &SolverStats {
336        &self.stats
337    }
338
339    /// Returns a mutable reference to the solver statistics.
340    pub fn stats_mut(&mut self) -> &mut SolverStats {
341        &mut self.stats
342    }
343}