Skip to main content

solverforge_solver/stats/
solver.rs

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