1use 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
17pub struct PhaseScope<'t, 'a, S: PlanningSolution, D: Director<S>, BestCb = ()> {
26 solver_scope: &'a mut SolverScope<'t, S, D, BestCb>,
28 phase_index: usize,
30 candidate_trace_phase_index: Option<usize>,
32 starting_score: Option<S::Score>,
34 step_count: u64,
36 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 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 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 pub fn calculate_score(&mut self) -> S::Score {
119 self.solver_scope.calculate_score()
120 }
121
122 pub fn update_best_solution(&mut self) {
124 self.solver_scope.update_best_solution()
125 }
126
127 pub(crate) fn promote_current_solution_on_score_tie(&mut self) {
129 self.solver_scope.promote_current_solution_on_score_tie()
130 }
131
132 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 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}