| //! Checks that a set of measurements looks like a linear function rather than | 
 | //! like a quadratic function. Algorithm: | 
 | //! | 
 | //! 1. Linearly scale input to be in [0; 1) | 
 | //! 2. Using linear regression, compute the best linear function approximating | 
 | //!    the input. | 
 | //! 3. Compute RMSE and  maximal absolute error. | 
 | //! 4. Check that errors are within tolerances and that the constant term is not | 
 | //!    too negative. | 
 | //! | 
 | //! Ideally, we should use a proper "model selection" to directly compare | 
 | //! quadratic and linear models, but that sounds rather complicated: | 
 | //! | 
 | //! > https://stats.stackexchange.com/questions/21844/selecting-best-model-based-on-linear-quadratic-and-cubic-fit-of-data | 
 | //! | 
 | //! We might get false positives on a VM, but never false negatives. So, if the | 
 | //! first round fails, we repeat the ordeal three more times and fail only if | 
 | //! every time there's a fault. | 
 | use stdx::format_to; | 
 |  | 
 | #[derive(Default)] | 
 | pub struct AssertLinear { | 
 |     rounds: Vec<Round>, | 
 | } | 
 |  | 
 | #[derive(Default)] | 
 | struct Round { | 
 |     samples: Vec<(f64, f64)>, | 
 |     plot: String, | 
 |     linear: bool, | 
 | } | 
 |  | 
 | impl AssertLinear { | 
 |     pub fn next_round(&mut self) -> bool { | 
 |         if let Some(round) = self.rounds.last_mut() { | 
 |             round.finish(); | 
 |         } | 
 |         if self.rounds.iter().any(|it| it.linear) || self.rounds.len() == 4 { | 
 |             return false; | 
 |         } | 
 |         self.rounds.push(Round::default()); | 
 |         true | 
 |     } | 
 |  | 
 |     pub fn sample(&mut self, x: f64, y: f64) { | 
 |         self.rounds.last_mut().unwrap().samples.push((x, y)); | 
 |     } | 
 | } | 
 |  | 
 | impl Drop for AssertLinear { | 
 |     fn drop(&mut self) { | 
 |         assert!(!self.rounds.is_empty()); | 
 |         if self.rounds.iter().all(|it| !it.linear) { | 
 |             for round in &self.rounds { | 
 |                 eprintln!("\n{}", round.plot); | 
 |             } | 
 |             panic!("Doesn't look linear!"); | 
 |         } | 
 |     } | 
 | } | 
 |  | 
 | impl Round { | 
 |     fn finish(&mut self) { | 
 |         let (mut xs, mut ys): (Vec<_>, Vec<_>) = self.samples.iter().copied().unzip(); | 
 |         normalize(&mut xs); | 
 |         normalize(&mut ys); | 
 |         let xy = xs.iter().copied().zip(ys.iter().copied()); | 
 |  | 
 |         // Linear regression: finding a and b to fit y = a + b*x. | 
 |  | 
 |         let mean_x = mean(&xs); | 
 |         let mean_y = mean(&ys); | 
 |  | 
 |         let b = { | 
 |             let mut num = 0.0; | 
 |             let mut denom = 0.0; | 
 |             for (x, y) in xy.clone() { | 
 |                 num += (x - mean_x) * (y - mean_y); | 
 |                 denom += (x - mean_x).powi(2); | 
 |             } | 
 |             num / denom | 
 |         }; | 
 |  | 
 |         let a = mean_y - b * mean_x; | 
 |  | 
 |         self.plot = format!("y_pred = {a:.3} + {b:.3} * x\n\nx     y     y_pred\n"); | 
 |  | 
 |         let mut se = 0.0; | 
 |         let mut max_error = 0.0f64; | 
 |         for (x, y) in xy { | 
 |             let y_pred = a + b * x; | 
 |             se += (y - y_pred).powi(2); | 
 |             max_error = max_error.max((y_pred - y).abs()); | 
 |  | 
 |             format_to!(self.plot, "{:.3} {:.3} {:.3}\n", x, y, y_pred); | 
 |         } | 
 |  | 
 |         let rmse = (se / xs.len() as f64).sqrt(); | 
 |         format_to!(self.plot, "\nrmse = {:.3} max error = {:.3}", rmse, max_error); | 
 |  | 
 |         self.linear = rmse < 0.05 && max_error < 0.1 && a > -0.1; | 
 |  | 
 |         fn normalize(xs: &mut [f64]) { | 
 |             let max = xs.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap(); | 
 |             xs.iter_mut().for_each(|it| *it /= max); | 
 |         } | 
 |  | 
 |         fn mean(xs: &[f64]) -> f64 { | 
 |             xs.iter().copied().sum::<f64>() / (xs.len() as f64) | 
 |         } | 
 |     } | 
 | } |