Skip to main content

solverforge_solver/stats/
solver.rs

1use std::collections::BTreeMap;
2use std::time::{Duration, Instant};
3
4use super::{SelectorTelemetry, SolverTelemetry, Throughput};
5
6#[derive(Debug, Default)]
7pub struct SolverStats {
8    start_time: Option<Instant>,
9    pause_started_at: Option<Instant>,
10    // Total steps taken across all phases.
11    pub step_count: u64,
12    // Total moves generated across all phases.
13    pub moves_generated: u64,
14    // Total moves evaluated across all phases.
15    pub moves_evaluated: u64,
16    // Total moves accepted across all phases.
17    pub moves_accepted: u64,
18    // Total moves applied across all phases.
19    pub moves_applied: u64,
20    pub moves_not_doable: u64,
21    pub moves_acceptor_rejected: u64,
22    pub moves_forager_ignored: u64,
23    pub moves_hard_improving: u64,
24    pub moves_hard_neutral: u64,
25    pub moves_hard_worse: u64,
26    pub conflict_repair_provider_generated: u64,
27    pub conflict_repair_duplicate_filtered: u64,
28    pub conflict_repair_illegal_filtered: u64,
29    pub conflict_repair_not_doable_filtered: u64,
30    pub conflict_repair_hard_improving: u64,
31    pub conflict_repair_exposed: u64,
32    // Total score calculations performed.
33    pub score_calculations: u64,
34    pub construction_slots_assigned: u64,
35    pub construction_slots_kept: u64,
36    pub construction_slots_no_doable: u64,
37    pub coverage_required_remaining: u64,
38    coverage_required_remaining_by_group: BTreeMap<&'static str, u64>,
39    generation_time: Duration,
40    evaluation_time: Duration,
41    selector_stats: Vec<SelectorTelemetry>,
42}
43
44impl SolverStats {
45    /// Marks the start of solving.
46    pub fn start(&mut self) {
47        self.start_time = Some(Instant::now());
48        self.pause_started_at = None;
49    }
50
51    pub fn elapsed(&self) -> Duration {
52        match (self.start_time, self.pause_started_at) {
53            (Some(start), Some(paused_at)) => paused_at.duration_since(start),
54            (Some(start), None) => start.elapsed(),
55            _ => Duration::default(),
56        }
57    }
58
59    pub fn pause(&mut self) {
60        if self.start_time.is_some() && self.pause_started_at.is_none() {
61            self.pause_started_at = Some(Instant::now());
62        }
63    }
64
65    pub fn resume(&mut self) {
66        if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
67            self.start_time = Some(start + paused_at.elapsed());
68        }
69    }
70
71    /// Records one or more generated candidate moves and the time spent generating them.
72    pub fn record_generated_batch(&mut self, count: u64, duration: Duration) {
73        self.moves_generated += count;
74        self.generation_time += duration;
75    }
76
77    pub fn record_selector_generated(
78        &mut self,
79        selector_index: usize,
80        count: u64,
81        duration: Duration,
82    ) {
83        self.record_generated_batch(count, duration);
84        let selector = self.selector_stats_entry(selector_index);
85        selector.moves_generated += count;
86        selector.generation_time += duration;
87    }
88
89    /// Records generation time that did not itself yield a counted move.
90    pub fn record_generation_time(&mut self, duration: Duration) {
91        self.generation_time += duration;
92    }
93
94    /// Records a single generated candidate move and the time spent generating it.
95    pub fn record_generated_move(&mut self, duration: Duration) {
96        self.record_generated_batch(1, duration);
97    }
98
99    /// Records a move evaluation and the time spent evaluating it.
100    pub fn record_evaluated_move(&mut self, duration: Duration) {
101        self.moves_evaluated += 1;
102        self.evaluation_time += duration;
103    }
104
105    pub fn record_selector_evaluated(&mut self, selector_index: usize, duration: Duration) {
106        self.record_evaluated_move(duration);
107        let selector = self.selector_stats_entry(selector_index);
108        selector.moves_evaluated += 1;
109        selector.evaluation_time += duration;
110    }
111
112    /// Records an accepted move.
113    pub fn record_move_accepted(&mut self) {
114        self.moves_accepted += 1;
115    }
116
117    pub fn record_selector_accepted(&mut self, selector_index: usize) {
118        self.record_move_accepted();
119        self.selector_stats_entry(selector_index).moves_accepted += 1;
120    }
121
122    pub fn record_move_applied(&mut self) {
123        self.moves_applied += 1;
124    }
125
126    pub fn record_selector_applied(&mut self, selector_index: usize) {
127        self.record_move_applied();
128        self.selector_stats_entry(selector_index).moves_applied += 1;
129    }
130
131    pub fn record_move_not_doable(&mut self) {
132        self.moves_not_doable += 1;
133    }
134
135    pub fn record_selector_not_doable(&mut self, selector_index: usize) {
136        self.record_move_not_doable();
137        self.selector_stats_entry(selector_index).moves_not_doable += 1;
138    }
139
140    pub fn record_move_acceptor_rejected(&mut self) {
141        self.moves_acceptor_rejected += 1;
142    }
143
144    pub fn record_selector_acceptor_rejected(&mut self, selector_index: usize) {
145        self.record_move_acceptor_rejected();
146        self.selector_stats_entry(selector_index)
147            .moves_acceptor_rejected += 1;
148    }
149
150    pub fn record_moves_forager_ignored(&mut self, count: u64) {
151        self.moves_forager_ignored += count;
152    }
153
154    pub fn record_move_hard_improving(&mut self) {
155        self.moves_hard_improving += 1;
156    }
157
158    pub fn record_move_hard_neutral(&mut self) {
159        self.moves_hard_neutral += 1;
160    }
161
162    pub fn record_move_hard_worse(&mut self) {
163        self.moves_hard_worse += 1;
164    }
165
166    pub fn record_conflict_repair_provider_generated(&mut self, count: u64) {
167        self.conflict_repair_provider_generated += count;
168    }
169
170    pub fn record_conflict_repair_duplicate_filtered(&mut self) {
171        self.conflict_repair_duplicate_filtered += 1;
172    }
173
174    pub fn record_conflict_repair_illegal_filtered(&mut self) {
175        self.conflict_repair_illegal_filtered += 1;
176    }
177
178    pub fn record_conflict_repair_not_doable_filtered(&mut self) {
179        self.conflict_repair_not_doable_filtered += 1;
180    }
181
182    pub fn record_conflict_repair_hard_improving(&mut self) {
183        self.conflict_repair_hard_improving += 1;
184    }
185
186    pub fn record_conflict_repair_exposed(&mut self) {
187        self.conflict_repair_exposed += 1;
188    }
189
190    /// Records a step completion.
191    pub fn record_step(&mut self) {
192        self.step_count += 1;
193    }
194
195    /// Records a score calculation.
196    pub fn record_score_calculation(&mut self) {
197        self.score_calculations += 1;
198    }
199
200    pub fn record_construction_slot_assigned(&mut self) {
201        self.construction_slots_assigned += 1;
202    }
203
204    pub fn record_construction_slot_kept(&mut self) {
205        self.construction_slots_kept += 1;
206    }
207
208    pub fn record_construction_slot_no_doable(&mut self) {
209        self.construction_slots_no_doable += 1;
210    }
211
212    pub fn record_coverage_required_remaining(&mut self, group_name: &'static str, count: u64) {
213        self.coverage_required_remaining_by_group
214            .insert(group_name, count);
215        self.coverage_required_remaining = self
216            .coverage_required_remaining_by_group
217            .values()
218            .copied()
219            .sum();
220    }
221
222    pub fn generated_throughput(&self) -> Throughput {
223        Throughput {
224            count: self.moves_generated,
225            elapsed: self.generation_time,
226        }
227    }
228
229    pub fn evaluated_throughput(&self) -> Throughput {
230        Throughput {
231            count: self.moves_evaluated,
232            elapsed: self.evaluation_time,
233        }
234    }
235
236    pub fn acceptance_rate(&self) -> f64 {
237        if self.moves_evaluated == 0 {
238            0.0
239        } else {
240            self.moves_accepted as f64 / self.moves_evaluated as f64
241        }
242    }
243
244    pub fn generation_time(&self) -> Duration {
245        self.generation_time
246    }
247
248    pub fn evaluation_time(&self) -> Duration {
249        self.evaluation_time
250    }
251
252    pub fn snapshot(&self) -> SolverTelemetry {
253        SolverTelemetry {
254            elapsed: self.elapsed(),
255            step_count: self.step_count,
256            moves_generated: self.moves_generated,
257            moves_evaluated: self.moves_evaluated,
258            moves_accepted: self.moves_accepted,
259            moves_applied: self.moves_applied,
260            moves_not_doable: self.moves_not_doable,
261            moves_acceptor_rejected: self.moves_acceptor_rejected,
262            moves_forager_ignored: self.moves_forager_ignored,
263            moves_hard_improving: self.moves_hard_improving,
264            moves_hard_neutral: self.moves_hard_neutral,
265            moves_hard_worse: self.moves_hard_worse,
266            conflict_repair_provider_generated: self.conflict_repair_provider_generated,
267            conflict_repair_duplicate_filtered: self.conflict_repair_duplicate_filtered,
268            conflict_repair_illegal_filtered: self.conflict_repair_illegal_filtered,
269            conflict_repair_not_doable_filtered: self.conflict_repair_not_doable_filtered,
270            conflict_repair_hard_improving: self.conflict_repair_hard_improving,
271            conflict_repair_exposed: self.conflict_repair_exposed,
272            score_calculations: self.score_calculations,
273            construction_slots_assigned: self.construction_slots_assigned,
274            construction_slots_kept: self.construction_slots_kept,
275            construction_slots_no_doable: self.construction_slots_no_doable,
276            coverage_required_remaining: self.coverage_required_remaining,
277            generation_time: self.generation_time,
278            evaluation_time: self.evaluation_time,
279            selector_telemetry: self.selector_stats.clone(),
280        }
281    }
282
283    pub fn record_selector_generated_with_label(
284        &mut self,
285        selector_index: usize,
286        selector_label: impl Into<String>,
287        count: u64,
288        duration: Duration,
289    ) {
290        self.record_generated_batch(count, duration);
291        let selector = self.selector_stats_entry_with_label(selector_index, selector_label);
292        selector.moves_generated += count;
293        selector.generation_time += duration;
294    }
295
296    fn selector_stats_entry(&mut self, selector_index: usize) -> &mut SelectorTelemetry {
297        self.selector_stats_entry_with_label(selector_index, format!("selector-{selector_index}"))
298    }
299
300    fn selector_stats_entry_with_label(
301        &mut self,
302        selector_index: usize,
303        selector_label: impl Into<String>,
304    ) -> &mut SelectorTelemetry {
305        let selector_label = selector_label.into();
306        if let Some(position) = self
307            .selector_stats
308            .iter()
309            .position(|entry| entry.selector_index == selector_index)
310        {
311            if self.selector_stats[position]
312                .selector_label
313                .starts_with("selector-")
314                && !selector_label.starts_with("selector-")
315            {
316                self.selector_stats[position].selector_label = selector_label;
317            }
318            return &mut self.selector_stats[position];
319        }
320        self.selector_stats.push(SelectorTelemetry {
321            selector_index,
322            selector_label,
323            ..SelectorTelemetry::default()
324        });
325        self.selector_stats
326            .last_mut()
327            .expect("selector stats entry was just inserted")
328    }
329}
330
331/* Phase-level statistics.
332
333Tracks metrics for a single solver phase.
334
335# Example
336
337```
338use solverforge_solver::stats::PhaseStats;
339use std::time::Duration;
340
341let mut stats = PhaseStats::new(0, "LocalSearch");
342stats.record_step();
343stats.record_generated_move(Duration::from_millis(1));
344stats.record_evaluated_move(Duration::from_millis(2));
345stats.record_move_accepted();
346
347assert_eq!(stats.phase_index, 0);
348assert_eq!(stats.phase_type, "LocalSearch");
349assert_eq!(stats.step_count, 1);
350assert_eq!(stats.moves_accepted, 1);
351```
352*/