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