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 best_solution(&self) -> Option<&S> {
222 self.best_solution.as_ref()
223 }
224
225 pub fn best_score(&self) -> Option<&S::Score> {
226 self.best_score.as_ref()
227 }
228
229 pub fn current_score(&self) -> Option<&S::Score> {
230 self.current_score.as_ref()
231 }
232
233 pub fn terminal_reason(&self) -> SolverTerminalReason {
234 self.terminal_reason
235 .unwrap_or(SolverTerminalReason::Completed)
236 }
237
238 pub fn set_current_score(&mut self, score: S::Score) {
239 self.current_score = Some(score);
240 }
241
242 pub fn report_progress(&self) {
243 self.progress_callback.invoke(SolverProgressRef {
244 kind: SolverProgressKind::Progress,
245 status: self.progress_state(),
246 solution: None,
247 current_score: self.current_score.as_ref(),
248 best_score: self.best_score.as_ref(),
249 telemetry: self.stats.snapshot(),
250 });
251 }
252
253 pub fn report_best_solution(&self) {
254 self.progress_callback.invoke(SolverProgressRef {
255 kind: SolverProgressKind::BestSolution,
256 status: self.progress_state(),
257 solution: self.best_solution.as_ref(),
258 current_score: self.current_score.as_ref(),
259 best_score: self.best_score.as_ref(),
260 telemetry: self.stats.snapshot(),
261 });
262 }
263
264 pub fn update_best_solution(&mut self) {
265 let current_score = self.score_director.calculate_score();
266 self.current_score = Some(current_score);
267 let is_better = match &self.best_score {
268 None => true,
269 Some(best) => current_score > *best,
270 };
271
272 if is_better {
273 self.best_solution = Some(self.score_director.clone_working_solution());
274 self.best_score = Some(current_score);
275 self.last_best_elapsed = self.elapsed();
276 self.report_best_solution();
277 }
278 }
279
280 pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
281 if self.start_time.is_none() {
282 self.start_solving();
283 }
284 self.current_score = Some(score);
285 self.best_solution = Some(solution);
286 self.best_score = Some(score);
287 self.last_best_elapsed = self.elapsed();
288 }
289
290 pub fn rng(&mut self) -> &mut StdRng {
291 &mut self.rng
292 }
293
294 pub fn increment_step_count(&mut self) -> u64 {
295 self.total_step_count += 1;
296 self.stats.record_step();
297 self.total_step_count
298 }
299
300 pub fn total_step_count(&self) -> u64 {
301 self.total_step_count
302 }
303
304 pub fn take_best_solution(self) -> Option<S> {
305 self.best_solution
306 }
307
308 pub fn take_best_or_working_solution(self) -> S {
309 self.best_solution
310 .unwrap_or_else(|| self.score_director.clone_working_solution())
311 }
312
313 pub fn take_solution_and_stats(
314 self,
315 ) -> (
316 S,
317 Option<S::Score>,
318 S::Score,
319 SolverStats,
320 SolverTerminalReason,
321 ) {
322 let terminal_reason = self.terminal_reason();
323 let solution = self
324 .best_solution
325 .unwrap_or_else(|| self.score_director.clone_working_solution());
326 let best_score = self
327 .best_score
328 .or(self.current_score)
329 .expect("solver finished without a canonical score");
330 (
331 solution,
332 self.current_score,
333 best_score,
334 self.stats,
335 terminal_reason,
336 )
337 }
338
339 pub fn is_terminate_early(&self) -> bool {
340 self.terminate
341 .is_some_and(|flag| flag.load(Ordering::SeqCst))
342 || self
343 .runtime
344 .is_some_and(|runtime| runtime.is_cancel_requested())
345 }
346
347 pub(crate) fn pending_control(&self) -> PendingControl {
348 if self.is_terminate_early() {
349 return PendingControl::CancelRequested;
350 }
351 if self
352 .runtime
353 .is_some_and(|runtime| runtime.is_pause_requested())
354 {
355 return PendingControl::PauseRequested;
356 }
357 if self.time_limit_reached() {
358 return PendingControl::ConfigTerminationRequested;
359 }
360 PendingControl::Continue
361 }
362
363 pub fn set_time_limit(&mut self, limit: Duration) {
364 self.time_limit = Some(limit);
365 }
366
367 pub fn pause_if_requested(&mut self) {
368 self.settle_pause_if_requested();
369 }
370
371 pub fn pause_timers(&mut self) {
372 if self.paused_at.is_none() {
373 self.paused_at = Some(Instant::now());
374 self.stats.pause();
375 }
376 }
377
378 pub fn resume_timers(&mut self) {
379 if let Some(paused_at) = self.paused_at.take() {
380 let paused_for = paused_at.elapsed();
381 if let Some(start) = self.start_time {
382 self.start_time = Some(start + paused_for);
383 }
384 self.stats.resume();
385 }
386 }
387
388 pub fn should_terminate_construction(&mut self) -> bool {
389 self.settle_pause_if_requested();
390 if self.is_terminate_early() {
391 self.mark_cancelled();
392 return true;
393 }
394 if self.time_limit_reached() {
395 self.mark_terminated_by_config();
396 return true;
397 }
398 false
399 }
400
401 pub fn should_terminate(&mut self) -> bool {
402 self.settle_pause_if_requested();
403 if self.is_terminate_early() {
404 self.mark_cancelled();
405 return true;
406 }
407 if self.time_limit_reached() {
408 self.mark_terminated_by_config();
409 return true;
410 }
411 if let Some(limit) = self.inphase_step_count_limit {
412 if self.total_step_count >= limit {
413 self.mark_terminated_by_config();
414 return true;
415 }
416 }
417 if let Some(limit) = self.inphase_move_count_limit {
418 if self.stats.moves_evaluated >= limit {
419 self.mark_terminated_by_config();
420 return true;
421 }
422 }
423 if let Some(limit) = self.inphase_score_calc_count_limit {
424 if self.stats.score_calculations >= limit {
425 self.mark_terminated_by_config();
426 return true;
427 }
428 }
429 false
430 }
431
432 pub fn mark_cancelled(&mut self) {
433 self.terminal_reason
434 .get_or_insert(SolverTerminalReason::Cancelled);
435 }
436
437 pub fn mark_terminated_by_config(&mut self) {
438 self.terminal_reason
439 .get_or_insert(SolverTerminalReason::TerminatedByConfig);
440 }
441
442 pub fn stats(&self) -> &SolverStats {
443 &self.stats
444 }
445
446 pub fn stats_mut(&mut self) -> &mut SolverStats {
447 &mut self.stats
448 }
449
450 fn progress_state(&self) -> SolverLifecycleState {
451 self.runtime
452 .map(|runtime| {
453 if runtime.is_terminal() {
454 SolverLifecycleState::Completed
455 } else {
456 SolverLifecycleState::Solving
457 }
458 })
459 .unwrap_or(SolverLifecycleState::Solving)
460 }
461
462 fn settle_pause_if_requested(&mut self) {
463 if let Some(runtime) = self.runtime {
464 runtime.pause_if_requested(self);
465 }
466 }
467
468 fn time_limit_reached(&self) -> bool {
469 self.time_limit
470 .zip(self.elapsed())
471 .is_some_and(|(limit, elapsed)| elapsed >= limit)
472 }
473}