solverforge_solver/scope/
solver.rs1use 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::manager::{SolverLifecycleState, SolverRuntime, SolverTerminalReason};
13use crate::stats::SolverStats;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SolverProgressKind {
17 Progress,
18 BestSolution,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct SolverProgressRef<'a, S: PlanningSolution> {
23 pub kind: SolverProgressKind,
24 pub status: SolverLifecycleState,
25 pub solution: Option<&'a S>,
26 pub current_score: Option<&'a S::Score>,
27 pub best_score: Option<&'a S::Score>,
28 pub telemetry: crate::stats::SolverTelemetry,
29}
30
31pub trait ProgressCallback<S: PlanningSolution>: Send + Sync {
32 fn invoke(&self, progress: SolverProgressRef<'_, S>);
33}
34
35impl<S: PlanningSolution> ProgressCallback<S> for () {
36 fn invoke(&self, _progress: SolverProgressRef<'_, S>) {}
37}
38
39impl<S, F> ProgressCallback<S> for F
40where
41 S: PlanningSolution,
42 F: for<'a> Fn(SolverProgressRef<'a, S>) + Send + Sync,
43{
44 fn invoke(&self, progress: SolverProgressRef<'_, S>) {
45 self(progress);
46 }
47}
48
49pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
50 score_director: D,
51 best_solution: Option<S>,
52 current_score: Option<S::Score>,
53 best_score: Option<S::Score>,
54 rng: StdRng,
55 start_time: Option<Instant>,
56 paused_at: Option<Instant>,
57 total_step_count: u64,
58 terminate: Option<&'t AtomicBool>,
59 runtime: Option<SolverRuntime<S>>,
60 stats: SolverStats,
61 time_limit: Option<Duration>,
62 progress_callback: ProgressCb,
63 terminal_reason: Option<SolverTerminalReason>,
64 last_best_elapsed: Option<Duration>,
65 pub inphase_step_count_limit: Option<u64>,
66 pub inphase_move_count_limit: Option<u64>,
67 pub inphase_score_calc_count_limit: Option<u64>,
68}
69
70impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
71 pub fn new(score_director: D) -> Self {
72 Self {
73 score_director,
74 best_solution: None,
75 current_score: None,
76 best_score: None,
77 rng: StdRng::from_rng(&mut rand::rng()),
78 start_time: None,
79 paused_at: None,
80 total_step_count: 0,
81 terminate: None,
82 runtime: None,
83 stats: SolverStats::default(),
84 time_limit: None,
85 progress_callback: (),
86 terminal_reason: None,
87 last_best_elapsed: None,
88 inphase_step_count_limit: None,
89 inphase_move_count_limit: None,
90 inphase_score_calc_count_limit: None,
91 }
92 }
93}
94
95impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
96 SolverScope<'t, S, D, ProgressCb>
97{
98 pub fn new_with_callback(
99 score_director: D,
100 callback: ProgressCb,
101 terminate: Option<&'t AtomicBool>,
102 runtime: Option<SolverRuntime<S>>,
103 ) -> Self {
104 Self {
105 score_director,
106 best_solution: None,
107 current_score: None,
108 best_score: None,
109 rng: StdRng::from_rng(&mut rand::rng()),
110 start_time: None,
111 paused_at: None,
112 total_step_count: 0,
113 terminate,
114 runtime,
115 stats: SolverStats::default(),
116 time_limit: None,
117 progress_callback: callback,
118 terminal_reason: None,
119 last_best_elapsed: None,
120 inphase_step_count_limit: None,
121 inphase_move_count_limit: None,
122 inphase_score_calc_count_limit: None,
123 }
124 }
125
126 pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
127 self.terminate = terminate;
128 self
129 }
130
131 pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
132 self.runtime = runtime;
133 self
134 }
135
136 pub fn with_seed(mut self, seed: u64) -> Self {
137 self.rng = StdRng::seed_from_u64(seed);
138 self
139 }
140
141 pub fn with_progress_callback<F: ProgressCallback<S>>(
142 self,
143 callback: F,
144 ) -> SolverScope<'t, S, D, F> {
145 SolverScope {
146 score_director: self.score_director,
147 best_solution: self.best_solution,
148 current_score: self.current_score,
149 best_score: self.best_score,
150 rng: self.rng,
151 start_time: self.start_time,
152 paused_at: self.paused_at,
153 total_step_count: self.total_step_count,
154 terminate: self.terminate,
155 runtime: self.runtime,
156 stats: self.stats,
157 time_limit: self.time_limit,
158 progress_callback: callback,
159 terminal_reason: self.terminal_reason,
160 last_best_elapsed: self.last_best_elapsed,
161 inphase_step_count_limit: self.inphase_step_count_limit,
162 inphase_move_count_limit: self.inphase_move_count_limit,
163 inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
164 }
165 }
166
167 pub fn start_solving(&mut self) {
168 self.start_time = Some(Instant::now());
169 self.paused_at = None;
170 self.total_step_count = 0;
171 self.terminal_reason = None;
172 self.last_best_elapsed = None;
173 self.stats.start();
174 }
175
176 pub fn elapsed(&self) -> Option<Duration> {
177 match (self.start_time, self.paused_at) {
178 (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
179 (Some(start), None) => Some(start.elapsed()),
180 _ => None,
181 }
182 }
183
184 pub fn time_since_last_improvement(&self) -> Option<Duration> {
185 let elapsed = self.elapsed()?;
186 let last_best_elapsed = self.last_best_elapsed?;
187 Some(elapsed.saturating_sub(last_best_elapsed))
188 }
189
190 pub fn score_director(&self) -> &D {
191 &self.score_director
192 }
193
194 pub fn score_director_mut(&mut self) -> &mut D {
195 &mut self.score_director
196 }
197
198 pub fn working_solution(&self) -> &S {
199 self.score_director.working_solution()
200 }
201
202 pub fn working_solution_mut(&mut self) -> &mut S {
203 self.score_director.working_solution_mut()
204 }
205
206 pub fn calculate_score(&mut self) -> S::Score {
207 self.stats.record_score_calculation();
208 let score = self.score_director.calculate_score();
209 self.current_score = Some(score);
210 score
211 }
212
213 pub fn best_solution(&self) -> Option<&S> {
214 self.best_solution.as_ref()
215 }
216
217 pub fn best_score(&self) -> Option<&S::Score> {
218 self.best_score.as_ref()
219 }
220
221 pub fn current_score(&self) -> Option<&S::Score> {
222 self.current_score.as_ref()
223 }
224
225 pub fn terminal_reason(&self) -> SolverTerminalReason {
226 self.terminal_reason
227 .unwrap_or(SolverTerminalReason::Completed)
228 }
229
230 pub fn set_current_score(&mut self, score: S::Score) {
231 self.current_score = Some(score);
232 }
233
234 pub fn report_progress(&self) {
235 self.progress_callback.invoke(SolverProgressRef {
236 kind: SolverProgressKind::Progress,
237 status: self.progress_state(),
238 solution: None,
239 current_score: self.current_score.as_ref(),
240 best_score: self.best_score.as_ref(),
241 telemetry: self.stats.snapshot(),
242 });
243 }
244
245 pub fn report_best_solution(&self) {
246 self.progress_callback.invoke(SolverProgressRef {
247 kind: SolverProgressKind::BestSolution,
248 status: self.progress_state(),
249 solution: self.best_solution.as_ref(),
250 current_score: self.current_score.as_ref(),
251 best_score: self.best_score.as_ref(),
252 telemetry: self.stats.snapshot(),
253 });
254 }
255
256 pub fn update_best_solution(&mut self) {
257 let current_score = self.score_director.calculate_score();
258 self.current_score = Some(current_score);
259 let is_better = match &self.best_score {
260 None => true,
261 Some(best) => current_score > *best,
262 };
263
264 if is_better {
265 self.best_solution = Some(self.score_director.clone_working_solution());
266 self.best_score = Some(current_score);
267 self.last_best_elapsed = self.elapsed();
268 self.report_best_solution();
269 }
270 }
271
272 pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
273 if self.start_time.is_none() {
274 self.start_solving();
275 }
276 self.current_score = Some(score);
277 self.best_solution = Some(solution);
278 self.best_score = Some(score);
279 self.last_best_elapsed = self.elapsed();
280 }
281
282 pub fn rng(&mut self) -> &mut StdRng {
283 &mut self.rng
284 }
285
286 pub fn increment_step_count(&mut self) -> u64 {
287 self.total_step_count += 1;
288 self.stats.record_step();
289 self.total_step_count
290 }
291
292 pub fn total_step_count(&self) -> u64 {
293 self.total_step_count
294 }
295
296 pub fn take_best_solution(self) -> Option<S> {
297 self.best_solution
298 }
299
300 pub fn take_best_or_working_solution(self) -> S {
301 self.best_solution
302 .unwrap_or_else(|| self.score_director.clone_working_solution())
303 }
304
305 pub fn take_solution_and_stats(
306 self,
307 ) -> (
308 S,
309 Option<S::Score>,
310 S::Score,
311 SolverStats,
312 SolverTerminalReason,
313 ) {
314 let terminal_reason = self.terminal_reason();
315 let solution = self
316 .best_solution
317 .unwrap_or_else(|| self.score_director.clone_working_solution());
318 let best_score = self
319 .best_score
320 .or(self.current_score)
321 .expect("solver finished without a canonical score");
322 (
323 solution,
324 self.current_score,
325 best_score,
326 self.stats,
327 terminal_reason,
328 )
329 }
330
331 pub fn is_terminate_early(&self) -> bool {
332 self.terminate
333 .is_some_and(|flag| flag.load(Ordering::SeqCst))
334 }
335
336 pub fn set_time_limit(&mut self, limit: Duration) {
337 self.time_limit = Some(limit);
338 }
339
340 pub fn pause_if_requested(&mut self) {
341 self.settle_pause_if_requested();
342 }
343
344 pub fn pause_timers(&mut self) {
345 if self.paused_at.is_none() {
346 self.paused_at = Some(Instant::now());
347 self.stats.pause();
348 }
349 }
350
351 pub fn resume_timers(&mut self) {
352 if let Some(paused_at) = self.paused_at.take() {
353 let paused_for = paused_at.elapsed();
354 if let Some(start) = self.start_time {
355 self.start_time = Some(start + paused_for);
356 }
357 self.stats.resume();
358 }
359 }
360
361 pub fn should_terminate_construction(&mut self) -> bool {
362 self.settle_pause_if_requested();
363 if self.is_terminate_early() {
364 self.mark_cancelled();
365 return true;
366 }
367 if self.time_limit_reached() {
368 self.mark_terminated_by_config();
369 return true;
370 }
371 false
372 }
373
374 pub fn should_terminate(&mut self) -> bool {
375 self.settle_pause_if_requested();
376 if self.is_terminate_early() {
377 self.mark_cancelled();
378 return true;
379 }
380 if self.time_limit_reached() {
381 self.mark_terminated_by_config();
382 return true;
383 }
384 if let Some(limit) = self.inphase_step_count_limit {
385 if self.total_step_count >= limit {
386 self.mark_terminated_by_config();
387 return true;
388 }
389 }
390 if let Some(limit) = self.inphase_move_count_limit {
391 if self.stats.moves_evaluated >= limit {
392 self.mark_terminated_by_config();
393 return true;
394 }
395 }
396 if let Some(limit) = self.inphase_score_calc_count_limit {
397 if self.stats.score_calculations >= limit {
398 self.mark_terminated_by_config();
399 return true;
400 }
401 }
402 false
403 }
404
405 pub fn mark_cancelled(&mut self) {
406 self.terminal_reason
407 .get_or_insert(SolverTerminalReason::Cancelled);
408 }
409
410 pub fn mark_terminated_by_config(&mut self) {
411 self.terminal_reason
412 .get_or_insert(SolverTerminalReason::TerminatedByConfig);
413 }
414
415 pub fn stats(&self) -> &SolverStats {
416 &self.stats
417 }
418
419 pub fn stats_mut(&mut self) -> &mut SolverStats {
420 &mut self.stats
421 }
422
423 fn progress_state(&self) -> SolverLifecycleState {
424 self.runtime
425 .map(|runtime| {
426 if runtime.is_terminal() {
427 SolverLifecycleState::Completed
428 } else {
429 SolverLifecycleState::Solving
430 }
431 })
432 .unwrap_or(SolverLifecycleState::Solving)
433 }
434
435 fn settle_pause_if_requested(&mut self) {
436 if let Some(runtime) = self.runtime {
437 runtime.pause_if_requested(self);
438 }
439 }
440
441 fn time_limit_reached(&self) -> bool {
442 self.time_limit
443 .zip(self.elapsed())
444 .is_some_and(|(limit, elapsed)| elapsed >= limit)
445 }
446}