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
22pub struct SolverScope<'t, S: PlanningSolution, D: ScoreDirector<S>> {
23    /// The score director managing the working solution.
24    score_director: D,
25    /// The best solution found so far.
26    best_solution: Option<S>,
27    /// The score of the best solution.
28    best_score: Option<S::Score>,
29    /// Random number generator for stochastic algorithms.
30    rng: StdRng,
31    /// When solving started.
32    start_time: Option<Instant>,
33    /// Total number of steps across all phases.
34    total_step_count: u64,
35    /// Flag for early termination requests.
36    terminate: Option<&'t AtomicBool>,
37    /// Solver statistics.
38    stats: SolverStats,
39    /// Time limit for solving (checked by phases).
40    time_limit: Option<Duration>,
41}
42
43impl<'t, S: PlanningSolution, D: ScoreDirector<S>> SolverScope<'t, S, D> {
44    /// Creates a new solver scope with the given score director.
45    pub fn new(score_director: D) -> Self {
46        Self {
47            score_director,
48            best_solution: None,
49            best_score: None,
50            rng: StdRng::from_os_rng(),
51            start_time: None,
52            total_step_count: 0,
53            terminate: None,
54            stats: SolverStats::default(),
55            time_limit: None,
56        }
57    }
58
59    /// Creates a solver scope with a termination flag.
60    pub fn with_terminate(score_director: D, terminate: Option<&'t AtomicBool>) -> Self {
61        Self {
62            score_director,
63            best_solution: None,
64            best_score: None,
65            rng: StdRng::from_os_rng(),
66            start_time: None,
67            total_step_count: 0,
68            terminate,
69            stats: SolverStats::default(),
70            time_limit: None,
71        }
72    }
73
74    /// Creates a solver scope with a specific random seed.
75    pub fn with_seed(score_director: D, seed: u64) -> Self {
76        Self {
77            score_director,
78            best_solution: None,
79            best_score: None,
80            rng: StdRng::seed_from_u64(seed),
81            start_time: None,
82            total_step_count: 0,
83            terminate: None,
84            stats: SolverStats::default(),
85            time_limit: None,
86        }
87    }
88
89    /// Marks the start of solving.
90    pub fn start_solving(&mut self) {
91        self.start_time = Some(Instant::now());
92        self.total_step_count = 0;
93        self.stats.start();
94    }
95
96    /// Returns the elapsed time since solving started.
97    pub fn elapsed(&self) -> Option<std::time::Duration> {
98        self.start_time.map(|t| t.elapsed())
99    }
100
101    /// Returns a reference to the score director.
102    pub fn score_director(&self) -> &D {
103        &self.score_director
104    }
105
106    /// Returns a mutable reference to the score director.
107    pub fn score_director_mut(&mut self) -> &mut D {
108        &mut self.score_director
109    }
110
111    /// Returns a reference to the working solution.
112    pub fn working_solution(&self) -> &S {
113        self.score_director.working_solution()
114    }
115
116    /// Returns a mutable reference to the working solution.
117    pub fn working_solution_mut(&mut self) -> &mut S {
118        self.score_director.working_solution_mut()
119    }
120
121    /// Calculates and returns the current score.
122    pub fn calculate_score(&mut self) -> S::Score {
123        self.score_director.calculate_score()
124    }
125
126    /// Returns the best solution found so far.
127    pub fn best_solution(&self) -> Option<&S> {
128        self.best_solution.as_ref()
129    }
130
131    /// Returns the best score found so far.
132    pub fn best_score(&self) -> Option<&S::Score> {
133        self.best_score.as_ref()
134    }
135
136    /// Updates the best solution if the current solution is better.
137    pub fn update_best_solution(&mut self) {
138        let current_score = self.score_director.calculate_score();
139        let is_better = match &self.best_score {
140            None => true,
141            Some(best) => current_score > *best,
142        };
143
144        if is_better {
145            self.best_solution = Some(self.score_director.clone_working_solution());
146            self.best_score = Some(current_score);
147        }
148    }
149
150    /// Forces an update of the best solution regardless of score comparison.
151    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
152        self.best_solution = Some(solution);
153        self.best_score = Some(score);
154    }
155
156    /// Returns a reference to the RNG.
157    pub fn rng(&mut self) -> &mut StdRng {
158        &mut self.rng
159    }
160
161    /// Increments and returns the total step count.
162    pub fn increment_step_count(&mut self) -> u64 {
163        self.total_step_count += 1;
164        self.total_step_count
165    }
166
167    /// Returns the total step count.
168    pub fn total_step_count(&self) -> u64 {
169        self.total_step_count
170    }
171
172    /// Extracts the best solution, consuming this scope.
173    pub fn take_best_solution(self) -> Option<S> {
174        self.best_solution
175    }
176
177    /// Returns the best solution or the current working solution if no best was set.
178    pub fn take_best_or_working_solution(self) -> S {
179        self.best_solution
180            .unwrap_or_else(|| self.score_director.clone_working_solution())
181    }
182
183    /// Checks if early termination was requested (external flag only).
184    pub fn is_terminate_early(&self) -> bool {
185        self.terminate
186            .is_some_and(|flag| flag.load(Ordering::SeqCst))
187    }
188
189    /// Sets the time limit for solving.
190    pub fn set_time_limit(&mut self, limit: Duration) {
191        self.time_limit = Some(limit);
192    }
193
194    /// Checks if solving should terminate (external flag OR time limit).
195    pub fn should_terminate(&self) -> bool {
196        // Check external termination flag
197        if self.is_terminate_early() {
198            return true;
199        }
200        // Check time limit
201        if let (Some(start), Some(limit)) = (self.start_time, self.time_limit) {
202            if start.elapsed() >= limit {
203                return true;
204            }
205        }
206        false
207    }
208
209    /// Returns a reference to the solver statistics.
210    pub fn stats(&self) -> &SolverStats {
211        &self.stats
212    }
213
214    /// Returns a mutable reference to the solver statistics.
215    pub fn stats_mut(&mut self) -> &mut SolverStats {
216        &mut self.stats
217    }
218}