Skip to main content

jugar_probar/playbook/
complexity.rs

1//! Empirical computational complexity verification.
2//!
3//! Implements O(n) complexity detection via curve fitting.
4//! Reference: Goldsmith et al., "Measuring Empirical Computational Complexity" (ESEC/FSE 2007)
5
6use super::schema::ComplexityClass;
7
8/// Result of complexity analysis.
9#[derive(Debug, Clone)]
10pub struct ComplexityResult {
11    /// Detected complexity class
12    pub detected_class: ComplexityClass,
13    /// Confidence score (0.0 - 1.0)
14    pub confidence: f64,
15    /// R² value for the best fit
16    pub r_squared: f64,
17    /// Whether the complexity violates the expected class
18    pub is_violation: bool,
19    /// Expected complexity (if specified)
20    pub expected_class: Option<ComplexityClass>,
21    /// Detailed fit results for each complexity class
22    pub fit_results: Vec<FitResult>,
23}
24
25/// Result of fitting data to a complexity class.
26#[derive(Debug, Clone)]
27pub struct FitResult {
28    /// Complexity class
29    pub class: ComplexityClass,
30    /// R² coefficient of determination
31    pub r_squared: f64,
32    /// Fitted coefficients
33    pub coefficients: Vec<f64>,
34}
35
36/// Complexity analyzer using empirical curve fitting.
37pub struct ComplexityAnalyzer {
38    /// Input sizes (n values)
39    input_sizes: Vec<f64>,
40    /// Measured execution times
41    execution_times: Vec<f64>,
42}
43
44impl ComplexityAnalyzer {
45    /// Create a new analyzer with measurement data.
46    ///
47    /// # Arguments
48    /// * `measurements` - Pairs of (input_size, execution_time)
49    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    /// Analyze the measurements and determine complexity class.
60    ///
61    /// # Arguments
62    /// * `expected` - Optional expected complexity class for violation detection
63    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        // Fit data to each complexity class
76        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        // Find best fit (highest R², prefer simpler models as tiebreaker)
85        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                        // Prefer simpler models (lower complexity order) when R² is equal
91                        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        // Check for violation
103        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    /// Fit to O(1) - constant time.
120    fn fit_constant(&self) -> FitResult {
121        // y = c
122        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    /// Fit to O(log n) - logarithmic.
133    fn fit_logarithmic(&self) -> FitResult {
134        // y = a * log(n) + b
135        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    /// Fit to O(n) - linear.
147    fn fit_linear(&self) -> FitResult {
148        // y = a * n + b
149        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    /// Fit to O(n log n) - linearithmic.
160    fn fit_linearithmic(&self) -> FitResult {
161        // y = a * n * log(n) + b
162        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    /// Fit to O(n²) - quadratic.
174    fn fit_quadratic(&self) -> FitResult {
175        // y = a * n² + b
176        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    /// Simple linear regression: y = ax + b
188    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    /// Compute R² (coefficient of determination) for a model.
207    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; // Perfect fit if no variance
228        }
229
230        1.0 - (ss_res / ss_tot)
231    }
232}
233
234/// Get numerical order for complexity comparison.
235fn 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
245/// Check if measured complexity violates expected complexity.
246pub 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        // Generate O(n) data: t = 2n + 5
262        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        // Generate O(n²) data: t = n² + 10
276        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        // Generate O(1) data: t = 100 (exactly constant)
290        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        // Constant data should be detected as constant
296        assert_eq!(result.detected_class, ComplexityClass::Constant);
297    }
298
299    #[test]
300    fn test_violation_detection() {
301        // Generate O(n²) data but expect O(n)
302        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        // Generate O(n) data but expect O(n²)
316        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        // Only 2 points - not enough for reliable analysis
327        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}