tacet_core/preflight/
resolution.rs1extern crate alloc;
16
17use alloc::string::String;
18use alloc::vec::Vec;
19
20use crate::result::{PreflightCategory, PreflightSeverity, PreflightWarningInfo};
21
22#[derive(Debug, Clone)]
24#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
25pub enum ResolutionWarning {
26 InsufficientResolution {
33 unique_values: usize,
35 total_samples: usize,
37 zero_fraction: f64,
39 timer_resolution_ns: f64,
41 },
42
43 HighQuantization {
50 unique_values: usize,
52 total_samples: usize,
54 },
55}
56
57impl ResolutionWarning {
58 pub fn is_result_undermining(&self) -> bool {
60 matches!(self, ResolutionWarning::InsufficientResolution { .. })
61 }
62
63 pub fn severity(&self) -> PreflightSeverity {
65 match self {
66 ResolutionWarning::InsufficientResolution { .. } => {
67 PreflightSeverity::ResultUndermining
68 }
69 ResolutionWarning::HighQuantization { .. } => PreflightSeverity::Informational,
70 }
71 }
72
73 pub fn description(&self) -> String {
75 match self {
76 ResolutionWarning::InsufficientResolution {
77 unique_values,
78 total_samples,
79 zero_fraction,
80 timer_resolution_ns,
81 } => {
82 alloc::format!(
83 "Timer resolution (~{:.0}ns) is too coarse for this operation. \
84 Only {} unique values in {} samples ({:.0}% are zero).",
85 timer_resolution_ns,
86 unique_values,
87 total_samples,
88 zero_fraction * 100.0
89 )
90 }
91 ResolutionWarning::HighQuantization {
92 unique_values,
93 total_samples,
94 } => {
95 alloc::format!(
96 "High quantization: only {} unique values in {} samples. \
97 Timer resolution may be affecting measurement quality.",
98 unique_values,
99 total_samples
100 )
101 }
102 }
103 }
104
105 pub fn guidance(&self) -> Option<String> {
110 match self {
111 ResolutionWarning::InsufficientResolution { .. } => Some(
112 "Consider: (1) measuring multiple iterations per sample, \
113 (2) using a more complex operation, or \
114 (3) enabling cycle-accurate timing for ~0.3ns resolution."
115 .into(),
116 ),
117 ResolutionWarning::HighQuantization { .. } => Some(
118 "Timer resolution may be limiting measurement quality.".into(),
119 ),
120 }
121 }
122
123 pub fn to_warning_info(&self) -> PreflightWarningInfo {
125 match self.guidance() {
126 Some(guidance) => PreflightWarningInfo::with_guidance(
127 PreflightCategory::Resolution,
128 self.severity(),
129 self.description(),
130 guidance,
131 ),
132 None => PreflightWarningInfo::new(
133 PreflightCategory::Resolution,
134 self.severity(),
135 self.description(),
136 ),
137 }
138 }
139}
140
141const MIN_UNIQUE_PER_1000: usize = 20;
143
144const CRITICAL_ZERO_FRACTION: f64 = 0.5;
146
147pub fn resolution_check(samples: &[f64], timer_resolution_ns: f64) -> Option<ResolutionWarning> {
162 if samples.len() < 100 {
163 return None; }
165
166 let mut sorted: Vec<f64> = samples.to_vec();
168 sorted.sort_by(|a, b| a.total_cmp(b));
169
170 let mut unique_count = 1;
171 let mut last_value = sorted[0];
172 for &val in &sorted[1..] {
173 if (val - last_value).abs() > 0.1 {
175 unique_count += 1;
176 last_value = val;
177 }
178 }
179
180 let zero_count = samples.iter().filter(|&&x| x.abs() < 0.1).count();
182 let zero_fraction = zero_count as f64 / samples.len() as f64;
183
184 let expected_unique =
186 (samples.len() as f64 / 1000.0 * MIN_UNIQUE_PER_1000 as f64).max(10.0) as usize;
187
188 if unique_count < expected_unique && zero_fraction > CRITICAL_ZERO_FRACTION {
189 return Some(ResolutionWarning::InsufficientResolution {
190 unique_values: unique_count,
191 total_samples: samples.len(),
192 zero_fraction,
193 timer_resolution_ns,
194 });
195 }
196
197 if unique_count < expected_unique / 2 {
199 return Some(ResolutionWarning::HighQuantization {
200 unique_values: unique_count,
201 total_samples: samples.len(),
202 });
203 }
204
205 None
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn test_insufficient_resolution() {
214 let mut samples = alloc::vec![0.0; 800];
216 samples.extend(alloc::vec![41.0; 150]);
217 samples.extend(alloc::vec![82.0; 50]);
218
219 let result = resolution_check(&samples, 41.0);
220 assert!(result.is_some(), "Should detect insufficient resolution");
221 assert!(
222 result.as_ref().unwrap().is_result_undermining(),
223 "Should be result-undermining"
224 );
225 assert_eq!(
226 result.as_ref().unwrap().severity(),
227 PreflightSeverity::ResultUndermining
228 );
229 }
230
231 #[test]
232 fn test_severity() {
233 let insufficient = ResolutionWarning::InsufficientResolution {
234 unique_values: 3,
235 total_samples: 1000,
236 zero_fraction: 0.8,
237 timer_resolution_ns: 41.0,
238 };
239 assert_eq!(
240 insufficient.severity(),
241 PreflightSeverity::ResultUndermining
242 );
243 assert!(insufficient.is_result_undermining());
244
245 let high_quant = ResolutionWarning::HighQuantization {
246 unique_values: 5,
247 total_samples: 1000,
248 };
249 assert_eq!(high_quant.severity(), PreflightSeverity::Informational);
250 assert!(!high_quant.is_result_undermining());
251 }
252}