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