solverforge_solver/scope/
solver.rs

1//! Solver-level scope.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::Instant;
6
7use rand::rngs::StdRng;
8use rand::SeedableRng;
9
10use solverforge_core::domain::PlanningSolution;
11use solverforge_scoring::ScoreDirector;
12
13use crate::statistics::StatisticsCollector;
14
15/// Top-level scope for the entire solving process.
16///
17/// Holds the working solution, score director, and tracks the best solution found.
18pub struct SolverScope<S: PlanningSolution> {
19    /// The score director managing the working solution.
20    score_director: Box<dyn ScoreDirector<S>>,
21    /// The best solution found so far.
22    best_solution: Option<S>,
23    /// The score of the best solution.
24    best_score: Option<S::Score>,
25    /// Random number generator for stochastic algorithms.
26    rng: StdRng,
27    /// When solving started.
28    start_time: Option<Instant>,
29    /// Total number of steps across all phases.
30    total_step_count: u64,
31    /// Optional statistics collector for tracking solver metrics.
32    statistics: Option<Arc<StatisticsCollector<S::Score>>>,
33    /// Flag for early termination requests, shared with Solver.
34    terminate_early_flag: Option<Arc<AtomicBool>>,
35}
36
37impl<S: PlanningSolution> SolverScope<S> {
38    /// Creates a new solver scope with the given score director.
39    pub fn new(score_director: Box<dyn ScoreDirector<S>>) -> Self {
40        Self {
41            score_director,
42            best_solution: None,
43            best_score: None,
44            rng: StdRng::from_os_rng(),
45            start_time: None,
46            total_step_count: 0,
47            statistics: None,
48            terminate_early_flag: None,
49        }
50    }
51
52    /// Creates a solver scope with a specific random seed.
53    pub fn with_seed(score_director: Box<dyn ScoreDirector<S>>, seed: u64) -> Self {
54        Self {
55            score_director,
56            best_solution: None,
57            best_score: None,
58            rng: StdRng::seed_from_u64(seed),
59            start_time: None,
60            total_step_count: 0,
61            statistics: None,
62            terminate_early_flag: None,
63        }
64    }
65
66    /// Attaches a statistics collector to this scope.
67    ///
68    /// The collector will be updated during solving to track metrics.
69    pub fn with_statistics(mut self, collector: Arc<StatisticsCollector<S::Score>>) -> Self {
70        self.statistics = Some(collector);
71        self
72    }
73
74    /// Returns the statistics collector, if one is attached.
75    pub fn statistics(&self) -> Option<&Arc<StatisticsCollector<S::Score>>> {
76        self.statistics.as_ref()
77    }
78
79    /// Records a move evaluation in statistics.
80    ///
81    /// Does nothing if no statistics collector is attached.
82    pub fn record_move(&self, accepted: bool) {
83        if let Some(stats) = &self.statistics {
84            stats.record_move(accepted);
85        }
86    }
87
88    /// Records a score calculation in statistics.
89    ///
90    /// Does nothing if no statistics collector is attached.
91    pub fn record_score_calculation(&self) {
92        if let Some(stats) = &self.statistics {
93            stats.record_score_calculation();
94        }
95    }
96
97    /// Marks the start of solving.
98    pub fn start_solving(&mut self) {
99        self.start_time = Some(Instant::now());
100        self.total_step_count = 0;
101    }
102
103    /// Returns the elapsed time since solving started.
104    pub fn elapsed(&self) -> Option<std::time::Duration> {
105        self.start_time.map(|t| t.elapsed())
106    }
107
108    /// Returns a reference to the score director.
109    pub fn score_director(&self) -> &dyn ScoreDirector<S> {
110        self.score_director.as_ref()
111    }
112
113    /// Returns a mutable reference to the score director.
114    pub fn score_director_mut(&mut self) -> &mut dyn ScoreDirector<S> {
115        self.score_director.as_mut()
116    }
117
118    /// Returns a reference to the working solution.
119    pub fn working_solution(&self) -> &S {
120        self.score_director.working_solution()
121    }
122
123    /// Returns a mutable reference to the working solution.
124    pub fn working_solution_mut(&mut self) -> &mut S {
125        self.score_director.working_solution_mut()
126    }
127
128    /// Calculates and returns the current score.
129    pub fn calculate_score(&mut self) -> S::Score {
130        self.score_director.calculate_score()
131    }
132
133    /// Returns the best solution found so far.
134    pub fn best_solution(&self) -> Option<&S> {
135        self.best_solution.as_ref()
136    }
137
138    /// Returns the best score found so far.
139    pub fn best_score(&self) -> Option<&S::Score> {
140        self.best_score.as_ref()
141    }
142
143    /// Updates the best solution if the current solution is better.
144    pub fn update_best_solution(&mut self) {
145        let current_score = self.score_director.calculate_score();
146        let is_better = match &self.best_score {
147            None => true,
148            Some(best) => current_score > *best,
149        };
150
151        if is_better {
152            self.best_solution = Some(self.score_director.clone_working_solution());
153            self.best_score = Some(current_score.clone());
154
155            // Record improvement in statistics
156            if let Some(stats) = &self.statistics {
157                stats.record_improvement(current_score);
158            }
159        }
160    }
161
162    /// Forces an update of the best solution regardless of score comparison.
163    pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
164        self.best_solution = Some(solution);
165        self.best_score = Some(score);
166    }
167
168    /// Returns a reference to the RNG.
169    pub fn rng(&mut self) -> &mut StdRng {
170        &mut self.rng
171    }
172
173    /// Increments and returns the total step count.
174    pub fn increment_step_count(&mut self) -> u64 {
175        self.total_step_count += 1;
176        self.total_step_count
177    }
178
179    /// Returns the total step count.
180    pub fn total_step_count(&self) -> u64 {
181        self.total_step_count
182    }
183
184    /// Extracts the best solution, consuming this scope.
185    pub fn take_best_solution(self) -> Option<S> {
186        self.best_solution
187    }
188
189    /// Returns the best solution or the current working solution if no best was set.
190    ///
191    /// This is useful after construction heuristic where the working solution
192    /// may be the only valid solution even if it wasn't marked as "best".
193    pub fn take_best_or_working_solution(self) -> S {
194        self.best_solution
195            .unwrap_or_else(|| self.score_director.clone_working_solution())
196    }
197
198    /// Sets the terminate-early flag shared with the Solver.
199    ///
200    /// This allows phases to check if early termination was requested.
201    pub fn set_terminate_early_flag(&mut self, flag: Arc<AtomicBool>) {
202        self.terminate_early_flag = Some(flag);
203    }
204
205    /// Checks if early termination was requested.
206    ///
207    /// Returns `true` if the terminate-early flag is set, otherwise `false`.
208    pub fn is_terminate_early(&self) -> bool {
209        self.terminate_early_flag
210            .as_ref()
211            .is_some_and(|flag| flag.load(Ordering::SeqCst))
212    }
213}