1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
//! 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 = {:.3} + {:.3} * x\n\nx y y_pred\n", a, b);
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 Vec<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)
}
}
}
|