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