1use 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
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub(crate) enum PendingControl {
72 Continue,
73 PauseRequested,
74 CancelRequested,
75 ConfigTerminationRequested,
76}
77
78impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
79 pub fn new(score_director: D) -> Self {
80 Self {
81 score_director,
82 best_solution: None,
83 current_score: None,
84 best_score: None,
85 rng: StdRng::from_rng(&mut rand::rng()),
86 start_time: None,
87 paused_at: None,
88 total_step_count: 0,
89 terminate: None,
90 runtime: None,
91 stats: SolverStats::default(),
92 time_limit: None,
93 progress_callback: (),
94 terminal_reason: None,
95 last_best_elapsed: None,
96 inphase_step_count_limit: None,
97 inphase_move_count_limit: None,
98 inphase_score_calc_count_limit: None,
99 }
100 }
101}
102
103impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
104 SolverScope<'t, S, D, ProgressCb>
105{
106 pub fn new_with_callback(
107 score_director: D,
108 callback: ProgressCb,
109 terminate: Option<&'t AtomicBool>,
110 runtime: Option<SolverRuntime<S>>,
111 ) -> Self {
112 Self {
113 score_director,
114 best_solution: None,
115 current_score: None,
116 best_score: None,
117 rng: StdRng::from_rng(&mut rand::rng()),
118 start_time: None,
119 paused_at: None,
120 total_step_count: 0,
121 terminate,
122 runtime,
123 stats: SolverStats::default(),
124 time_limit: None,
125 progress_callback: callback,
126 terminal_reason: None,
127 last_best_elapsed: None,
128 inphase_step_count_limit: None,
129 inphase_move_count_limit: None,
130 inphase_score_calc_count_limit: None,
131 }
132 }
133
134 pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
135 self.terminate = terminate;
136 self
137 }
138
139 pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
140 self.runtime = runtime;
141 self
142 }
143
144 pub fn with_seed(mut self, seed: u64) -> Self {
145 self.rng = StdRng::seed_from_u64(seed);
146 self
147 }
148
149 pub fn with_progress_callback<F: ProgressCallback<S>>(
150 self,
151 callback: F,
152 ) -> SolverScope<'t, S, D, F> {
153 SolverScope {
154 score_director: self.score_director,
155 best_solution: self.best_solution,
156 current_score: self.current_score,
157 best_score: self.best_score,
158 rng: self.rng,
159 start_time: self.start_time,
160 paused_at: self.paused_at,
161 total_step_count: self.total_step_count,
162 terminate: self.terminate,
163 runtime: self.runtime,
164 stats: self.stats,
165 time_limit: self.time_limit,
166 progress_callback: callback,
167 terminal_reason: self.terminal_reason,
168 last_best_elapsed: self.last_best_elapsed,
169 inphase_step_count_limit: self.inphase_step_count_limit,
170 inphase_move_count_limit: self.inphase_move_count_limit,
171 inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
172 }
173 }
174
175 pub fn start_solving(&mut self) {
176 self.start_time = Some(Instant::now());
177 self.paused_at = None;
178 self.total_step_count = 0;
179 self.terminal_reason = None;
180 self.last_best_elapsed = None;
181 self.stats.start();
182 }
183
184 pub fn elapsed(&self) -> Option<Duration> {
185 match (self.start_time, self.paused_at) {
186 (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
187 (Some(start), None) => Some(start.elapsed()),
188 _ => None,
189 }
190 }
191
192 pub fn time_since_last_improvement(&self) -> Option<Duration> {
193 let elapsed = self.elapsed()?;
194 let last_best_elapsed = self.last_best_elapsed?;
195 Some(elapsed.saturating_sub(last_best_elapsed))
196 }
197
198 pub fn score_director(&self) -> &D {
199 &self.score_director
200 }
201
202 pub fn score_director_mut(&mut self) -> &mut D {
203 &mut self.score_director
204 }
205
206 pub fn working_solution(&self) -> &S {
207 self.score_director.working_solution()
208 }
209
210 pub fn working_solution_mut(&mut self) -> &mut S {
211 self.score_director.working_solution_mut()
212 }
213
214 pub fn calculate_score(&mut self) -> S::Score {
215 self.stats.record_score_calculation();
216 let score = self.score_director.calculate_score();
217 self.current_score = Some(score);
218 score
219 }
220
221 pub fn initialize_working_solution_as_best(&mut self) -> S::Score {
222 if self.start_time.is_none() {
223 self.start_solving();
224 }
225 let score = self.calculate_score();
226 let solution = self.score_director.clone_working_solution();
227 self.set_best_solution(solution, score);
228 score
229 }
230
231 pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
232 *self.score_director.working_solution_mut() = solution;
233 self.score_director.reset();
234 self.current_score = None;
235 self.calculate_score()
236 }
237
238 pub fn best_solution(&self) -> Option<&S> {
239 self.best_solution.as_ref()
240 }
241
242 pub fn best_score(&self) -> Option<&S::Score> {
243 self.best_score.as_ref()
244 }
245
246 pub fn current_score(&self) -> Option<&S::Score> {
247 self.current_score.as_ref()
248 }
249
250 pub fn terminal_reason(&self) -> SolverTerminalReason {
251 self.terminal_reason
252 .unwrap_or(SolverTerminalReason::Completed)
253 }
254
255 pub fn set_current_score(&mut self, score: S::Score) {
256 self.current_score = Some(score);
257 }
258
259 pub fn report_progress(&self) {
260 self.progress_callback.invoke(SolverProgressRef {
261 kind: SolverProgressKind::Progress,
262 status: self.progress_state(),
263 solution: None,
264 current_score: self.current_score.as_ref(),
265 best_score: self.best_score.as_ref(),
266 telemetry: self.stats.snapshot(),
267 });
268 }
269
270 pub fn report_best_solution(&self) {
271 self.progress_callback.invoke(SolverProgressRef {
272 kind: SolverProgressKind::BestSolution,
273 status: self.progress_state(),
274 solution: self.best_solution.as_ref(),
275 current_score: self.current_score.as_ref(),
276 best_score: self.best_score.as_ref(),
277 telemetry: self.stats.snapshot(),
278 });
279 }
280
281 pub fn update_best_solution(&mut self) {
282 let current_score = self.score_director.calculate_score();
283 self.current_score = Some(current_score);
284 let is_better = match &self.best_score {
285 None => true,
286 Some(best) => current_score > *best,
287 };
288
289 if is_better {
290 self.best_solution = Some(self.score_director.clone_working_solution());
291 self.best_score = Some(current_score);
292 self.last_best_elapsed = self.elapsed();
293 self.report_best_solution();
294 }
295 }
296
297 pub(crate) fn promote_current_solution_on_score_tie(&mut self) {
298 let Some(current_score) = self.current_score else {
299 return;
300 };
301 let Some(best_score) = self.best_score else {
302 return;
303 };
304
305 if current_score == best_score {
306 self.best_solution = Some(self.score_director.clone_working_solution());
307 self.report_best_solution();
308 }
309 }
310
311 pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
312 if self.start_time.is_none() {
313 self.start_solving();
314 }
315 self.current_score = Some(score);
316 self.best_solution = Some(solution);
317 self.best_score = Some(score);
318 self.last_best_elapsed = self.elapsed();
319 }
320
321 pub fn rng(&mut self) -> &mut StdRng {
322 &mut self.rng
323 }
324
325 pub fn increment_step_count(&mut self) -> u64 {
326 self.total_step_count += 1;
327 self.stats.record_step();
328 self.total_step_count
329 }
330
331 pub fn total_step_count(&self) -> u64 {
332 self.total_step_count
333 }
334
335 pub fn take_best_solution(self) -> Option<S> {
336 self.best_solution
337 }
338
339 pub fn take_best_or_working_solution(self) -> S {
340 self.best_solution
341 .unwrap_or_else(|| self.score_director.clone_working_solution())
342 }
343
344 pub fn take_solution_and_stats(
345 self,
346 ) -> (
347 S,
348 Option<S::Score>,
349 S::Score,
350 SolverStats,
351 SolverTerminalReason,
352 ) {
353 let terminal_reason = self.terminal_reason();
354 let solution = self
355 .best_solution
356 .unwrap_or_else(|| self.score_director.clone_working_solution());
357 let best_score = self
358 .best_score
359 .or(self.current_score)
360 .expect("solver finished without a canonical score");
361 (
362 solution,
363 self.current_score,
364 best_score,
365 self.stats,
366 terminal_reason,
367 )
368 }
369
370 pub fn is_terminate_early(&self) -> bool {
371 self.terminate
372 .is_some_and(|flag| flag.load(Ordering::SeqCst))
373 || self
374 .runtime
375 .is_some_and(|runtime| runtime.is_cancel_requested())
376 }
377
378 pub(crate) fn pending_control(&self) -> PendingControl {
379 if self.is_terminate_early() {
380 return PendingControl::CancelRequested;
381 }
382 if self
383 .runtime
384 .is_some_and(|runtime| runtime.is_pause_requested())
385 {
386 return PendingControl::PauseRequested;
387 }
388 if self.time_limit_reached() {
389 return PendingControl::ConfigTerminationRequested;
390 }
391 PendingControl::Continue
392 }
393
394 pub fn set_time_limit(&mut self, limit: Duration) {
395 self.time_limit = Some(limit);
396 }
397
398 pub fn pause_if_requested(&mut self) {
399 self.settle_pause_if_requested();
400 }
401
402 pub fn pause_timers(&mut self) {
403 if self.paused_at.is_none() {
404 self.paused_at = Some(Instant::now());
405 self.stats.pause();
406 }
407 }
408
409 pub fn resume_timers(&mut self) {
410 if let Some(paused_at) = self.paused_at.take() {
411 let paused_for = paused_at.elapsed();
412 if let Some(start) = self.start_time {
413 self.start_time = Some(start + paused_for);
414 }
415 self.stats.resume();
416 }
417 }
418
419 pub fn should_terminate_construction(&mut self) -> bool {
420 self.settle_pause_if_requested();
421 if self.is_terminate_early() {
422 self.mark_cancelled();
423 return true;
424 }
425 if self.time_limit_reached() {
426 self.mark_terminated_by_config();
427 return true;
428 }
429 false
430 }
431
432 pub fn should_terminate(&mut self) -> bool {
433 self.settle_pause_if_requested();
434 if self.is_terminate_early() {
435 self.mark_cancelled();
436 return true;
437 }
438 if self.time_limit_reached() {
439 self.mark_terminated_by_config();
440 return true;
441 }
442 if let Some(limit) = self.inphase_step_count_limit {
443 if self.total_step_count >= limit {
444 self.mark_terminated_by_config();
445 return true;
446 }
447 }
448 if let Some(limit) = self.inphase_move_count_limit {
449 if self.stats.moves_evaluated >= limit {
450 self.mark_terminated_by_config();
451 return true;
452 }
453 }
454 if let Some(limit) = self.inphase_score_calc_count_limit {
455 if self.stats.score_calculations >= limit {
456 self.mark_terminated_by_config();
457 return true;
458 }
459 }
460 false
461 }
462
463 pub fn mark_cancelled(&mut self) {
464 self.terminal_reason
465 .get_or_insert(SolverTerminalReason::Cancelled);
466 }
467
468 pub fn mark_terminated_by_config(&mut self) {
469 self.terminal_reason
470 .get_or_insert(SolverTerminalReason::TerminatedByConfig);
471 }
472
473 pub fn stats(&self) -> &SolverStats {
474 &self.stats
475 }
476
477 pub fn stats_mut(&mut self) -> &mut SolverStats {
478 &mut self.stats
479 }
480
481 fn progress_state(&self) -> SolverLifecycleState {
482 self.runtime
483 .map(|runtime| {
484 if runtime.is_terminal() {
485 SolverLifecycleState::Completed
486 } else {
487 SolverLifecycleState::Solving
488 }
489 })
490 .unwrap_or(SolverLifecycleState::Solving)
491 }
492
493 fn settle_pause_if_requested(&mut self) {
494 if let Some(runtime) = self.runtime {
495 runtime.pause_if_requested(self);
496 }
497 }
498
499 fn time_limit_reached(&self) -> bool {
500 self.time_limit
501 .zip(self.elapsed())
502 .is_some_and(|(limit, elapsed)| elapsed >= limit)
503 }
504}