Skip to main content

solverforge_solver/scope/
phase.rs

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