Skip to main content

solverforge_solver/scope/solver/
scope_core.rs

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    total_step_count: u64,
84    terminate: Option<&'t AtomicBool>,
85    runtime: Option<SolverRuntime<S>>,
86    publication: Publication,
87    best_solution_publication_enabled: bool,
88    yielded_to_parent: bool,
89    environment_mode: EnvironmentMode,
90    stats: SolverStats,
91    time_limit: Option<Duration>,
92    time_deadline: Option<Instant>,
93    progress_callback: ProgressCb,
94    progress_pulse: Option<ProgressPulse>,
95    terminal_reason: Option<SolverTerminalReason>,
96    last_best_elapsed: Option<Duration>,
97    best_solution_revision: Option<u64>,
98    solution_revision: u64,
99    /* Monotonic count of working-solution writes: every committed mutation
100    and every wholesale replacement. Unlike `solution_revision`, it never
101    resets, so evaluator replicas can detect any write since they synced.
102    */
103    working_solution_generation: u64,
104    construction_frontier: ConstructionFrontier,
105    phase_budget: Option<&'t PhaseBudget>,
106    pub inphase_step_count_limit: Option<u64>,
107    pub inphase_move_count_limit: Option<u64>,
108    pub inphase_score_calc_count_limit: Option<u64>,
109    inphase_best_score_limit: Option<S::Score>,
110    phase_termination: Option<ScopedPhaseTermination<S>>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114enum Publication {
115    Enabled,
116    Disabled,
117}
118
119pub(crate) struct PhaseBudget {
120    step_count_limit: Option<u64>,
121    move_count_limit: Option<u64>,
122    score_calc_count_limit: Option<u64>,
123    step_count: AtomicU64,
124    moves_evaluated: AtomicU64,
125    score_calculations: AtomicU64,
126}
127
128impl PhaseBudget {
129    fn from_scope<S, D, ProgressCb>(scope: &SolverScope<'_, S, D, ProgressCb>) -> Self
130    where
131        S: PlanningSolution,
132        D: Director<S>,
133        ProgressCb: ProgressCallback<S>,
134    {
135        Self {
136            step_count_limit: remaining_limit(
137                scope.inphase_step_count_limit,
138                scope.total_step_count,
139            ),
140            move_count_limit: remaining_limit(
141                scope.inphase_move_count_limit,
142                scope.stats.moves_evaluated(),
143            ),
144            score_calc_count_limit: remaining_limit(
145                scope.inphase_score_calc_count_limit,
146                scope.stats.score_calculations(),
147            ),
148            step_count: AtomicU64::new(0),
149            moves_evaluated: AtomicU64::new(0),
150            score_calculations: AtomicU64::new(0),
151        }
152    }
153
154    fn has_limits(&self) -> bool {
155        self.step_count_limit.is_some()
156            || self.move_count_limit.is_some()
157            || self.score_calc_count_limit.is_some()
158    }
159
160    fn record_step(&self) {
161        self.step_count.fetch_add(1, Ordering::SeqCst);
162    }
163
164    fn record_evaluated_move(&self) {
165        self.moves_evaluated.fetch_add(1, Ordering::SeqCst);
166    }
167
168    fn record_score_calculation(&self) {
169        self.score_calculations.fetch_add(1, Ordering::SeqCst);
170    }
171
172    fn limit_reached(&self) -> bool {
173        limit_reached(self.step_count_limit, self.step_count.load(Ordering::SeqCst))
174            || limit_reached(
175                self.move_count_limit,
176                self.moves_evaluated.load(Ordering::SeqCst),
177            )
178            || limit_reached(
179                self.score_calc_count_limit,
180                self.score_calculations.load(Ordering::SeqCst),
181            )
182    }
183}
184
185#[derive(Clone, Copy)]
186struct ScopedPhaseTermination<S: PlanningSolution> {
187    start_step_count: u64,
188    start_elapsed: Duration,
189    time_limit: Option<Duration>,
190    step_count_limit: Option<u64>,
191    best_score_limit: Option<S::Score>,
192    unimproved_step_count_limit: Option<u64>,
193    unimproved_time_limit: Option<Duration>,
194    best_score: Option<S::Score>,
195    improvement_score: Option<S::Score>,
196    last_improvement_step_count: u64,
197    last_improvement_elapsed: Duration,
198}
199
200impl<S> ScopedPhaseTermination<S>
201where
202    S: PlanningSolution,
203{
204    fn from_config<D, ProgressCb>(
205        scope: &SolverScope<'_, S, D, ProgressCb>,
206        config: &TerminationConfig,
207    ) -> Option<Self>
208    where
209        D: Director<S>,
210        ProgressCb: ProgressCallback<S>,
211        S::Score: ParseableScore,
212    {
213        let best_score_limit = config
214            .best_score_limit
215            .as_deref()
216            .and_then(|score| S::Score::parse(score).ok());
217        let time_limit = config.time_limit();
218        let step_count_limit = config.step_count_limit;
219        let unimproved_step_count_limit = config.unimproved_step_count_limit;
220        let unimproved_time_limit = config.unimproved_time_limit();
221        if time_limit.is_none()
222            && step_count_limit.is_none()
223            && best_score_limit.is_none()
224            && unimproved_step_count_limit.is_none()
225            && unimproved_time_limit.is_none()
226        {
227            return None;
228        }
229        let elapsed = scope.elapsed().unwrap_or_default();
230        let best_score = match (scope.best_score, scope.current_score) {
231            (Some(best), Some(current)) => Some(best.max(current)),
232            (Some(best), None) | (None, Some(best)) => Some(best),
233            (None, None) => None,
234        };
235        Some(Self {
236            start_step_count: scope.total_step_count,
237            start_elapsed: elapsed,
238            time_limit,
239            step_count_limit,
240            best_score_limit,
241            unimproved_step_count_limit,
242            unimproved_time_limit,
243            best_score,
244            improvement_score: scope.current_score.or(scope.best_score),
245            last_improvement_step_count: scope.total_step_count,
246            last_improvement_elapsed: elapsed,
247        })
248    }
249
250    fn needs_clock(&self) -> bool {
251        self.time_limit.is_some() || self.unimproved_time_limit.is_some()
252    }
253
254    fn is_reached(
255        &self,
256        total_step_count: u64,
257        elapsed: Duration,
258    ) -> bool {
259        let phase_steps = total_step_count.saturating_sub(self.start_step_count);
260        let phase_elapsed = elapsed.saturating_sub(self.start_elapsed);
261        self.time_limit.is_some_and(|limit| phase_elapsed >= limit)
262            || self.step_count_limit.is_some_and(|limit| phase_steps >= limit)
263            || self
264                .best_score_limit
265                .is_some_and(|limit| self.best_score.is_some_and(|score| score >= limit))
266            || self.unimproved_step_count_limit.is_some_and(|limit| {
267                total_step_count.saturating_sub(self.last_improvement_step_count) >= limit
268            })
269            || self.unimproved_time_limit.is_some_and(|limit| {
270                elapsed.saturating_sub(self.last_improvement_elapsed) >= limit
271            })
272    }
273
274    fn record_improvement(&mut self, total_step_count: u64, elapsed: Duration) {
275        self.last_improvement_step_count = total_step_count;
276        self.last_improvement_elapsed = elapsed;
277    }
278
279    fn observe_score(
280        &mut self,
281        score: S::Score,
282        completed_step_count: u64,
283        elapsed: Duration,
284    ) {
285        if self.best_score.is_none_or(|best| score > best) {
286            self.best_score = Some(score);
287        }
288        if self.improvement_score.is_none_or(|best| score > best) {
289            self.improvement_score = Some(score);
290            self.record_improvement(completed_step_count, elapsed);
291        }
292    }
293
294    fn needs_score_observation(&self) -> bool {
295        self.best_score_limit.is_some()
296            || self.unimproved_step_count_limit.is_some()
297            || self.unimproved_time_limit.is_some()
298    }
299}
300
301fn remaining_limit(limit: Option<u64>, used: u64) -> Option<u64> {
302    limit.map(|limit| limit.saturating_sub(used))
303}
304
305fn limit_reached(limit: Option<u64>, used: u64) -> bool {
306    limit.is_some_and(|limit| used >= limit)
307}
308
309#[derive(Clone, Copy)]
310pub(crate) struct SolverScopeChildConfig<'t, S: PlanningSolution> {
311    terminate: Option<&'t AtomicBool>,
312    runtime: Option<SolverRuntime<S>>,
313    environment_mode: EnvironmentMode,
314    time_deadline: Option<Instant>,
315    phase_budget: Option<&'t PhaseBudget>,
316    inphase_step_count_limit: Option<u64>,
317    inphase_move_count_limit: Option<u64>,
318    inphase_score_calc_count_limit: Option<u64>,
319    inphase_best_score_limit: Option<S::Score>,
320}
321
322impl<'t, S: PlanningSolution> SolverScopeChildConfig<'t, S> {
323    pub(crate) fn build_scope<PD>(&self, score_director: PD, seed: u64) -> SolverScope<'t, S, PD>
324    where
325        PD: Director<S>,
326    {
327        let terminate = self
328            .terminate
329            .or_else(|| self.runtime.map(|runtime| runtime.cancel_flag()));
330        let mut scope = SolverScope::new(score_director)
331            .with_terminate(terminate)
332            .with_runtime(self.runtime)
333            .without_publication()
334            .with_environment_mode(self.environment_mode)
335            .with_seed(seed);
336        scope.time_deadline = self.time_deadline;
337        scope.phase_budget = self.phase_budget;
338        if self.phase_budget.is_none() {
339            scope.inphase_step_count_limit = self.inphase_step_count_limit;
340            scope.inphase_move_count_limit = self.inphase_move_count_limit;
341            scope.inphase_score_calc_count_limit = self.inphase_score_calc_count_limit;
342        }
343        scope.inphase_best_score_limit = self.inphase_best_score_limit;
344        scope
345    }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
349pub(crate) enum PendingControl {
350    Continue,
351    PauseRequested,
352    CancelRequested,
353    ConfigTerminationRequested,
354}
355
356impl<'t, S: PlanningSolution, D: Director<S>> SolverScope<'t, S, D, ()> {
357    pub fn new(score_director: D) -> Self {
358        let construction_frontier = ConstructionFrontier::new();
359        Self {
360            score_director,
361            best_solution: None,
362            current_score: None,
363            best_score: None,
364            rng: StdRng::from_rng(&mut rand::rng()),
365            total_step_count: 0,
366            terminate: None,
367            runtime: None,
368            publication: Publication::Enabled,
369            best_solution_publication_enabled: true,
370            yielded_to_parent: false,
371            environment_mode: EnvironmentMode::default(),
372            stats: SolverStats::default(),
373            time_limit: None,
374            time_deadline: None,
375            progress_callback: (),
376            progress_pulse: None,
377            terminal_reason: None,
378            last_best_elapsed: None,
379            best_solution_revision: None,
380            solution_revision: 1,
381            working_solution_generation: 0,
382            construction_frontier,
383            phase_budget: None,
384            inphase_step_count_limit: None,
385            inphase_move_count_limit: None,
386            inphase_score_calc_count_limit: None,
387            inphase_best_score_limit: None,
388            phase_termination: None,
389        }
390    }
391}
392
393impl<'t, S: PlanningSolution, D: Director<S>, ProgressCb: ProgressCallback<S>>
394    SolverScope<'t, S, D, ProgressCb>
395{
396    pub fn new_with_callback(
397        score_director: D,
398        callback: ProgressCb,
399        terminate: Option<&'t AtomicBool>,
400        runtime: Option<SolverRuntime<S>>,
401    ) -> Self {
402        let construction_frontier = ConstructionFrontier::new();
403        Self {
404            score_director,
405            best_solution: None,
406            current_score: None,
407            best_score: None,
408            rng: StdRng::from_rng(&mut rand::rng()),
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            working_solution_generation: 0,
426            construction_frontier,
427            phase_budget: None,
428            inphase_step_count_limit: None,
429            inphase_move_count_limit: None,
430            inphase_score_calc_count_limit: None,
431            inphase_best_score_limit: None,
432            phase_termination: None,
433        }
434    }
435
436    pub fn with_terminate(mut self, terminate: Option<&'t AtomicBool>) -> Self {
437        self.terminate = terminate;
438        self
439    }
440
441    pub fn with_runtime(mut self, runtime: Option<SolverRuntime<S>>) -> Self {
442        self.runtime = runtime;
443        self
444    }
445
446    pub(crate) fn without_publication(mut self) -> Self {
447        self.publication = Publication::Disabled;
448        self
449    }
450
451    pub(crate) fn defer_best_solution_publication(&mut self) {
452        self.best_solution_publication_enabled = false;
453    }
454
455    pub(crate) fn best_solution_publication_enabled(&self) -> bool {
456        self.best_solution_publication_enabled
457    }
458
459    pub(crate) fn yielded_to_parent(&self) -> bool {
460        self.yielded_to_parent
461    }
462
463    pub fn with_environment_mode(mut self, environment_mode: EnvironmentMode) -> Self {
464        self.environment_mode = environment_mode;
465        self
466    }
467
468    pub fn with_seed(mut self, seed: u64) -> Self {
469        self.rng = StdRng::seed_from_u64(seed);
470        self
471    }
472
473    pub(crate) fn child_phase_budget(&self) -> PhaseBudget {
474        PhaseBudget::from_scope(self)
475    }
476
477    pub(crate) fn child_config<'a>(
478        &'a self,
479        phase_budget: Option<&'a PhaseBudget>,
480    ) -> SolverScopeChildConfig<'a, S> {
481        let phase_budget = self
482            .phase_budget
483            .or_else(|| phase_budget.filter(|budget| budget.has_limits()));
484        SolverScopeChildConfig {
485            terminate: self.terminate,
486            runtime: self.runtime,
487            environment_mode: self.environment_mode,
488            time_deadline: self.child_time_deadline(),
489            phase_budget,
490            inphase_step_count_limit: self.inphase_step_count_limit,
491            inphase_move_count_limit: self.inphase_move_count_limit,
492            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
493            inphase_best_score_limit: self.inphase_best_score_limit,
494        }
495    }
496
497    /// Runs one configured phase with a phase-relative termination overlay.
498    ///
499    /// The overlay is intentionally independent from solver-wide termination:
500    /// it starts at this phase boundary, applies every supported
501    /// `TerminationConfig` limit, and is restored before the next phase runs.
502    /// Mandatory default construction deliberately does not install this
503    /// overlay, so its required-completion pass remains interruptible only by
504    /// lifecycle control.
505    pub(crate) fn with_phase_termination<T>(
506        &mut self,
507        config: Option<&TerminationConfig>,
508        work: impl FnOnce(&mut Self) -> T,
509    ) -> T
510    where
511        S::Score: ParseableScore,
512    {
513        let previous = self.phase_termination.take();
514        self.phase_termination =
515            config.and_then(|config| ScopedPhaseTermination::from_config(self, config));
516        let result = work(self);
517        self.phase_termination = previous;
518        result
519    }
520
521    fn phase_termination_reached(&self) -> bool {
522        self.phase_termination.as_ref().is_some_and(|termination| {
523            // Only scoped terminations with clock-based limits need the
524            // wall clock; step/budget/score limits run on counters and the
525            // control poll calls this per candidate.
526            let elapsed = if termination.needs_clock() {
527                self.elapsed().unwrap_or_default()
528            } else {
529                Duration::ZERO
530            };
531            termination.is_reached(self.total_step_count, elapsed)
532        })
533    }
534
535    pub(crate) fn phase_termination_requires_score_observation(&self) -> bool {
536        self.phase_termination
537            .as_ref()
538            .is_some_and(ScopedPhaseTermination::needs_score_observation)
539    }
540
541    fn observe_phase_score(&mut self, score: S::Score, completed_step_count: u64) {
542        let elapsed = self.elapsed().unwrap_or_default();
543        if let Some(termination) = &mut self.phase_termination {
544            termination.observe_score(score, completed_step_count, elapsed);
545        }
546    }
547
548    pub(crate) fn observe_phase_step_score(&mut self, score: S::Score) {
549        self.observe_phase_score(score, self.total_step_count.saturating_add(1));
550    }
551
552    fn child_time_deadline(&self) -> Option<Instant> {
553        self.time_deadline.or_else(|| {
554            self.time_limit.map(|limit| {
555                self.stats
556                    .started_at()
557                    .map(|start| start + limit)
558                    .unwrap_or_else(|| Instant::now() + limit)
559            })
560        })
561    }
562
563    pub fn with_progress_callback<F: ProgressCallback<S>>(
564        self,
565        callback: F,
566    ) -> SolverScope<'t, S, D, F> {
567        SolverScope {
568            score_director: self.score_director,
569            best_solution: self.best_solution,
570            current_score: self.current_score,
571            best_score: self.best_score,
572            rng: self.rng,
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            working_solution_generation: self.working_solution_generation,
590            construction_frontier: self.construction_frontier,
591            phase_budget: self.phase_budget,
592            inphase_step_count_limit: self.inphase_step_count_limit,
593            inphase_move_count_limit: self.inphase_move_count_limit,
594            inphase_score_calc_count_limit: self.inphase_score_calc_count_limit,
595            inphase_best_score_limit: self.inphase_best_score_limit,
596            phase_termination: self.phase_termination,
597        }
598    }
599
600    pub fn start_solving(&mut self) {
601        self.total_step_count = 0;
602        self.terminal_reason = None;
603        self.last_best_elapsed = None;
604        self.yielded_to_parent = false;
605        self.best_solution_revision = None;
606        self.solution_revision = 1;
607        self.progress_pulse = None;
608        self.construction_frontier.reset();
609        self.stats.start();
610    }
611
612    pub fn elapsed(&self) -> Option<Duration> {
613        self.stats.active_elapsed()
614    }
615
616    pub fn time_since_last_improvement(&self) -> Option<Duration> {
617        let elapsed = self.elapsed()?;
618        let last_best_elapsed = self.last_best_elapsed?;
619        Some(elapsed.saturating_sub(last_best_elapsed))
620    }
621
622    pub fn score_director(&self) -> &D {
623        &self.score_director
624    }
625
626    pub(crate) fn score_director_mut(&mut self) -> &mut D {
627        &mut self.score_director
628    }
629
630    pub fn working_solution(&self) -> &S {
631        self.score_director.working_solution()
632    }
633
634    pub fn mutate<T, F>(&mut self, mutate: F) -> T
635    where
636        F: FnOnce(&mut D) -> T,
637    {
638        self.committed_mutation(mutate)
639    }
640
641    pub fn calculate_score(&mut self) -> S::Score {
642        self.record_score_calculation();
643        let score = self.score_director.calculate_score();
644        self.current_score = Some(score);
645        self.assert_score_consistent("calculate_score", score);
646        score
647    }
648
649    pub(crate) fn assert_score_consistent(&self, context: &str, score: S::Score) {
650        if self.environment_mode != EnvironmentMode::FullAssert {
651            return;
652        }
653        let Some(fresh_score) = self.score_director.fresh_score() else {
654            return;
655        };
656        assert_eq!(
657            score, fresh_score,
658            "score director drift after {context}: cached score {score:?} != fresh score {fresh_score:?}"
659        );
660    }
661
662    pub fn initialize_working_solution_as_best(&mut self) -> S::Score {
663        if self.stats.started_at().is_none() {
664            self.start_solving();
665        }
666        let score = self.calculate_score();
667        let solution = self.score_director.clone_working_solution();
668        self.set_best_solution(solution, score);
669        score
670    }
671
672    pub fn replace_working_solution_and_reinitialize(&mut self, solution: S) -> S::Score {
673        self.working_solution_generation += 1;
674        *self.score_director.working_solution_mut() = solution;
675        self.score_director.reset();
676        self.current_score = None;
677        self.best_solution_revision = None;
678        self.solution_revision = 1;
679        self.construction_frontier.reset();
680        self.calculate_score()
681    }
682
683    pub fn best_solution(&self) -> Option<&S> {
684        self.best_solution.as_ref()
685    }
686
687    pub fn best_score(&self) -> Option<&S::Score> {
688        self.best_score.as_ref()
689    }
690
691    pub fn current_score(&self) -> Option<&S::Score> {
692        self.current_score.as_ref()
693    }
694
695    pub(crate) fn is_scalar_slot_completed(&self, slot_id: ConstructionSlotId) -> bool {
696        self.construction_frontier
697            .is_scalar_completed(slot_id, self.solution_revision)
698    }
699
700    pub(crate) fn mark_scalar_slot_completed(&mut self, slot_id: ConstructionSlotId) {
701        self.construction_frontier
702            .mark_scalar_completed(slot_id, self.solution_revision);
703    }
704
705    pub(crate) fn is_group_slot_completed(&self, slot_id: &ConstructionGroupSlotId) -> bool {
706        self.construction_frontier
707            .is_group_completed(slot_id, self.solution_revision)
708    }
709
710    pub(crate) fn mark_group_slot_completed(&mut self, slot_id: ConstructionGroupSlotId) {
711        self.construction_frontier
712            .mark_group_completed(slot_id, self.solution_revision);
713    }
714
715    pub(crate) fn is_list_element_completed(&self, element_id: ConstructionListElementId) -> bool {
716        self.construction_frontier
717            .is_list_completed(element_id, self.solution_revision)
718    }
719
720    pub(crate) fn mark_list_element_completed(&mut self, element_id: ConstructionListElementId) {
721        self.construction_frontier
722            .mark_list_completed(element_id, self.solution_revision);
723    }
724
725    #[cfg(test)]
726    pub(crate) fn solution_revision(&self) -> u64 {
727        self.solution_revision
728    }
729
730    pub(crate) fn working_solution_generation(&self) -> u64 {
731        self.working_solution_generation
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}