1use super::schema::ComplexityClass;
7
8#[derive(Debug, Clone)]
10pub struct ComplexityResult {
11 pub detected_class: ComplexityClass,
13 pub confidence: f64,
15 pub r_squared: f64,
17 pub is_violation: bool,
19 pub expected_class: Option<ComplexityClass>,
21 pub fit_results: Vec<FitResult>,
23}
24
25#[derive(Debug, Clone)]
27pub struct FitResult {
28 pub class: ComplexityClass,
30 pub r_squared: f64,
32 pub coefficients: Vec<f64>,
34}
35
36pub struct ComplexityAnalyzer {
38 input_sizes: Vec<f64>,
40 execution_times: Vec<f64>,
42}
43
44impl ComplexityAnalyzer {
45 pub fn new(measurements: Vec<(usize, f64)>) -> Self {
50 let (input_sizes, execution_times): (Vec<f64>, Vec<f64>) =
51 measurements.into_iter().map(|(n, t)| (n as f64, t)).unzip();
52
53 Self {
54 input_sizes,
55 execution_times,
56 }
57 }
58
59 pub fn analyze(&self, expected: Option<ComplexityClass>) -> ComplexityResult {
64 if self.input_sizes.len() < 3 {
65 return ComplexityResult {
66 detected_class: ComplexityClass::Linear,
67 confidence: 0.0,
68 r_squared: 0.0,
69 is_violation: false,
70 expected_class: expected,
71 fit_results: Vec::new(),
72 };
73 }
74
75 let fit_results = vec![
77 self.fit_constant(),
78 self.fit_logarithmic(),
79 self.fit_linear(),
80 self.fit_linearithmic(),
81 self.fit_quadratic(),
82 ];
83
84 let best_fit = fit_results
86 .iter()
87 .max_by(|a, b| {
88 match a.r_squared.partial_cmp(&b.r_squared) {
89 Some(std::cmp::Ordering::Equal) => {
90 complexity_order(b.class).cmp(&complexity_order(a.class))
92 }
93 Some(ord) => ord,
94 None => std::cmp::Ordering::Equal,
95 }
96 })
97 .expect("fit_results is non-empty");
98
99 let detected_class = best_fit.class;
100 let confidence = best_fit.r_squared;
101
102 let is_violation = if let Some(exp) = expected {
104 complexity_order(detected_class) > complexity_order(exp)
105 } else {
106 false
107 };
108
109 ComplexityResult {
110 detected_class,
111 confidence,
112 r_squared: best_fit.r_squared,
113 is_violation,
114 expected_class: expected,
115 fit_results,
116 }
117 }
118
119 fn fit_constant(&self) -> FitResult {
121 let mean = self.execution_times.iter().sum::<f64>() / self.execution_times.len() as f64;
123 let r_squared = self.compute_r_squared(|_| mean);
124
125 FitResult {
126 class: ComplexityClass::Constant,
127 r_squared,
128 coefficients: vec![mean],
129 }
130 }
131
132 fn fit_logarithmic(&self) -> FitResult {
134 let log_n: Vec<f64> = self.input_sizes.iter().map(|n| n.ln()).collect();
136 let (a, b) = self.linear_regression(&log_n, &self.execution_times);
137 let r_squared = self.compute_r_squared(|n| a * n.ln() + b);
138
139 FitResult {
140 class: ComplexityClass::Logarithmic,
141 r_squared,
142 coefficients: vec![a, b],
143 }
144 }
145
146 fn fit_linear(&self) -> FitResult {
148 let (a, b) = self.linear_regression(&self.input_sizes, &self.execution_times);
150 let r_squared = self.compute_r_squared(|n| a * n + b);
151
152 FitResult {
153 class: ComplexityClass::Linear,
154 r_squared,
155 coefficients: vec![a, b],
156 }
157 }
158
159 fn fit_linearithmic(&self) -> FitResult {
161 let n_log_n: Vec<f64> = self.input_sizes.iter().map(|n| n * n.ln()).collect();
163 let (a, b) = self.linear_regression(&n_log_n, &self.execution_times);
164 let r_squared = self.compute_r_squared(|n| a * n * n.ln() + b);
165
166 FitResult {
167 class: ComplexityClass::Linearithmic,
168 r_squared,
169 coefficients: vec![a, b],
170 }
171 }
172
173 fn fit_quadratic(&self) -> FitResult {
175 let n_squared: Vec<f64> = self.input_sizes.iter().map(|n| n * n).collect();
177 let (a, b) = self.linear_regression(&n_squared, &self.execution_times);
178 let r_squared = self.compute_r_squared(|n| a * n * n + b);
179
180 FitResult {
181 class: ComplexityClass::Quadratic,
182 r_squared,
183 coefficients: vec![a, b],
184 }
185 }
186
187 fn linear_regression(&self, x: &[f64], y: &[f64]) -> (f64, f64) {
189 let n = x.len() as f64;
190 let sum_x: f64 = x.iter().sum();
191 let sum_y: f64 = y.iter().sum();
192 let sum_xy: f64 = x.iter().zip(y.iter()).map(|(xi, yi)| xi * yi).sum();
193 let sum_xx: f64 = x.iter().map(|xi| xi * xi).sum();
194
195 let denom = n * sum_xx - sum_x * sum_x;
196 if denom.abs() < f64::EPSILON {
197 return (0.0, sum_y / n);
198 }
199
200 let a = (n * sum_xy - sum_x * sum_y) / denom;
201 let b = (sum_y - a * sum_x) / n;
202
203 (a, b)
204 }
205
206 fn compute_r_squared<F>(&self, model: F) -> f64
208 where
209 F: Fn(f64) -> f64,
210 {
211 let y_mean = self.execution_times.iter().sum::<f64>() / self.execution_times.len() as f64;
212
213 let ss_tot: f64 = self
214 .execution_times
215 .iter()
216 .map(|y| (y - y_mean).powi(2))
217 .sum();
218
219 let ss_res: f64 = self
220 .input_sizes
221 .iter()
222 .zip(self.execution_times.iter())
223 .map(|(n, y)| (y - model(*n)).powi(2))
224 .sum();
225
226 if ss_tot.abs() < f64::EPSILON {
227 return 1.0; }
229
230 1.0 - (ss_res / ss_tot)
231 }
232}
233
234fn complexity_order(class: ComplexityClass) -> u8 {
236 match class {
237 ComplexityClass::Constant => 0,
238 ComplexityClass::Logarithmic => 1,
239 ComplexityClass::Linear => 2,
240 ComplexityClass::Linearithmic => 3,
241 ComplexityClass::Quadratic => 4,
242 }
243}
244
245pub fn check_complexity_violation(
247 measurements: Vec<(usize, f64)>,
248 expected: ComplexityClass,
249) -> ComplexityResult {
250 let analyzer = ComplexityAnalyzer::new(measurements);
251 analyzer.analyze(Some(expected))
252}
253
254#[cfg(test)]
255#[allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_detect_linear_complexity() {
261 let measurements: Vec<(usize, f64)> = (1..=10)
263 .map(|n| (n * 100, (2 * n * 100) as f64 + 5.0))
264 .collect();
265
266 let analyzer = ComplexityAnalyzer::new(measurements);
267 let result = analyzer.analyze(None);
268
269 assert_eq!(result.detected_class, ComplexityClass::Linear);
270 assert!(result.r_squared > 0.99);
271 }
272
273 #[test]
274 fn test_detect_quadratic_complexity() {
275 let measurements: Vec<(usize, f64)> = (1..=10)
277 .map(|n| (n * 10, ((n * 10) * (n * 10)) as f64 + 10.0))
278 .collect();
279
280 let analyzer = ComplexityAnalyzer::new(measurements);
281 let result = analyzer.analyze(None);
282
283 assert_eq!(result.detected_class, ComplexityClass::Quadratic);
284 assert!(result.r_squared > 0.99);
285 }
286
287 #[test]
288 fn test_detect_constant_complexity() {
289 let measurements: Vec<(usize, f64)> = (1..=10).map(|n| (n * 100, 100.0)).collect();
291
292 let analyzer = ComplexityAnalyzer::new(measurements);
293 let result = analyzer.analyze(None);
294
295 assert_eq!(result.detected_class, ComplexityClass::Constant);
297 }
298
299 #[test]
300 fn test_violation_detection() {
301 let measurements: Vec<(usize, f64)> = (1..=10)
303 .map(|n| (n * 10, ((n * 10) * (n * 10)) as f64))
304 .collect();
305
306 let result = check_complexity_violation(measurements, ComplexityClass::Linear);
307
308 assert!(result.is_violation);
309 assert_eq!(result.expected_class, Some(ComplexityClass::Linear));
310 assert_eq!(result.detected_class, ComplexityClass::Quadratic);
311 }
312
313 #[test]
314 fn test_no_violation_when_better() {
315 let measurements: Vec<(usize, f64)> =
317 (1..=10).map(|n| (n * 100, (n * 100) as f64)).collect();
318
319 let result = check_complexity_violation(measurements, ComplexityClass::Quadratic);
320
321 assert!(!result.is_violation);
322 }
323
324 #[test]
325 fn test_insufficient_data() {
326 let measurements = vec![(100, 100.0), (200, 200.0)];
328
329 let analyzer = ComplexityAnalyzer::new(measurements);
330 let result = analyzer.analyze(None);
331
332 assert_eq!(result.confidence, 0.0);
333 }
334}