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, RecordingDirector};
11
12use crate::heuristic::r#move::Move;
13use crate::manager::{SolverLifecycleState, SolverRuntime, SolverTerminalReason};
14use crate::phase::construction::{
15 ConstructionFrontier, ConstructionListElementId, ConstructionSlotId,
16};
17use crate::stats::SolverStats;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SolverProgressKind {
21 Progress,
22 BestSolution,
23}
24
25#[derive(Debug, Clone, Copy)]
26pub struct SolverProgressRef<'a, S: PlanningSolution> {
27 pub kind: SolverProgressKind,
28 pub status: SolverLifecycleState,
29 pub solution: Option<&'a S>,
30 pub current_score: Option<&'a S::Score>,
31 pub best_score: Option<&'a S::Score>,
32 pub telemetry: crate::stats::SolverTelemetry,
33}
34
35pub trait ProgressCallback<S: PlanningSolution>: Send + Sync {
36 fn invoke(&self, progress: SolverProgressRef<'_, S>);
37}
38
39impl<S: PlanningSolution> ProgressCallback<S> for () {
40 fn invoke(&self, _progress: SolverProgressRef<'_, S>) {}
41}
42
43impl<S, F> ProgressCallback<S> for F
44where
45 S: PlanningSolution,
46 F: for<'a> Fn(SolverProgressRef<'a, S>) + Send + Sync,
47{
48 fn invoke(&self, progress: SolverProgressRef<'_, S>) {
49 self(progress);
50 }
51}
52
53pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
54 score_director: D,
55 best_solution: Option<S>,
56 current_score: Option<S::Score>,
57 best_score: Option<S::Score>,
58 rng: StdRng,
59 start_time: Option<Instant>,
60 paused_at: Option<Instant>,
61 total_step_count: u64,
62 terminate: Option<&'t AtomicBool>,
63 runtime: Option<SolverRuntime<S>>,
64 stats: SolverStats,
65 time_limit: Option<Duration>,
66 progress_callback: ProgressCb,
67 terminal_reason: Option<SolverTerminalReason>,
68 last_best_elapsed: Option<Duration>,
69 best_solution_revision: Option<u64>,
70 solution_revision: u64,
71 construction_frontier: ConstructionFrontier,
72 pub inphase_step_count_limit: Option<u64>,
73 pub inphase_move_count_limit: Option<u64>,
74 pub inphase_score_calc_count_limit: Option<u64>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) enum PendingControl {
79 Continue,
80 PauseRequested,
81 CancelRequested,
82 ConfigTerminationRequested,
83}
84
85impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
86 pub fn new(score_director: D) -> Self {
87 let construction_frontier = ConstructionFrontier::new();
88 Self {
89 score_director,
90 best_solution: None,
91 current_score: None,
92 best_score: None,
93 rng: StdRng::from_rng(&mut rand::rng()),
94 start_time: None,
95 paused_at: None,
96 total_step_count: 0,
97 terminate: None,
98 runtime: None,
99 stats: SolverStats::default(),
100 time_limit: None,
101 progress_callback: (),
102 terminal_reason: None,
103 last_best_elapsed: None,
104 best_solution_revision: None,
105 solution_revision: 1,
106 construction_frontier,
107 inphase_step_count_limit: None,
108 inphase_move_count_limit: None,
109 inphase_score_calc_count_limit: None,
110 }
111 }
112}
113
114impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
115 SolverScope<'t, S, D, ProgressCb>
116{
117 pub fn new_with_callback(
118 score_director: D,
119 callback: ProgressCb,
120 terminate: Option<&'t AtomicBool>,
121 runtime: Option<SolverRuntime<S>>,
122 ) -> Self {
123 let construction_frontier = ConstructionFrontier::new();
124 Self {
125 score_director,
126 best_solution: None,
127 current_score: None,
128 best_score: None,
129 rng: StdRng::from_rng(&mut rand::rng()),
130 start_time: None,
131 paused_at: None,
132 total_step_count: 0,
133 terminate,
134 runtime,
135 stats: SolverStats::default(),
136 time_limit: None,
137 progress_callback: callback,
138 terminal_reason: None,
139 last_best_elapsed: None,
140 best_solution_revision: None,
141 solution_revision: 1,
142 construction_frontier,
143 inphase_step_count_limit: None,
144 inphase_move_count_limit: None,
145 inphase_score_calc_count_limit: None,
146 }
147 }
148
149 pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
150 self.terminate = terminate;
151 self
152 }
153
154 pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
155 self.runtime = runtime;
156 self
157 }
158
159 pub fn with_seed(mut self, seed: u64) -> Self {
160 self.rng = StdRng::seed_from_u64(seed);
161 self
162 }
163
164 pub fn with_progress_callback<F: ProgressCallback<S>>(
165 self,
166 callback: F,
167 ) -> SolverScope<'t, S, D, F> {
168 SolverScope {
169 score_director: self.score_director,
170 best_solution: self.best_solution,
171 current_score: self.current_score,
172 best_score: self.best_score,
173 rng: self.rng,
174 start_time: self.start_time,
175 paused_at: self.paused_at,
176 total_step_count: self.total_step_count,
177 terminate: self.terminate,
178 runtime: self.runtime,
179 stats: self.stats,
180 time_limit: self.time_limit,
181 progress_callback: callback,
182 terminal_reason: self.terminal_reason,
183 last_best_elapsed: self.last_best_elapsed,
184 best_solution_revision: self.best_solution_revision,
185 solution_revision: self.solution_revision,
186 construction_frontier: self.construction_frontier,
187 inphase_step_count_limit: self.inphase_step_count_limit,
188 inphase_move_count_limit: self.inphase_move_count_limit,
189 inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
190 }
191 }
192
193 pub fn start_solving(&mut self) {
194 self.start_time = Some(Instant::now());
195 self.paused_at = None;
196 self.total_step_count = 0;
197 self.terminal_reason = None;
198 self.last_best_elapsed = None;
199 self.best_solution_revision = None;
200 self.solution_revision = 1;
201 self.construction_frontier.reset();
202 self.stats.start();
203 }
204
205 pub fn elapsed(&self) -> Option<Duration> {
206 match (self.start_time, self.paused_at) {
207 (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
208 (Some(start), None) => Some(start.elapsed()),
209 _ => None,
210 }
211 }
212
213 pub fn time_since_last_improvement(&self) -> Option<Duration> {
214 let elapsed = self.elapsed()?;
215 let last_best_elapsed = self.last_best_elapsed?;
216 Some(elapsed.saturating_sub(last_best_elapsed))
217 }
218
219 pub fn score_director(&self) -> &D {
220 &self.score_director
221 }
222
223 pub(crate) fn score_director_mut(&mut self) -> &mut D {
224 &mut self.score_director
225 }
226
227 pub fn working_solution(&self) -> &S {
228 self.score_director.working_solution()
229 }
230
231 pub fn trial<T, F>(&mut self, trial: F) -> T
232 where
233 F: FnOnce(&mut RecordingDirector<'_, S, D>) -> T,
234 {
235 let mut recording = RecordingDirector::new(&mut self.score_director);
236 let output = trial(&mut recording);
237 recording.undo_changes();
238 output
239 }
240
241 pub fn mutate<T, F>(&mut self, mutate: F) -> T
242 where
243 F: FnOnce(&mut D) -> T,
244 {
245 self.committed_mutation(mutate)
246 }
247
248 pub fn calculate_score(&mut self) -> S::Score {
249 self.stats.record_score_calculation();
250 let score = self.score_director.calculate_score();
251 self.current_score = Some(score);
252 score
253 }
254
255 pub fn initialize_working_solution_as_best(&mut self) -> S::Score {
256 if self.start_time.is_none() {
257 self.start_solving();
258 }
259 let score = self.calculate_score();
260 let solution = self.score_director.clone_working_solution();
261 self.set_best_solution(solution, score);
262 score
263 }
264
265 pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
266 *self.score_director.working_solution_mut() = solution;
267 self.score_director.reset();
268 self.current_score = None;
269 self.best_solution_revision = None;
270 self.solution_revision = 1;
271 self.construction_frontier.reset();
272 self.calculate_score()
273 }
274
275 pub fn best_solution(&self) -> Option<&S> {
276 self.best_solution.as_ref()
277 }
278
279 pub fn best_score(&self) -> Option<&S::Score> {
280 self.best_score.as_ref()
281 }
282
283 pub fn current_score(&self) -> Option<&S::Score> {
284 self.current_score.as_ref()
285 }
286
287 pub(crate) fn is_standard_slot_completed(&self, slot_id: ConstructionSlotId) -> bool {
288 self.construction_frontier
289 .is_standard_completed(slot_id, self.solution_revision)
290 }
291
292 pub(crate) fn mark_standard_slot_completed(&mut self, slot_id: ConstructionSlotId) {
293 self.construction_frontier
294 .mark_standard_completed(slot_id, self.solution_revision);
295 }
296
297 pub(crate) fn is_list_element_completed(&self, element_id: ConstructionListElementId) -> bool {
298 self.construction_frontier
299 .is_list_completed(element_id, self.solution_revision)
300 }
301
302 pub(crate) fn mark_list_element_completed(&mut self, element_id: ConstructionListElementId) {
303 self.construction_frontier
304 .mark_list_completed(element_id, self.solution_revision);
305 }
306
307 pub(crate) fn solution_revision(&self) -> u64 {
308 self.solution_revision
309 }
310
311 pub(crate) fn apply_committed_move<M>(&mut self, mov: &M)
312 where
313 M: Move<S>,
314 {
315 self.committed_mutation(|score_director| mov.do_move(score_director));
316 }
317
318 pub(crate) fn apply_committed_change<F>(&mut self, change: F)
319 where
320 F: FnOnce(&mut D),
321 {
322 self.committed_mutation(change);
323 }
324
325 pub(crate) fn construction_frontier(&self) -> &ConstructionFrontier {
326 &self.construction_frontier
327 }
328
329 pub fn terminal_reason(&self) -> SolverTerminalReason {
330 self.terminal_reason
331 .unwrap_or(SolverTerminalReason::Completed)
332 }
333
334 pub fn set_current_score(&mut self, score: S::Score) {
335 self.current_score = Some(score);
336 }
337
338 pub fn report_progress(&self) {
339 self.progress_callback.invoke(SolverProgressRef {
340 kind: SolverProgressKind::Progress,
341 status: self.progress_state(),
342 solution: None,
343 current_score: self.current_score.as_ref(),
344 best_score: self.best_score.as_ref(),
345 telemetry: self.stats.snapshot(),
346 });
347 }
348
349 pub fn report_best_solution(&self) {
350 self.progress_callback.invoke(SolverProgressRef {
351 kind: SolverProgressKind::BestSolution,
352 status: self.progress_state(),
353 solution: self.best_solution.as_ref(),
354 current_score: self.current_score.as_ref(),
355 best_score: self.best_score.as_ref(),
356 telemetry: self.stats.snapshot(),
357 });
358 }
359
360 pub fn update_best_solution(&mut self) {
361 let current_score = self.score_director.calculate_score();
362 self.current_score = Some(current_score);
363 let is_better = match &self.best_score {
364 None => true,
365 Some(best) => current_score > *best,
366 };
367
368 if is_better {
369 self.best_solution = Some(self.score_director.clone_working_solution());
370 self.best_score = Some(current_score);
371 self.last_best_elapsed = self.elapsed();
372 self.best_solution_revision = Some(self.solution_revision);
373 self.report_best_solution();
374 }
375 }
376
377 pub(crate) fn promote_current_solution_on_score_tie(&mut self) {
378 let Some(current_score) = self.current_score else {
379 return;
380 };
381 let Some(best_score) = self.best_score else {
382 return;
383 };
384
385 if current_score == best_score
386 && self.best_solution_revision != Some(self.solution_revision)
387 {
388 self.best_solution = Some(self.score_director.clone_working_solution());
389 self.best_solution_revision = Some(self.solution_revision);
390 self.report_best_solution();
391 }
392 }
393
394 pub fn set_best_solution(&mut self, solution: S, score: S::Score) {
395 if self.start_time.is_none() {
396 self.start_solving();
397 }
398 self.current_score = Some(score);
399 self.best_solution = Some(solution);
400 self.best_score = Some(score);
401 self.last_best_elapsed = self.elapsed();
402 self.best_solution_revision = Some(self.solution_revision);
403 }
404
405 pub fn rng(&mut self) -> &mut StdRng {
406 &mut self.rng
407 }
408
409 pub fn increment_step_count(&mut self) -> u64 {
410 self.total_step_count += 1;
411 self.stats.record_step();
412 self.total_step_count
413 }
414
415 pub fn total_step_count(&self) -> u64 {
416 self.total_step_count
417 }
418
419 pub fn take_best_solution(self) -> Option<S> {
420 self.best_solution
421 }
422
423 pub fn take_best_or_working_solution(self) -> S {
424 self.best_solution
425 .unwrap_or_else(|| self.score_director.clone_working_solution())
426 }
427
428 pub fn take_solution_and_stats(
429 self,
430 ) -> (
431 S,
432 Option<S::Score>,
433 S::Score,
434 SolverStats,
435 SolverTerminalReason,
436 ) {
437 let terminal_reason = self.terminal_reason();
438 let solution = self
439 .best_solution
440 .unwrap_or_else(|| self.score_director.clone_working_solution());
441 let best_score = self
442 .best_score
443 .or(self.current_score)
444 .expect("solver finished without a canonical score");
445 (
446 solution,
447 self.current_score,
448 best_score,
449 self.stats,
450 terminal_reason,
451 )
452 }
453
454 pub fn is_terminate_early(&self) -> bool {
455 self.terminate
456 .is_some_and(|flag| flag.load(Ordering::SeqCst))
457 || self
458 .runtime
459 .is_some_and(|runtime| runtime.is_cancel_requested())
460 }
461
462 pub(crate) fn pending_control(&self) -> PendingControl {
463 if self.is_terminate_early() {
464 return PendingControl::CancelRequested;
465 }
466 if self
467 .runtime
468 .is_some_and(|runtime| runtime.is_pause_requested())
469 {
470 return PendingControl::PauseRequested;
471 }
472 if self.time_limit_reached() {
473 return PendingControl::ConfigTerminationRequested;
474 }
475 PendingControl::Continue
476 }
477
478 pub fn set_time_limit(&mut self, limit: Duration) {
479 self.time_limit = Some(limit);
480 }
481
482 pub fn pause_if_requested(&mut self) {
483 self.settle_pause_if_requested();
484 }
485
486 pub fn pause_timers(&mut self) {
487 if self.paused_at.is_none() {
488 self.paused_at = Some(Instant::now());
489 self.stats.pause();
490 }
491 }
492
493 pub fn resume_timers(&mut self) {
494 if let Some(paused_at) = self.paused_at.take() {
495 let paused_for = paused_at.elapsed();
496 if let Some(start) = self.start_time {
497 self.start_time = Some(start + paused_for);
498 }
499 self.stats.resume();
500 }
501 }
502
503 pub fn should_terminate_construction(&mut self) -> bool {
504 self.settle_pause_if_requested();
505 if self.is_terminate_early() {
506 self.mark_cancelled();
507 return true;
508 }
509 if self.time_limit_reached() {
510 self.mark_terminated_by_config();
511 return true;
512 }
513 false
514 }
515
516 pub fn should_terminate(&mut self) -> bool {
517 self.settle_pause_if_requested();
518 if self.is_terminate_early() {
519 self.mark_cancelled();
520 return true;
521 }
522 if self.time_limit_reached() {
523 self.mark_terminated_by_config();
524 return true;
525 }
526 if let Some(limit) = self.inphase_step_count_limit {
527 if self.total_step_count >= limit {
528 self.mark_terminated_by_config();
529 return true;
530 }
531 }
532 if let Some(limit) = self.inphase_move_count_limit {
533 if self.stats.moves_evaluated >= limit {
534 self.mark_terminated_by_config();
535 return true;
536 }
537 }
538 if let Some(limit) = self.inphase_score_calc_count_limit {
539 if self.stats.score_calculations >= limit {
540 self.mark_terminated_by_config();
541 return true;
542 }
543 }
544 false
545 }
546
547 pub fn mark_cancelled(&mut self) {
548 self.terminal_reason
549 .get_or_insert(SolverTerminalReason::Cancelled);
550 }
551
552 pub fn mark_terminated_by_config(&mut self) {
553 self.terminal_reason
554 .get_or_insert(SolverTerminalReason::TerminatedByConfig);
555 }
556
557 pub fn stats(&self) -> &SolverStats {
558 &self.stats
559 }
560
561 pub fn stats_mut(&mut self) -> &mut SolverStats {
562 &mut self.stats
563 }
564
565 fn progress_state(&self) -> SolverLifecycleState {
566 self.runtime
567 .map(|runtime| {
568 if runtime.is_terminal() {
569 SolverLifecycleState::Completed
570 } else {
571 SolverLifecycleState::Solving
572 }
573 })
574 .unwrap_or(SolverLifecycleState::Solving)
575 }
576
577 fn settle_pause_if_requested(&mut self) {
578 if let Some(runtime) = self.runtime {
579 runtime.pause_if_requested(self);
580 }
581 }
582
583 fn time_limit_reached(&self) -> bool {
584 self.time_limit
585 .zip(self.elapsed())
586 .is_some_and(|(limit, elapsed)| elapsed >= limit)
587 }
588
589 fn advance_solution_revision(&mut self) {
590 self.solution_revision = self.solution_revision.wrapping_add(1);
591 if self.solution_revision == 0 {
592 self.solution_revision = 1;
593 self.construction_frontier.reset();
594 }
595 }
596
597 fn committed_mutation<T, F>(&mut self, mutate: F) -> T
598 where
599 F: FnOnce(&mut D) -> T,
600 {
601 self.current_score = None;
602 let output = mutate(&mut self.score_director);
603 self.advance_solution_revision();
604 output
605 }
606}