1const PROGRESS_INTERVAL: Duration = Duration::from_secs(1);
2
3#[derive(Debug, Clone, Copy)]
4pub(crate) struct ProgressTick {
5 pub elapsed: Duration,
6 pub step_delta: u64,
7 pub move_delta: u64,
8}
9
10#[derive(Debug, Clone, Copy)]
11struct ProgressPulse {
12 phase_index: usize,
13 phase_type: &'static str,
14 initial_report_pending: bool,
15 next_deadline: Instant,
16 last_reported_at: Instant,
17 last_step_count: u64,
18 last_move_count: u64,
19}
20
21impl ProgressPulse {
22 fn new(
23 now: Instant,
24 phase_index: usize,
25 phase_type: &'static str,
26 step_count: u64,
27 move_count: u64,
28 ) -> Self {
29 Self {
30 phase_index,
31 phase_type,
32 initial_report_pending: true,
33 next_deadline: now + PROGRESS_INTERVAL,
34 last_reported_at: now,
35 last_step_count: step_count,
36 last_move_count: move_count,
37 }
38 }
39
40 fn take_due(
41 &mut self,
42 now: Instant,
43 phase_index: usize,
44 phase_type: &'static str,
45 step_count: u64,
46 move_count: u64,
47 ) -> Option<ProgressTick> {
48 if self.phase_index != phase_index || self.phase_type != phase_type {
49 *self = Self::new(now, phase_index, phase_type, step_count, move_count);
50 return None;
51 }
52 let has_new_work = step_count > self.last_step_count || move_count > self.last_move_count;
53 let is_initial_report = self.initial_report_pending && has_new_work;
54 if !is_initial_report && now < self.next_deadline {
55 return None;
56 }
57 let tick = ProgressTick {
58 elapsed: now.duration_since(self.last_reported_at),
59 step_delta: step_count.saturating_sub(self.last_step_count),
60 move_delta: move_count.saturating_sub(self.last_move_count),
61 };
62 self.last_reported_at = now;
63 self.last_step_count = step_count;
64 self.last_move_count = move_count;
65 if is_initial_report {
66 self.initial_report_pending = false;
67 self.next_deadline = now + PROGRESS_INTERVAL;
68 } else {
69 while self.next_deadline <= now {
70 self.next_deadline += PROGRESS_INTERVAL;
71 }
72 }
73 Some(tick)
74 }
75}
76
77pub struct SolverScope<'t, S: PlanningSolution, D: Director<S>, ProgressCb = ()> {
78 score_director: D,
79 best_solution: Option<S>,
80 current_score: Option<S::Score>,
81 best_score: Option<S::Score>,
82 rng: StdRng,
83 start_time: Option<Instant>,
84 paused_at: Option<Instant>,
85 total_step_count: u64,
86 terminate: Option<&'t AtomicBool>,
87 runtime: Option<SolverRuntime<S>>,
88 publication: Publication,
89 best_solution_publication_enabled: bool,
90 yielded_to_parent: bool,
91 environment_mode: EnvironmentMode,
92 stats: SolverStats,
93 time_limit: Option<Duration>,
94 time_deadline: Option<Instant>,
95 progress_callback: ProgressCb,
96 progress_pulse: Option<ProgressPulse>,
97 terminal_reason: Option<SolverTerminalReason>,
98 last_best_elapsed: Option<Duration>,
99 best_solution_revision: Option<u64>,
100 solution_revision: u64,
101 construction_frontier: ConstructionFrontier,
102 phase_budget: Option<&'t PhaseBudget>,
103 pub inphase_step_count_limit: Option<u64>,
104 pub inphase_move_count_limit: Option<u64>,
105 pub inphase_score_calc_count_limit: Option<u64>,
106 inphase_best_score_limit: Option<S::Score>,
107 phase_termination: Option<ScopedPhaseTermination<S>>,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum Publication {
112 Enabled,
113 Disabled,
114}
115
116pub(crate) struct PhaseBudget {
117 step_count_limit: Option<u64>,
118 move_count_limit: Option<u64>,
119 score_calc_count_limit: Option<u64>,
120 step_count: AtomicU64,
121 moves_evaluated: AtomicU64,
122 score_calculations: AtomicU64,
123}
124
125impl PhaseBudget {
126 fn from_scope<S, D, ProgressCb>(scope: &SolverScope<'_, S, D, ProgressCb>) -> Self
127 where
128 S: PlanningSolution,
129 D: Director<S>,
130 ProgressCb: ProgressCallback<S>,
131 {
132 Self {
133 step_count_limit: remaining_limit(
134 scope.inphase_step_count_limit,
135 scope.total_step_count,
136 ),
137 move_count_limit: remaining_limit(
138 scope.inphase_move_count_limit,
139 scope.stats.moves_evaluated,
140 ),
141 score_calc_count_limit: remaining_limit(
142 scope.inphase_score_calc_count_limit,
143 scope.stats.score_calculations,
144 ),
145 step_count: AtomicU64::new(0),
146 moves_evaluated: AtomicU64::new(0),
147 score_calculations: AtomicU64::new(0),
148 }
149 }
150
151 fn has_limits(&self) -> bool {
152 self.step_count_limit.is_some()
153 || self.move_count_limit.is_some()
154 || self.score_calc_count_limit.is_some()
155 }
156
157 fn record_step(&self) {
158 self.step_count.fetch_add(1, Ordering::SeqCst);
159 }
160
161 fn record_evaluated_move(&self) {
162 self.moves_evaluated.fetch_add(1, Ordering::SeqCst);
163 }
164
165 fn record_score_calculation(&self) {
166 self.score_calculations.fetch_add(1, Ordering::SeqCst);
167 }
168
169 fn limit_reached(&self) -> bool {
170 limit_reached(self.step_count_limit, self.step_count.load(Ordering::SeqCst))
171 || limit_reached(
172 self.move_count_limit,
173 self.moves_evaluated.load(Ordering::SeqCst),
174 )
175 || limit_reached(
176 self.score_calc_count_limit,
177 self.score_calculations.load(Ordering::SeqCst),
178 )
179 }
180}
181
182#[derive(Clone, Copy)]
183struct ScopedPhaseTermination<S: PlanningSolution> {
184 start_step_count: u64,
185 start_elapsed: Duration,
186 time_limit: Option<Duration>,
187 step_count_limit: Option<u64>,
188 best_score_limit: Option<S::Score>,
189 unimproved_step_count_limit: Option<u64>,
190 unimproved_time_limit: Option<Duration>,
191 best_score: Option<S::Score>,
192 improvement_score: Option<S::Score>,
193 last_improvement_step_count: u64,
194 last_improvement_elapsed: Duration,
195}
196
197impl<S> ScopedPhaseTermination<S>
198where
199 S: PlanningSolution,
200{
201 fn from_config<D, ProgressCb>(
202 scope: &SolverScope<'_, S, D, ProgressCb>,
203 config: &TerminationConfig,
204 ) -> Option<Self>
205 where
206 D: Director<S>,
207 ProgressCb: ProgressCallback<S>,
208 S::Score: ParseableScore,
209 {
210 let best_score_limit = config
211 .best_score_limit
212 .as_deref()
213 .and_then(|score| S::Score::parse(score).ok());
214 let time_limit = config.time_limit();
215 let step_count_limit = config.step_count_limit;
216 let unimproved_step_count_limit = config.unimproved_step_count_limit;
217 let unimproved_time_limit = config.unimproved_time_limit();
218 if time_limit.is_none()
219 && step_count_limit.is_none()
220 && best_score_limit.is_none()
221 && unimproved_step_count_limit.is_none()
222 && unimproved_time_limit.is_none()
223 {
224 return None;
225 }
226 let elapsed = scope.elapsed().unwrap_or_default();
227 let best_score = match (scope.best_score, scope.current_score) {
228 (Some(best), Some(current)) => Some(best.max(current)),
229 (Some(best), None) | (None, Some(best)) => Some(best),
230 (None, None) => None,
231 };
232 Some(Self {
233 start_step_count: scope.total_step_count,
234 start_elapsed: elapsed,
235 time_limit,
236 step_count_limit,
237 best_score_limit,
238 unimproved_step_count_limit,
239 unimproved_time_limit,
240 best_score,
241 improvement_score: scope.current_score.or(scope.best_score),
242 last_improvement_step_count: scope.total_step_count,
243 last_improvement_elapsed: elapsed,
244 })
245 }
246
247 fn needs_clock(&self) -> bool {
248 self.time_limit.is_some() || self.unimproved_time_limit.is_some()
249 }
250
251 fn is_reached(
252 &self,
253 total_step_count: u64,
254 elapsed: Duration,
255 ) -> bool {
256 let phase_steps = total_step_count.saturating_sub(self.start_step_count);
257 let phase_elapsed = elapsed.saturating_sub(self.start_elapsed);
258 self.time_limit.is_some_and(|limit| phase_elapsed >= limit)
259 || self.step_count_limit.is_some_and(|limit| phase_steps >= limit)
260 || self
261 .best_score_limit
262 .is_some_and(|limit| self.best_score.is_some_and(|score| score >= limit))
263 || self.unimproved_step_count_limit.is_some_and(|limit| {
264 total_step_count.saturating_sub(self.last_improvement_step_count) >= limit
265 })
266 || self.unimproved_time_limit.is_some_and(|limit| {
267 elapsed.saturating_sub(self.last_improvement_elapsed) >= limit
268 })
269 }
270
271 fn record_improvement(&mut self, total_step_count: u64, elapsed: Duration) {
272 self.last_improvement_step_count = total_step_count;
273 self.last_improvement_elapsed = elapsed;
274 }
275
276 fn observe_score(
277 &mut self,
278 score: S::Score,
279 completed_step_count: u64,
280 elapsed: Duration,
281 ) {
282 if self.best_score.is_none_or(|best| score > best) {
283 self.best_score = Some(score);
284 }
285 if self.improvement_score.is_none_or(|best| score > best) {
286 self.improvement_score = Some(score);
287 self.record_improvement(completed_step_count, elapsed);
288 }
289 }
290
291 fn needs_score_observation(&self) -> bool {
292 self.best_score_limit.is_some()
293 || self.unimproved_step_count_limit.is_some()
294 || self.unimproved_time_limit.is_some()
295 }
296}
297
298fn remaining_limit(limit: Option<u64>, used: u64) -> Option<u64> {
299 limit.map(|limit| limit.saturating_sub(used))
300}
301
302fn limit_reached(limit: Option<u64>, used: u64) -> bool {
303 limit.is_some_and(|limit| used >= limit)
304}
305
306#[derive(Clone, Copy)]
307pub(crate) struct SolverScopeChildConfig<'t, S: PlanningSolution> {
308 terminate: Option<&'t AtomicBool>,
309 runtime: Option<SolverRuntime<S>>,
310 environment_mode: EnvironmentMode,
311 time_deadline: Option<Instant>,
312 phase_budget: Option<&'t PhaseBudget>,
313 inphase_step_count_limit: Option<u64>,
314 inphase_move_count_limit: Option<u64>,
315 inphase_score_calc_count_limit: Option<u64>,
316 inphase_best_score_limit: Option<S::Score>,
317}
318
319impl<'t, S: PlanningSolution> SolverScopeChildConfig<'t, S> {
320 pub(crate) fn build_scope<PD>(&self, score_director: PD, seed: u64) -> SolverScope<'t, S, PD>
321 where
322 PD: Director<S>,
323 {
324 let terminate = self
325 .terminate
326 .or_else(|| self.runtime.map(|runtime| runtime.cancel_flag()));
327 let mut scope = SolverScope::new(score_director)
328 .with_terminate(terminate)
329 .with_runtime(self.runtime)
330 .without_publication()
331 .with_environment_mode(self.environment_mode)
332 .with_seed(seed);
333 scope.time_deadline = self.time_deadline;
334 scope.phase_budget = self.phase_budget;
335 if self.phase_budget.is_none() {
336 scope.inphase_step_count_limit = self.inphase_step_count_limit;
337 scope.inphase_move_count_limit = self.inphase_move_count_limit;
338 scope.inphase_score_calc_count_limit = self.inphase_score_calc_count_limit;
339 }
340 scope.inphase_best_score_limit = self.inphase_best_score_limit;
341 scope
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub(crate) enum PendingControl {
347 Continue,
348 PauseRequested,
349 CancelRequested,
350 ConfigTerminationRequested,
351}
352
353impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
354 pub fn new(score_director: D) -> Self {
355 let construction_frontier = ConstructionFrontier::new();
356 Self {
357 score_director,
358 best_solution: None,
359 current_score: None,
360 best_score: None,
361 rng: StdRng::from_rng(&mut rand::rng()),
362 start_time: None,
363 paused_at: None,
364 total_step_count: 0,
365 terminate: None,
366 runtime: None,
367 publication: Publication::Enabled,
368 best_solution_publication_enabled: true,
369 yielded_to_parent: false,
370 environment_mode: EnvironmentMode::default(),
371 stats: SolverStats::default(),
372 time_limit: None,
373 time_deadline: None,
374 progress_callback: (),
375 progress_pulse: None,
376 terminal_reason: None,
377 last_best_elapsed: None,
378 best_solution_revision: None,
379 solution_revision: 1,
380 construction_frontier,
381 phase_budget: None,
382 inphase_step_count_limit: None,
383 inphase_move_count_limit: None,
384 inphase_score_calc_count_limit: None,
385 inphase_best_score_limit: None,
386 phase_termination: None,
387 }
388 }
389}
390
391impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
392 SolverScope<'t, S, D, ProgressCb>
393{
394 pub fn new_with_callback(
395 score_director: D,
396 callback: ProgressCb,
397 terminate: Option<&'t AtomicBool>,
398 runtime: Option<SolverRuntime<S>>,
399 ) -> Self {
400 let construction_frontier = ConstructionFrontier::new();
401 Self {
402 score_director,
403 best_solution: None,
404 current_score: None,
405 best_score: None,
406 rng: StdRng::from_rng(&mut rand::rng()),
407 start_time: None,
408 paused_at: None,
409 total_step_count: 0,
410 terminate,
411 runtime,
412 publication: Publication::Enabled,
413 best_solution_publication_enabled: true,
414 yielded_to_parent: false,
415 environment_mode: EnvironmentMode::default(),
416 stats: SolverStats::default(),
417 time_limit: None,
418 time_deadline: None,
419 progress_callback: callback,
420 progress_pulse: None,
421 terminal_reason: None,
422 last_best_elapsed: None,
423 best_solution_revision: None,
424 solution_revision: 1,
425 construction_frontier,
426 phase_budget: None,
427 inphase_step_count_limit: None,
428 inphase_move_count_limit: None,
429 inphase_score_calc_count_limit: None,
430 inphase_best_score_limit: None,
431 phase_termination: None,
432 }
433 }
434
435 pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
436 self.terminate = terminate;
437 self
438 }
439
440 pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
441 self.runtime = runtime;
442 self
443 }
444
445 pub(crate) fn without_publication(mut self) -> Self {
446 self.publication = Publication::Disabled;
447 self
448 }
449
450 pub(crate) fn defer_best_solution_publication(&mut self) {
451 self.best_solution_publication_enabled = false;
452 }
453
454 pub(crate) fn best_solution_publication_enabled(&self) -> bool {
455 self.best_solution_publication_enabled
456 }
457
458 pub(crate) fn yielded_to_parent(&self) -> bool {
459 self.yielded_to_parent
460 }
461
462 pub fn with_environment_mode(mut self, environment_mode: EnvironmentMode) -> Self {
463 self.environment_mode = environment_mode;
464 self
465 }
466
467 pub fn with_seed(mut self, seed: u64) -> Self {
468 self.rng = StdRng::seed_from_u64(seed);
469 self
470 }
471
472 pub(crate) fn child_phase_budget(&self) -> PhaseBudget {
473 PhaseBudget::from_scope(self)
474 }
475
476 pub(crate) fn child_config<'a>(
477 &'a self,
478 phase_budget: Option<&'a PhaseBudget>,
479 ) -> SolverScopeChildConfig<'a, S> {
480 let phase_budget = self
481 .phase_budget
482 .or_else(|| phase_budget.filter(|budget| budget.has_limits()));
483 SolverScopeChildConfig {
484 terminate: self.terminate,
485 runtime: self.runtime,
486 environment_mode: self.environment_mode,
487 time_deadline: self.child_time_deadline(),
488 phase_budget,
489 inphase_step_count_limit: self.inphase_step_count_limit,
490 inphase_move_count_limit: self.inphase_move_count_limit,
491 inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
492 inphase_best_score_limit: self.inphase_best_score_limit,
493 }
494 }
495
496 pub(crate) fn with_phase_termination<T>(
505 &mut self,
506 config: Option<&TerminationConfig>,
507 work: impl FnOnce(&mut Self) -> T,
508 ) -> T
509 where
510 S::Score: ParseableScore,
511 {
512 let previous = self.phase_termination.take();
513 self.phase_termination =
514 config.and_then(|config| ScopedPhaseTermination::from_config(self, config));
515 let result = work(self);
516 self.phase_termination = previous;
517 result
518 }
519
520 fn phase_termination_reached(&self) -> bool {
521 self.phase_termination.as_ref().is_some_and(|termination| {
522 let elapsed = if termination.needs_clock() {
526 self.elapsed().unwrap_or_default()
527 } else {
528 Duration::ZERO
529 };
530 termination.is_reached(self.total_step_count, elapsed)
531 })
532 }
533
534 pub(crate) fn phase_termination_requires_score_observation(&self) -> bool {
535 self.phase_termination
536 .as_ref()
537 .is_some_and(ScopedPhaseTermination::needs_score_observation)
538 }
539
540 fn observe_phase_score(&mut self, score: S::Score, completed_step_count: u64) {
541 let elapsed = self.elapsed().unwrap_or_default();
542 if let Some(termination) = &mut self.phase_termination {
543 termination.observe_score(score, completed_step_count, elapsed);
544 }
545 }
546
547 pub(crate) fn observe_phase_step_score(&mut self, score: S::Score) {
548 self.observe_phase_score(score, self.total_step_count.saturating_add(1));
549 }
550
551 fn child_time_deadline(&self) -> Option<Instant> {
552 self.time_deadline.or_else(|| {
553 self.time_limit.map(|limit| {
554 self.start_time
555 .map(|start| start + limit)
556 .unwrap_or_else(|| Instant::now() + limit)
557 })
558 })
559 }
560
561 pub fn with_progress_callback<F: ProgressCallback<S>>(
562 self,
563 callback: F,
564 ) -> SolverScope<'t, S, D, F> {
565 SolverScope {
566 score_director: self.score_director,
567 best_solution: self.best_solution,
568 current_score: self.current_score,
569 best_score: self.best_score,
570 rng: self.rng,
571 start_time: self.start_time,
572 paused_at: self.paused_at,
573 total_step_count: self.total_step_count,
574 terminate: self.terminate,
575 runtime: self.runtime,
576 publication: self.publication,
577 best_solution_publication_enabled: self.best_solution_publication_enabled,
578 yielded_to_parent: self.yielded_to_parent,
579 environment_mode: self.environment_mode,
580 stats: self.stats,
581 time_limit: self.time_limit,
582 time_deadline: self.time_deadline,
583 progress_callback: callback,
584 progress_pulse: self.progress_pulse,
585 terminal_reason: self.terminal_reason,
586 last_best_elapsed: self.last_best_elapsed,
587 best_solution_revision: self.best_solution_revision,
588 solution_revision: self.solution_revision,
589 construction_frontier: self.construction_frontier,
590 phase_budget: self.phase_budget,
591 inphase_step_count_limit: self.inphase_step_count_limit,
592 inphase_move_count_limit: self.inphase_move_count_limit,
593 inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
594 inphase_best_score_limit: self.inphase_best_score_limit,
595 phase_termination: self.phase_termination,
596 }
597 }
598
599 pub fn start_solving(&mut self) {
600 self.start_time = Some(Instant::now());
601 self.paused_at = None;
602 self.total_step_count = 0;
603 self.terminal_reason = None;
604 self.last_best_elapsed = None;
605 self.yielded_to_parent = false;
606 self.best_solution_revision = None;
607 self.solution_revision = 1;
608 self.progress_pulse = None;
609 self.construction_frontier.reset();
610 self.stats.start();
611 }
612
613 pub fn elapsed(&self) -> Option<Duration> {
614 match (self.start_time, self.paused_at) {
615 (Some(start), Some(paused_at)) => Some(paused_at.duration_since(start)),
616 (Some(start), None) => Some(start.elapsed()),
617 _ => None,
618 }
619 }
620
621 pub fn time_since_last_improvement(&self) -> Option<Duration> {
622 let elapsed = self.elapsed()?;
623 let last_best_elapsed = self.last_best_elapsed?;
624 Some(elapsed.saturating_sub(last_best_elapsed))
625 }
626
627 pub fn score_director(&self) -> &D {
628 &self.score_director
629 }
630
631 pub(crate) fn score_director_mut(&mut self) -> &mut D {
632 &mut self.score_director
633 }
634
635 pub fn working_solution(&self) -> &S {
636 self.score_director.working_solution()
637 }
638
639 pub fn mutate<T, F>(&mut self, mutate: F) -> T
640 where
641 F: FnOnce(&mut D) -> T,
642 {
643 self.committed_mutation(mutate)
644 }
645
646 pub fn calculate_score(&mut self) -> S::Score {
647 self.record_score_calculation();
648 let score = self.score_director.calculate_score();
649 self.current_score = Some(score);
650 self.assert_score_consistent("calculate_score", score);
651 score
652 }
653
654 pub(crate) fn assert_score_consistent(&self, context: &str, score: S::Score) {
655 if self.environment_mode != EnvironmentMode::FullAssert {
656 return;
657 }
658 let Some(fresh_score) = self.score_director.fresh_score() else {
659 return;
660 };
661 assert_eq!(
662 score, fresh_score,
663 "score director drift after {context}: cached score {score:?} != fresh score {fresh_score:?}"
664 );
665 }
666
667 pub fn initialize_working_solution_as_best(&mut self) -> S::Score {
668 if self.start_time.is_none() {
669 self.start_solving();
670 }
671 let score = self.calculate_score();
672 let solution = self.score_director.clone_working_solution();
673 self.set_best_solution(solution, score);
674 score
675 }
676
677 pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
678 *self.score_director.working_solution_mut() = solution;
679 self.score_director.reset();
680 self.current_score = None;
681 self.best_solution_revision = None;
682 self.solution_revision = 1;
683 self.construction_frontier.reset();
684 self.calculate_score()
685 }
686
687 pub fn best_solution(&self) -> Option<&S> {
688 self.best_solution.as_ref()
689 }
690
691 pub fn best_score(&self) -> Option<&S::Score> {
692 self.best_score.as_ref()
693 }
694
695 pub fn current_score(&self) -> Option<&S::Score> {
696 self.current_score.as_ref()
697 }
698
699 pub(crate) fn is_scalar_slot_completed(&self, slot_id: ConstructionSlotId) -> bool {
700 self.construction_frontier
701 .is_scalar_completed(slot_id, self.solution_revision)
702 }
703
704 pub(crate) fn mark_scalar_slot_completed(&mut self, slot_id: ConstructionSlotId) {
705 self.construction_frontier
706 .mark_scalar_completed(slot_id, self.solution_revision);
707 }
708
709 pub(crate) fn is_group_slot_completed(&self, slot_id: &ConstructionGroupSlotId) -> bool {
710 self.construction_frontier
711 .is_group_completed(slot_id, self.solution_revision)
712 }
713
714 pub(crate) fn mark_group_slot_completed(&mut self, slot_id: ConstructionGroupSlotId) {
715 self.construction_frontier
716 .mark_group_completed(slot_id, self.solution_revision);
717 }
718
719 pub(crate) fn is_list_element_completed(&self, element_id: ConstructionListElementId) -> bool {
720 self.construction_frontier
721 .is_list_completed(element_id, self.solution_revision)
722 }
723
724 pub(crate) fn mark_list_element_completed(&mut self, element_id: ConstructionListElementId) {
725 self.construction_frontier
726 .mark_list_completed(element_id, self.solution_revision);
727 }
728
729 #[cfg(test)]
730 pub(crate) fn solution_revision(&self) -> u64 {
731 self.solution_revision
732 }
733
734 pub(crate) fn apply_committed_move<M>(&mut self, mov: &M)
735 where
736 M: Move<S>,
737 {
738 self.committed_mutation(|score_director| mov.do_move(score_director));
739 }
740
741 pub(crate) fn apply_committed_change<F>(&mut self, change: F)
742 where
743 F: FnOnce(&mut D),
744 {
745 self.committed_mutation(change);
746 }
747
748}