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)]
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    // Total steps taken across all phases.
43    pub step_count: u64,
44    // Total moves evaluated across all phases.
45    pub moves_evaluated: u64,
46    // Total moves accepted across all phases.
47    pub moves_accepted: u64,
48    // Total score calculations performed.
49    pub score_calculations: u64,
50}
51
52impl SolverStats {
53    /// Marks the start of solving.
54    pub fn start(&mut self) {
55        self.start_time = Some(Instant::now());
56    }
57
58    pub fn elapsed(&self) -> Duration {
59        self.start_time.map(|t| t.elapsed()).unwrap_or_default()
60    }
61
62    /// Records a move evaluation and whether it was accepted.
63    pub fn record_move(&mut self, accepted: bool) {
64        self.moves_evaluated += 1;
65        if accepted {
66            self.moves_accepted += 1;
67        }
68    }
69
70    /// Records a step completion.
71    pub fn record_step(&mut self) {
72        self.step_count += 1;
73    }
74
75    /// Records a score calculation.
76    pub fn record_score_calculation(&mut self) {
77        self.score_calculations += 1;
78    }
79
80    pub fn moves_per_second(&self) -> f64 {
81        let secs = self.elapsed().as_secs_f64();
82        if secs > 0.0 {
83            self.moves_evaluated as f64 / secs
84        } else {
85            0.0
86        }
87    }
88
89    pub fn acceptance_rate(&self) -> f64 {
90        if self.moves_evaluated == 0 {
91            0.0
92        } else {
93            self.moves_accepted as f64 / self.moves_evaluated as f64
94        }
95    }
96
97    pub fn snapshot(&self) -> SolverTelemetry {
98        SolverTelemetry {
99            elapsed_ms: self.elapsed().as_millis() as u64,
100            step_count: self.step_count,
101            moves_evaluated: self.moves_evaluated,
102            moves_accepted: self.moves_accepted,
103            score_calculations: self.score_calculations,
104            moves_per_second: self.moves_per_second() as u64,
105            acceptance_rate: self.acceptance_rate(),
106        }
107    }
108}
109
110/* Phase-level statistics.
111
112Tracks metrics for a single solver phase.
113
114# Example
115
116```
117use solverforge_solver::stats::PhaseStats;
118
119let mut stats = PhaseStats::new(0, "LocalSearch");
120stats.record_step();
121stats.record_move(true);
122
123assert_eq!(stats.phase_index, 0);
124assert_eq!(stats.phase_type, "LocalSearch");
125assert_eq!(stats.step_count, 1);
126assert_eq!(stats.moves_accepted, 1);
127```
128*/
129#[derive(Debug)]
130pub struct PhaseStats {
131    // Index of this phase (0-based).
132    pub phase_index: usize,
133    // Type name of the phase.
134    pub phase_type: &'static str,
135    start_time: Instant,
136    // Number of steps taken in this phase.
137    pub step_count: u64,
138    // Number of moves evaluated in this phase.
139    pub moves_evaluated: u64,
140    // Number of moves accepted in this phase.
141    pub moves_accepted: u64,
142}
143
144impl PhaseStats {
145    /// Creates new phase statistics.
146    pub fn new(phase_index: usize, phase_type: &'static str) -> Self {
147        Self {
148            phase_index,
149            phase_type,
150            start_time: Instant::now(),
151            step_count: 0,
152            moves_evaluated: 0,
153            moves_accepted: 0,
154        }
155    }
156
157    pub fn elapsed(&self) -> Duration {
158        self.start_time.elapsed()
159    }
160
161    pub fn elapsed_ms(&self) -> u64 {
162        self.start_time.elapsed().as_millis() as u64
163    }
164
165    /// Records a step completion.
166    pub fn record_step(&mut self) {
167        self.step_count += 1;
168    }
169
170    /// Records a move evaluation and whether it was accepted.
171    pub fn record_move(&mut self, accepted: bool) {
172        self.moves_evaluated += 1;
173        if accepted {
174            self.moves_accepted += 1;
175        }
176    }
177
178    pub fn moves_per_second(&self) -> u64 {
179        let secs = self.elapsed().as_secs_f64();
180        if secs > 0.0 {
181            (self.moves_evaluated as f64 / secs) as u64
182        } else {
183            0
184        }
185    }
186
187    pub fn acceptance_rate(&self) -> f64 {
188        if self.moves_evaluated == 0 {
189            0.0
190        } else {
191            self.moves_accepted as f64 / self.moves_evaluated as f64
192        }
193    }
194}