Skip to main content

solverforge_solver/stats/
phase.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::time::{Duration, Instant};
4
5use super::{AppliedMoveTelemetry, MoveTelemetry, PhaseTelemetry, SelectorTelemetry, Throughput};
6
7const APPLIED_MOVE_TRACE_LIMIT: usize = 8;
8
9#[path = "phase_absorb.rs"]
10mod absorb;
11
12#[derive(Debug, Clone)]
13pub struct PhaseStats {
14    // Index of this phase (0-based).
15    pub phase_index: usize,
16    // Type name of the phase.
17    pub phase_type: &'static str,
18    start_time: Instant,
19    // Number of steps taken in this phase.
20    pub step_count: u64,
21    /// Number of candidate moves actually yielded by cursors in this phase.
22    /// Unrequested logical neighborhood tails are not generated work.
23    pub moves_generated: u64,
24    // Number of moves evaluated in this phase.
25    pub moves_evaluated: u64,
26    /// Candidates parallel evaluation evaluated ahead of the sequential
27    /// decision point in this phase and then discarded.
28    pub moves_speculative: u64,
29    // Number of moves accepted in this phase.
30    pub moves_accepted: u64,
31    // Number of moves applied in this phase.
32    pub moves_applied: u64,
33    pub moves_not_doable: u64,
34    pub moves_acceptor_rejected: u64,
35    pub moves_forager_ignored: u64,
36    pub moves_hard_improving: u64,
37    pub moves_hard_neutral: u64,
38    pub moves_hard_worse: u64,
39    pub conflict_repair_provider_generated: u64,
40    pub conflict_repair_duplicate_filtered: u64,
41    pub conflict_repair_illegal_filtered: u64,
42    pub conflict_repair_not_doable_filtered: u64,
43    pub conflict_repair_hard_improving: u64,
44    pub conflict_repair_exposed: u64,
45    // Number of score calculations in this phase.
46    pub score_calculations: u64,
47    pub construction_slots_assigned: u64,
48    pub construction_slots_kept: u64,
49    pub construction_slots_no_doable: u64,
50    pub scalar_assignment_required_remaining: u64,
51    generation_time: Duration,
52    evaluation_time: Duration,
53    selector_stats: Vec<SelectorTelemetry>,
54    move_stats: BTreeMap<&'static str, MoveTelemetry>,
55    applied_move_trace: Vec<AppliedMoveTelemetry>,
56}
57
58impl PhaseStats {
59    /// Creates new phase statistics.
60    pub fn new(phase_index: usize, phase_type: &'static str) -> Self {
61        Self {
62            phase_index,
63            phase_type,
64            start_time: Instant::now(),
65            step_count: 0,
66            moves_generated: 0,
67            moves_evaluated: 0,
68            moves_speculative: 0,
69            moves_accepted: 0,
70            moves_applied: 0,
71            moves_not_doable: 0,
72            moves_acceptor_rejected: 0,
73            moves_forager_ignored: 0,
74            moves_hard_improving: 0,
75            moves_hard_neutral: 0,
76            moves_hard_worse: 0,
77            conflict_repair_provider_generated: 0,
78            conflict_repair_duplicate_filtered: 0,
79            conflict_repair_illegal_filtered: 0,
80            conflict_repair_not_doable_filtered: 0,
81            conflict_repair_hard_improving: 0,
82            conflict_repair_exposed: 0,
83            score_calculations: 0,
84            construction_slots_assigned: 0,
85            construction_slots_kept: 0,
86            construction_slots_no_doable: 0,
87            scalar_assignment_required_remaining: 0,
88            generation_time: Duration::default(),
89            evaluation_time: Duration::default(),
90            selector_stats: Vec::new(),
91            move_stats: BTreeMap::new(),
92            applied_move_trace: Vec::new(),
93        }
94    }
95
96    pub fn elapsed(&self) -> Duration {
97        self.start_time.elapsed()
98    }
99
100    pub fn snapshot(&self) -> PhaseTelemetry {
101        self.snapshot_with_elapsed(self.elapsed())
102    }
103
104    pub(crate) fn snapshot_with_elapsed(&self, elapsed: Duration) -> PhaseTelemetry {
105        PhaseTelemetry {
106            phase_index: self.phase_index,
107            phase_type: self.phase_type.to_string(),
108            elapsed,
109            step_count: self.step_count,
110            moves_generated: self.moves_generated,
111            moves_evaluated: self.moves_evaluated,
112            moves_speculative: self.moves_speculative,
113            moves_accepted: self.moves_accepted,
114            moves_applied: self.moves_applied,
115            moves_score_improving: self.moves_score_improving(),
116            moves_applied_improving: self.moves_applied_improving(),
117            score_calculations: self.score_calculations,
118            generation_time: self.generation_time,
119            evaluation_time: self.evaluation_time,
120        }
121    }
122
123    /// Records a step completion.
124    pub fn record_step(&mut self) {
125        self.step_count += 1;
126    }
127
128    /// Records one or more generated candidate moves and the time spent generating them.
129    pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
130        self.moves_generated += count;
131        self.generation_time += duration;
132    }
133
134    pub fn record_selector_generated(
135        &mut self,
136        selector_index: usize,
137        count: u64,
138        duration: Duration,
139    ) {
140        self.record_generated_batch(count, duration);
141        let selector = self.selector_stats_entry(selector_index);
142        selector.moves_generated += count;
143        selector.generation_time += duration;
144    }
145
146    /// Records generation time that did not itself yield a counted move.
147    pub fn record_generation_time(&mut self, duration: Duration) {
148        self.generation_time += duration;
149    }
150
151    /// Records a single generated candidate move and the time spent generating it.
152    pub fn record_generated_move(&mut self, duration: Duration) {
153        self.record_generated_batch(1, duration);
154    }
155
156    /// Records a move evaluation and the time spent evaluating it.
157    pub fn record_evaluated_move(&mut self, duration: Duration) {
158        self.moves_evaluated += 1;
159        self.evaluation_time += duration;
160    }
161
162    pub fn record_selector_evaluated(&mut self, selector_index: usize, duration: Duration) {
163        self.record_evaluated_move(duration);
164        let selector = self.selector_stats_entry(selector_index);
165        selector.moves_evaluated += 1;
166        selector.evaluation_time += duration;
167    }
168
169    /// Records an accepted move.
170    pub fn record_move_accepted(&mut self) {
171        self.moves_accepted += 1;
172    }
173
174    pub fn record_selector_accepted(&mut self, selector_index: usize) {
175        self.record_move_accepted();
176        self.selector_stats_entry(selector_index).moves_accepted += 1;
177    }
178
179    pub fn record_move_applied(&mut self) {
180        self.moves_applied += 1;
181    }
182
183    pub fn record_selector_applied(&mut self, selector_index: usize) {
184        self.record_move_applied();
185        self.selector_stats_entry(selector_index).moves_applied += 1;
186    }
187
188    pub fn record_move_not_doable(&mut self) {
189        self.moves_not_doable += 1;
190    }
191
192    pub fn record_selector_not_doable(&mut self, selector_index: usize) {
193        self.record_move_not_doable();
194        self.selector_stats_entry(selector_index).moves_not_doable += 1;
195    }
196
197    pub fn record_move_acceptor_rejected(&mut self) {
198        self.moves_acceptor_rejected += 1;
199    }
200
201    pub fn record_selector_acceptor_rejected(&mut self, selector_index: usize) {
202        self.record_move_acceptor_rejected();
203        self.selector_stats_entry(selector_index)
204            .moves_acceptor_rejected += 1;
205    }
206
207    pub fn record_moves_forager_ignored(&mut self, count: u64) {
208        self.moves_forager_ignored += count;
209    }
210
211    pub fn record_move_hard_improving(&mut self) {
212        self.moves_hard_improving += 1;
213    }
214
215    pub fn record_move_hard_neutral(&mut self) {
216        self.moves_hard_neutral += 1;
217    }
218
219    pub fn record_move_hard_worse(&mut self) {
220        self.moves_hard_worse += 1;
221    }
222
223    pub fn record_conflict_repair_provider_generated(&mut self, count: u64) {
224        self.conflict_repair_provider_generated += count;
225    }
226
227    pub fn record_conflict_repair_duplicate_filtered(&mut self) {
228        self.conflict_repair_duplicate_filtered += 1;
229    }
230
231    pub fn record_conflict_repair_illegal_filtered(&mut self) {
232        self.conflict_repair_illegal_filtered += 1;
233    }
234
235    pub fn record_conflict_repair_not_doable_filtered(&mut self) {
236        self.conflict_repair_not_doable_filtered += 1;
237    }
238
239    pub fn record_conflict_repair_hard_improving(&mut self) {
240        self.conflict_repair_hard_improving += 1;
241    }
242
243    pub fn record_conflict_repair_exposed(&mut self) {
244        self.conflict_repair_exposed += 1;
245    }
246
247    /// Records a score calculation.
248    pub fn record_score_calculation(&mut self) {
249        self.score_calculations += 1;
250    }
251
252    /// Records a candidate that parallel evaluation evaluated ahead of the
253    /// sequential decision point and then discarded.
254    pub fn record_move_speculative(&mut self) {
255        self.moves_speculative += 1;
256    }
257
258    pub fn record_construction_slot_assigned(&mut self) {
259        self.construction_slots_assigned += 1;
260    }
261
262    pub fn record_construction_slot_kept(&mut self) {
263        self.construction_slots_kept += 1;
264    }
265
266    pub fn record_construction_slot_no_doable(&mut self) {
267        self.construction_slots_no_doable += 1;
268    }
269
270    pub fn record_scalar_assignment_required_remaining(&mut self, count: u64) {
271        self.scalar_assignment_required_remaining = count;
272    }
273
274    pub fn generated_throughput(&self) -> Throughput {
275        Throughput {
276            count: self.moves_generated,
277            elapsed: self.generation_time,
278        }
279    }
280
281    pub fn evaluated_throughput(&self) -> Throughput {
282        Throughput {
283            count: self.moves_evaluated,
284            elapsed: self.evaluation_time,
285        }
286    }
287
288    pub fn acceptance_rate(&self) -> f64 {
289        if self.moves_evaluated == 0 {
290            0.0
291        } else {
292            self.moves_accepted as f64 / self.moves_evaluated as f64
293        }
294    }
295
296    pub fn generation_time(&self) -> Duration {
297        self.generation_time
298    }
299
300    pub fn evaluation_time(&self) -> Duration {
301        self.evaluation_time
302    }
303
304    pub fn selector_telemetry(&self) -> &[SelectorTelemetry] {
305        &self.selector_stats
306    }
307
308    pub fn applied_move_trace(&self) -> &[AppliedMoveTelemetry] {
309        &self.applied_move_trace
310    }
311
312    pub fn move_telemetry(&self) -> Vec<MoveTelemetry> {
313        self.move_stats.values().cloned().collect()
314    }
315
316    pub fn moves_score_improving(&self) -> u64 {
317        self.move_stats
318            .values()
319            .map(|entry| entry.moves_score_improving)
320            .sum()
321    }
322
323    pub fn moves_applied_improving(&self) -> u64 {
324        self.move_stats
325            .values()
326            .map(|entry| entry.moves_applied_improving)
327            .sum()
328    }
329
330    pub fn can_record_applied_move_trace(&self) -> bool {
331        self.applied_move_trace.len() < APPLIED_MOVE_TRACE_LIMIT
332    }
333
334    pub fn record_move_kind_generated(&mut self, move_label: &'static str) {
335        self.move_stats_entry(move_label).moves_generated += 1;
336    }
337
338    pub fn record_move_kind_evaluated(
339        &mut self,
340        move_label: &'static str,
341        score_ordering: Ordering,
342    ) {
343        let entry = self.move_stats_entry(move_label);
344        entry.moves_evaluated += 1;
345        match score_ordering {
346            Ordering::Greater => entry.moves_score_improving += 1,
347            Ordering::Equal => entry.moves_score_equal += 1,
348            Ordering::Less => entry.moves_score_worse += 1,
349        }
350    }
351
352    pub fn record_move_kind_evaluated_unscored(&mut self, move_label: &'static str) {
353        self.move_stats_entry(move_label).moves_evaluated += 1;
354    }
355
356    pub fn record_move_kind_accepted(&mut self, move_label: &'static str) {
357        self.move_stats_entry(move_label).moves_accepted += 1;
358    }
359
360    pub fn record_move_kind_applied(&mut self, move_label: &'static str, score_improvement: f64) {
361        let entry = self.move_stats_entry(move_label);
362        entry.moves_applied += 1;
363        if score_improvement > 0.0 {
364            entry.moves_applied_improving += 1;
365            entry.applied_score_improvement += score_improvement;
366        }
367    }
368
369    pub fn record_move_kind_not_doable(&mut self, move_label: &'static str) {
370        self.move_stats_entry(move_label).moves_not_doable += 1;
371    }
372
373    pub fn record_move_kind_acceptor_rejected(
374        &mut self,
375        move_label: &'static str,
376        score_ordering: Ordering,
377    ) {
378        let entry = self.move_stats_entry(move_label);
379        entry.moves_acceptor_rejected += 1;
380        if score_ordering == Ordering::Greater {
381            entry.moves_rejected_improving += 1;
382        }
383    }
384
385    pub fn record_move_kind_forager_ignored(&mut self, move_label: &'static str, count: u64) {
386        if count == 0 {
387            return;
388        }
389        self.move_stats_entry(move_label).moves_forager_ignored += count;
390    }
391
392    pub fn record_applied_move_trace(&mut self, applied_move: AppliedMoveTelemetry) {
393        if self.applied_move_trace.len() < APPLIED_MOVE_TRACE_LIMIT {
394            self.applied_move_trace.push(applied_move);
395        }
396    }
397
398    pub fn record_selector_generated_with_label(
399        &mut self,
400        selector_index: usize,
401        selector_label: impl Into<String>,
402        count: u64,
403        duration: Duration,
404    ) {
405        self.record_generated_batch(count, duration);
406        let selector = self.selector_stats_entry_with_label(selector_index, selector_label);
407        selector.moves_generated += count;
408        selector.generation_time += duration;
409    }
410
411    fn selector_stats_entry(&mut self, selector_index: usize) -> &mut SelectorTelemetry {
412        self.selector_stats_entry_with_label(selector_index, format!("selector-{selector_index}"))
413    }
414
415    fn selector_stats_entry_with_label(
416        &mut self,
417        selector_index: usize,
418        selector_label: impl Into<String>,
419    ) -> &mut SelectorTelemetry {
420        let selector_label = selector_label.into();
421        if let Some(position) = self
422            .selector_stats
423            .iter()
424            .position(|entry| entry.selector_index == selector_index)
425        {
426            if self.selector_stats[position]
427                .selector_label
428                .starts_with("selector-")
429                && !selector_label.starts_with("selector-")
430            {
431                self.selector_stats[position].selector_label = selector_label;
432            }
433            return &mut self.selector_stats[position];
434        }
435        self.selector_stats.push(SelectorTelemetry {
436            selector_index,
437            selector_label,
438            ..SelectorTelemetry::default()
439        });
440        self.selector_stats
441            .last_mut()
442            .expect("selector stats entry was just inserted")
443    }
444
445    fn move_stats_entry(&mut self, move_label: &'static str) -> &mut MoveTelemetry {
446        self.move_stats
447            .entry(move_label)
448            .or_insert_with(|| MoveTelemetry {
449                move_label: move_label.to_string(),
450                ..MoveTelemetry::default()
451            })
452    }
453}