Skip to main content

solverforge_solver/
stats.rs

1/* Solver statistics (zero-erasure).
2
3Stack-allocated statistics for solver and phase performance tracking.
4*/
5
6use std::time::{Duration, Instant};
7
8/* Solver-level statistics.
9
10Tracks aggregate metrics across all phases of a solve run.
11
12# Example
13
14```
15use solverforge_solver::stats::SolverStats;
16
17let mut stats = SolverStats::default();
18stats.start();
19stats.record_step();
20stats.record_move(true);
21stats.record_move(false);
22
23assert_eq!(stats.step_count, 1);
24assert_eq!(stats.moves_evaluated, 2);
25assert_eq!(stats.moves_accepted, 1);
26```
27*/
28#[derive(Debug, Clone, Copy, Default, PartialEq)]
29pub struct SolverTelemetry {
30    pub elapsed_ms: u64,
31    pub step_count: u64,
32    pub moves_evaluated: u64,
33    pub moves_accepted: u64,
34    pub score_calculations: u64,
35    pub moves_per_second: u64,
36    pub acceptance_rate: f64,
37}
38
39#[derive(Debug, Default)]
40pub struct SolverStats {
41    start_time: Option<Instant>,
42    pause_started_at: Option<Instant>,
43    // Total steps taken across all phases.
44    pub step_count: u64,
45    // Total moves evaluated across all phases.
46    pub moves_evaluated: u64,
47    // Total moves accepted across all phases.
48    pub moves_accepted: u64,
49    // Total score calculations performed.
50    pub score_calculations: u64,
51}
52
53impl SolverStats {
54    /// Marks the start of solving.
55    pub fn start(&mut self) {
56        self.start_time = Some(Instant::now());
57        self.pause_started_at = None;
58    }
59
60    pub fn elapsed(&self) -> Duration {
61        match (self.start_time, self.pause_started_at) {
62            (Some(start), Some(paused_at)) => paused_at.duration_since(start),
63            (Some(start), None) => start.elapsed(),
64            _ => Duration::default(),
65        }
66    }
67
68    pub fn pause(&mut self) {
69        if self.start_time.is_some() && self.pause_started_at.is_none() {
70            self.pause_started_at = Some(Instant::now());
71        }
72    }
73
74    pub fn resume(&mut self) {
75        if let (Some(start), Some(paused_at)) = (self.start_time, self.pause_started_at.take()) {
76            self.start_time = Some(start + paused_at.elapsed());
77        }
78    }
79
80    /// Records a move evaluation and whether it was accepted.
81    pub fn record_move(&mut self, accepted: bool) {
82        self.moves_evaluated += 1;
83        if accepted {
84            self.moves_accepted += 1;
85        }
86    }
87
88    /// Records a step completion.
89    pub fn record_step(&mut self) {
90        self.step_count += 1;
91    }
92
93    /// Records a score calculation.
94    pub fn record_score_calculation(&mut self) {
95        self.score_calculations += 1;
96    }
97
98    pub fn moves_per_second(&self) -> f64 {
99        let secs = self.elapsed().as_secs_f64();
100        if secs > 0.0 {
101            self.moves_evaluated as f64 / secs
102        } else {
103            0.0
104        }
105    }
106
107    pub fn acceptance_rate(&self) -> f64 {
108        if self.moves_evaluated == 0 {
109            0.0
110        } else {
111            self.moves_accepted as f64 / self.moves_evaluated as f64
112        }
113    }
114
115    pub fn snapshot(&self) -> SolverTelemetry {
116        SolverTelemetry {
117            elapsed_ms: self.elapsed().as_millis() as u64,
118            step_count: self.step_count,
119            moves_evaluated: self.moves_evaluated,
120            moves_accepted: self.moves_accepted,
121            score_calculations: self.score_calculations,
122            moves_per_second: self.moves_per_second() as u64,
123            acceptance_rate: self.acceptance_rate(),
124        }
125    }
126}
127
128/* Phase-level statistics.
129
130Tracks metrics for a single solver phase.
131
132# Example
133
134```
135use solverforge_solver::stats::PhaseStats;
136
137let mut stats = PhaseStats::new(0, "LocalSearch");
138stats.record_step();
139stats.record_move(true);
140
141assert_eq!(stats.phase_index, 0);
142assert_eq!(stats.phase_type, "LocalSearch");
143assert_eq!(stats.step_count, 1);
144assert_eq!(stats.moves_accepted, 1);
145```
146*/
147#[derive(Debug)]
148pub struct PhaseStats {
149    // Index of this phase (0-based).
150    pub phase_index: usize,
151    // Type name of the phase.
152    pub phase_type: &'static str,
153    start_time: Instant,
154    // Number of steps taken in this phase.
155    pub step_count: u64,
156    // Number of moves evaluated in this phase.
157    pub moves_evaluated: u64,
158    // Number of moves accepted in this phase.
159    pub moves_accepted: u64,
160}
161
162impl PhaseStats {
163    /// Creates new phase statistics.
164    pub fn new(phase_index: usize, phase_type: &'static str) -> Self {
165        Self {
166            phase_index,
167            phase_type,
168            start_time: Instant::now(),
169            step_count: 0,
170            moves_evaluated: 0,
171            moves_accepted: 0,
172        }
173    }
174
175    pub fn elapsed(&self) -> Duration {
176        self.start_time.elapsed()
177    }
178
179    pub fn elapsed_ms(&self) -> u64 {
180        self.start_time.elapsed().as_millis() as u64
181    }
182
183    /// Records a step completion.
184    pub fn record_step(&mut self) {
185        self.step_count += 1;
186    }
187
188    /// Records a move evaluation and whether it was accepted.
189    pub fn record_move(&mut self, accepted: bool) {
190        self.moves_evaluated += 1;
191        if accepted {
192            self.moves_accepted += 1;
193        }
194    }
195
196    pub fn moves_per_second(&self) -> u64 {
197        let secs = self.elapsed().as_secs_f64();
198        if secs > 0.0 {
199            (self.moves_evaluated as f64 / secs) as u64
200        } else {
201            0
202        }
203    }
204
205    pub fn acceptance_rate(&self) -> f64 {
206        if self.moves_evaluated == 0 {
207            0.0
208        } else {
209            self.moves_accepted as f64 / self.moves_evaluated as f64
210        }
211    }
212}