solverforge_solver/
stats.rs

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