1use 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
18pub struct PhaseScope<'t, 'a, S: PlanningSolution, D: Director<S>, BestCb = ()> {
27 solver_scope: &'a mut SolverScope<'t, S, D, BestCb>,
29 phase_index: usize,
31 candidate_trace_phase_index: Option<usize>,
33 starting_score: Option<S::Score>,
35 step_count: u64,
37 start_time: Instant,
39 solver_elapsed_at_start: Option<Duration>,
41 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 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 pub fn calculate_score(&mut self) -> S::Score {
138 self.stats.record_score_calculation();
139 self.solver_scope.calculate_score()
140 }
141
142 pub fn update_best_solution(&mut self) {
144 self.solver_scope.update_best_solution()
145 }
146
147 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}