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}