Skip to main content

solverforge_solver/scope/
phase.rs

1// Phase-level scope.
2
3use std::cmp::Ordering;
4use std::time::Duration;
5
6use solverforge_core::domain::PlanningSolution;
7use solverforge_scoring::Director;
8use tracing::debug;
9
10use super::solver::ProgressCallback;
11use super::SolverScope;
12use crate::stats::{
13    whole_units_per_second, AppliedMoveTelemetry, CandidateTraceConstructionTarget,
14    CandidateTraceDisposition, CandidateTracePullToken, CandidateTraceSource, PhaseStats,
15};
16
17/// Scope for a single phase of solving.
18///
19/// # Type Parameters
20/// * `'t` - Lifetime of the termination flag
21/// * `'a` - Lifetime of the solver scope reference
22/// * `S` - The planning solution type
23/// * `D` - The score director type
24/// * `BestCb` - The best-solution callback type
25pub struct PhaseScope<'t, 'a, S: PlanningSolution, D: Director<S>, BestCb = ()> {
26    // Reference to the parent solver scope.
27    solver_scope: &'a mut SolverScope<'t, S, D, BestCb>,
28    // Index of this phase (0-based).
29    phase_index: usize,
30    // Monotonic execution index used only by an enabled candidate trace.
31    candidate_trace_phase_index: Option<usize>,
32    // Score at the start of this phase.
33    starting_score: Option<S::Score>,
34    // Number of steps in this phase.
35    step_count: u64,
36    // Phase type name, also the type of the phase's statistics record.
37    phase_type: &'static str,
38}
39
40impl<'t, 'a, S: PlanningSolution, D: Director<S>, BestCb: ProgressCallback<S>>
41    PhaseScope<'t, 'a, S, D, BestCb>
42{
43    pub fn new(solver_scope: &'a mut SolverScope<'t, S, D, BestCb>, phase_index: usize) -> Self {
44        Self::with_phase_type(solver_scope, phase_index, "Unknown")
45    }
46
47    pub fn with_phase_type(
48        solver_scope: &'a mut SolverScope<'t, S, D, BestCb>,
49        phase_index: usize,
50        phase_type: &'static str,
51    ) -> Self {
52        let candidate_trace_phase_index = solver_scope.begin_candidate_trace_phase();
53        solver_scope.begin_phase_progress(phase_index, phase_type, 0, 0);
54        solver_scope.stats_mut().begin_phase(phase_type);
55        let starting_score = solver_scope.best_score().cloned();
56        Self {
57            solver_scope,
58            phase_index,
59            candidate_trace_phase_index,
60            starting_score,
61            step_count: 0,
62            phase_type,
63        }
64    }
65
66    pub fn phase_index(&self) -> usize {
67        self.phase_index
68    }
69
70    pub fn starting_score(&self) -> Option<&S::Score> {
71        self.starting_score.as_ref()
72    }
73
74    /// Solver-active time this phase has spent; the solver clock excludes
75    /// pauses.
76    pub fn elapsed(&self) -> std::time::Duration {
77        self.solver_scope
78            .stats()
79            .phase_elapsed()
80            .expect("a phase scope keeps its phase record open")
81    }
82
83    pub fn step_count(&self) -> u64 {
84        self.step_count
85    }
86
87    /// Increments the phase step count.
88    pub fn increment_step_count(&mut self) -> u64 {
89        self.step_count += 1;
90        self.solver_scope.increment_step_count();
91        self.step_count
92    }
93
94    pub fn solver_scope(&self) -> &SolverScope<'t, S, D, BestCb> {
95        self.solver_scope
96    }
97
98    pub fn solver_scope_mut(&mut self) -> &mut SolverScope<'t, S, D, BestCb> {
99        self.solver_scope
100    }
101
102    pub fn score_director(&self) -> &D {
103        self.solver_scope.score_director()
104    }
105
106    pub(crate) fn score_director_mut(&mut self) -> &mut D {
107        self.solver_scope.score_director_mut()
108    }
109
110    pub fn mutate<T, F>(&mut self, mutate: F) -> T
111    where
112        F: FnOnce(&mut D) -> T,
113    {
114        self.solver_scope.mutate(mutate)
115    }
116
117    /// Calculates the current score.
118    pub fn calculate_score(&mut self) -> S::Score {
119        self.solver_scope.calculate_score()
120    }
121
122    /// Updates best solution.
123    pub fn update_best_solution(&mut self) {
124        self.solver_scope.update_best_solution()
125    }
126
127    /// Publishes the current working solution when it ties the current best score.
128    pub(crate) fn promote_current_solution_on_score_tie(&mut self) {
129        self.solver_scope.promote_current_solution_on_score_tie()
130    }
131
132    /// Statistics of this phase. They live in the solver's statistics, which
133    /// open this phase's record for the scope's lifetime.
134    pub fn stats(&self) -> &PhaseStats {
135        self.solver_scope
136            .stats()
137            .phase()
138            .expect("a phase scope keeps its phase record open")
139    }
140
141    pub fn stats_mut(&mut self) -> &mut PhaseStats {
142        self.solver_scope
143            .stats_mut()
144            .phase_mut()
145            .expect("a phase scope keeps its phase record open")
146    }
147
148    pub fn report_progress(&self) {
149        self.solver_scope.report_phase_progress();
150    }
151
152    pub fn report_progress_if_due(&mut self) -> bool {
153        if !BestCb::PUBLISHES_PROGRESS && !tracing::enabled!(tracing::Level::DEBUG) {
154            return false;
155        }
156        let (step_count, moves_evaluated) = {
157            let stats = self.stats();
158            (stats.step_count, stats.moves_evaluated)
159        };
160        let tick = self.solver_scope.take_phase_progress_tick(
161            self.phase_index,
162            self.phase_type,
163            step_count,
164            moves_evaluated,
165        );
166        let Some(tick) = tick else {
167            return false;
168        };
169        let speed_count = if tick.move_delta > 0 {
170            tick.move_delta
171        } else {
172            tick.step_delta
173        };
174        let current_score = self.solver_scope.current_score();
175        let published_best_score = self.solver_scope.best_score();
176        let reported_best_score = match (current_score, published_best_score) {
177            (Some(current), Some(best)) if current > best => Some(current),
178            (_, Some(best)) => Some(best),
179            (Some(current), None) => Some(current),
180            (None, None) => None,
181        };
182        let current_score = current_score
183            .map(ToString::to_string)
184            .unwrap_or_else(|| "none".to_string());
185        let best_score = reported_best_score
186            .map(ToString::to_string)
187            .unwrap_or_else(|| "none".to_string());
188        let stats = self.stats();
189        debug!(
190            event = "progress",
191            phase = self.phase_type,
192            phase_index = self.phase_index,
193            steps = stats.step_count,
194            moves_generated = stats.moves_generated,
195            moves_evaluated = stats.moves_evaluated,
196            moves_accepted = stats.moves_accepted,
197            moves_score_improving = stats.moves_score_improving(),
198            moves_applied_improving = stats.moves_applied_improving(),
199            score_calculations = stats.score_calculations,
200            speed = whole_units_per_second(speed_count, tick.elapsed),
201            acceptance_rate = format!("{:.1}%", stats.acceptance_rate() * 100.0),
202            current_score = current_score,
203            best_score = best_score,
204        );
205        self.report_progress();
206        true
207    }
208
209    pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
210        self.solver_scope
211            .stats_mut()
212            .record_generated_batch(count, duration);
213    }
214
215    pub fn record_generated_move(&mut self, duration: Duration) {
216        self.record_generated_batch(1, duration);
217    }
218
219    pub fn record_selector_generated_move(&mut self, selector_index: usize, duration: Duration) {
220        self.solver_scope
221            .stats_mut()
222            .record_selector_generated(selector_index, 1, duration);
223    }
224
225    pub fn record_selector_generated_move_with_label(
226        &mut self,
227        selector_index: usize,
228        selector_label: impl Into<String>,
229        duration: Duration,
230    ) {
231        self.solver_scope
232            .stats_mut()
233            .record_selector_generated_with_label(selector_index, selector_label, 1, duration);
234    }
235
236    pub fn record_generation_time(&mut self, duration: Duration) {
237        self.solver_scope
238            .stats_mut()
239            .record_generation_time(duration);
240    }
241
242    pub fn record_evaluated_move(&mut self, duration: Duration) {
243        self.solver_scope.record_evaluated_move(duration);
244    }
245
246    pub fn record_selector_evaluated_move(&mut self, selector_index: usize, duration: Duration) {
247        self.solver_scope
248            .record_selector_evaluated(selector_index, duration);
249    }
250
251    pub fn record_move_accepted(&mut self) {
252        self.solver_scope.stats_mut().record_move_accepted();
253    }
254
255    pub fn record_move_applied(&mut self) {
256        self.solver_scope.stats_mut().record_move_applied();
257    }
258
259    pub fn record_selector_move_accepted(&mut self, selector_index: usize) {
260        self.solver_scope
261            .stats_mut()
262            .record_selector_accepted(selector_index);
263    }
264
265    pub fn record_selector_move_applied(&mut self, selector_index: usize) {
266        self.solver_scope
267            .stats_mut()
268            .record_selector_applied(selector_index);
269    }
270
271    pub fn record_move_not_doable(&mut self) {
272        self.solver_scope.stats_mut().record_move_not_doable();
273    }
274
275    pub fn record_selector_move_not_doable(&mut self, selector_index: usize) {
276        self.solver_scope
277            .stats_mut()
278            .record_selector_not_doable(selector_index);
279    }
280
281    pub fn record_move_acceptor_rejected(&mut self) {
282        self.solver_scope
283            .stats_mut()
284            .record_move_acceptor_rejected();
285    }
286
287    pub fn record_selector_move_acceptor_rejected(&mut self, selector_index: usize) {
288        self.solver_scope
289            .stats_mut()
290            .record_selector_acceptor_rejected(selector_index);
291    }
292
293    pub fn record_moves_forager_ignored(&mut self, count: u64) {
294        self.solver_scope
295            .stats_mut()
296            .record_moves_forager_ignored(count);
297    }
298
299    pub fn record_move_kind_generated(&mut self, move_label: &'static str) {
300        self.solver_scope
301            .stats_mut()
302            .record_move_kind_generated(move_label);
303    }
304
305    pub fn record_move_kind_evaluated(
306        &mut self,
307        move_label: &'static str,
308        score_ordering: Ordering,
309    ) {
310        self.solver_scope
311            .stats_mut()
312            .record_move_kind_evaluated(move_label, score_ordering);
313    }
314
315    pub fn record_move_kind_evaluated_unscored(&mut self, move_label: &'static str) {
316        self.solver_scope
317            .stats_mut()
318            .record_move_kind_evaluated_unscored(move_label);
319    }
320
321    pub fn record_move_kind_accepted(&mut self, move_label: &'static str) {
322        self.solver_scope
323            .stats_mut()
324            .record_move_kind_accepted(move_label);
325    }
326
327    pub fn record_move_kind_applied(&mut self, move_label: &'static str, score_improvement: f64) {
328        self.solver_scope
329            .stats_mut()
330            .record_move_kind_applied(move_label, score_improvement);
331    }
332
333    pub fn record_move_kind_not_doable(&mut self, move_label: &'static str) {
334        self.solver_scope
335            .stats_mut()
336            .record_move_kind_not_doable(move_label);
337    }
338
339    pub fn record_move_kind_acceptor_rejected(
340        &mut self,
341        move_label: &'static str,
342        score_ordering: Ordering,
343    ) {
344        self.solver_scope
345            .stats_mut()
346            .record_move_kind_acceptor_rejected(move_label, score_ordering);
347    }
348
349    pub fn record_move_kind_forager_ignored(&mut self, move_label: &'static str, count: u64) {
350        self.solver_scope
351            .stats_mut()
352            .record_move_kind_forager_ignored(move_label, count);
353    }
354
355    pub fn record_applied_move_trace(&mut self, applied_move: AppliedMoveTelemetry) {
356        self.solver_scope
357            .stats_mut()
358            .record_applied_move_trace(applied_move);
359    }
360
361    pub fn can_record_applied_move_trace(&self) -> bool {
362        self.stats().can_record_applied_move_trace()
363    }
364
365    #[allow(clippy::too_many_arguments)]
366    pub(crate) fn record_candidate_pull<M>(
367        &mut self,
368        source: CandidateTraceSource,
369        selector_index: Option<usize>,
370        candidate_index: usize,
371        construction_target: Option<CandidateTraceConstructionTarget>,
372        candidate: &M,
373    ) -> Option<CandidateTracePullToken>
374    where
375        M: crate::heuristic::r#move::Move<S>,
376    {
377        self.solver_scope.record_candidate_pull(
378            source,
379            self.candidate_trace_phase_index.unwrap_or(self.phase_index),
380            self.phase_type,
381            self.step_count,
382            selector_index,
383            candidate_index,
384            construction_target,
385            candidate,
386        )
387    }
388
389    #[allow(clippy::too_many_arguments)]
390    pub(crate) fn record_candidate_operation<I, T>(
391        &mut self,
392        source: CandidateTraceSource,
393        selector_index: Option<usize>,
394        candidate_index: usize,
395        construction_target: Option<CandidateTraceConstructionTarget>,
396        descriptor_index: usize,
397        operation: &'static str,
398        components: I,
399    ) -> Option<CandidateTracePullToken>
400    where
401        I: IntoIterator<Item = T>,
402        T: Into<crate::stats::CandidateTraceCoordinate>,
403    {
404        self.solver_scope.record_candidate_operation(
405            source,
406            self.candidate_trace_phase_index.unwrap_or(self.phase_index),
407            self.phase_type,
408            self.step_count,
409            selector_index,
410            candidate_index,
411            construction_target,
412            descriptor_index,
413            operation,
414            components,
415        )
416    }
417
418    pub(crate) fn record_candidate_trace_disposition(
419        &mut self,
420        token: CandidateTracePullToken,
421        disposition: CandidateTraceDisposition,
422    ) {
423        self.solver_scope
424            .record_candidate_trace_disposition(token, disposition);
425    }
426
427    pub fn record_move_hard_improving(&mut self) {
428        self.solver_scope.stats_mut().record_move_hard_improving();
429    }
430
431    pub fn record_move_hard_neutral(&mut self) {
432        self.solver_scope.stats_mut().record_move_hard_neutral();
433    }
434
435    pub fn record_move_hard_worse(&mut self) {
436        self.solver_scope.stats_mut().record_move_hard_worse();
437    }
438
439    pub fn record_score_calculation(&mut self) {
440        self.solver_scope.record_score_calculation();
441    }
442
443    /// Records a candidate evaluated ahead of the sequential decision point by
444    /// parallel evaluation and then discarded.
445    pub fn record_move_speculative(&mut self) {
446        self.solver_scope.record_move_speculative();
447    }
448
449    pub fn record_construction_slot_assigned(&mut self) {
450        self.solver_scope
451            .stats_mut()
452            .record_construction_slot_assigned();
453    }
454
455    pub fn record_construction_slot_kept(&mut self) {
456        self.solver_scope
457            .stats_mut()
458            .record_construction_slot_kept();
459    }
460
461    pub fn record_construction_slot_no_doable(&mut self) {
462        self.solver_scope
463            .stats_mut()
464            .record_construction_slot_no_doable();
465    }
466
467    pub fn record_scalar_assignment_required_remaining(
468        &mut self,
469        group_name: &'static str,
470        count: u64,
471    ) {
472        self.solver_scope
473            .stats_mut()
474            .record_scalar_assignment_required_remaining(group_name, count);
475    }
476}
477
478impl<S: PlanningSolution, D: Director<S>, BestCb> Drop for PhaseScope<'_, '_, S, D, BestCb> {
479    fn drop(&mut self) {
480        self.solver_scope.end_phase_stats();
481    }
482}