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