1 | use std::iter; |
2 | use std::process::Child; |
3 | |
4 | use crate::stats::univariate::Sample; |
5 | use crate::stats::Distribution; |
6 | use criterion_plot::prelude::*; |
7 | |
8 | use super::*; |
9 | use crate::estimate::Estimate; |
10 | use crate::estimate::Statistic; |
11 | use crate::kde; |
12 | use crate::measurement::ValueFormatter; |
13 | use crate::report::{BenchmarkId, ComparisonData, MeasurementData, ReportContext}; |
14 | |
15 | fn abs_distribution( |
16 | id: &BenchmarkId, |
17 | context: &ReportContext, |
18 | formatter: &dyn ValueFormatter, |
19 | statistic: Statistic, |
20 | distribution: &Distribution<f64>, |
21 | estimate: &Estimate, |
22 | size: Option<Size>, |
23 | ) -> Child { |
24 | let ci = &estimate.confidence_interval; |
25 | let typical = ci.upper_bound; |
26 | let mut ci_values = [ci.lower_bound, ci.upper_bound, estimate.point_estimate]; |
27 | let unit = formatter.scale_values(typical, &mut ci_values); |
28 | let (lb, ub, point) = (ci_values[0], ci_values[1], ci_values[2]); |
29 | |
30 | let start = lb - (ub - lb) / 9.; |
31 | let end = ub + (ub - lb) / 9.; |
32 | let mut scaled_xs: Vec<f64> = distribution.iter().cloned().collect(); |
33 | let _ = formatter.scale_values(typical, &mut scaled_xs); |
34 | let scaled_xs_sample = Sample::new(&scaled_xs); |
35 | let (kde_xs, ys) = kde::sweep(scaled_xs_sample, KDE_POINTS, Some((start, end))); |
36 | |
37 | // interpolate between two points of the KDE sweep to find the Y position at the point estimate. |
38 | let n_point = kde_xs |
39 | .iter() |
40 | .position(|&x| x >= point) |
41 | .unwrap_or(kde_xs.len() - 1) |
42 | .max(1); // Must be at least the second element or this will panic |
43 | let slope = (ys[n_point] - ys[n_point - 1]) / (kde_xs[n_point] - kde_xs[n_point - 1]); |
44 | let y_point = ys[n_point - 1] + (slope * (point - kde_xs[n_point - 1])); |
45 | |
46 | let zero = iter::repeat(0); |
47 | |
48 | let start = kde_xs |
49 | .iter() |
50 | .enumerate() |
51 | .find(|&(_, &x)| x >= lb) |
52 | .unwrap() |
53 | .0; |
54 | let end = kde_xs |
55 | .iter() |
56 | .enumerate() |
57 | .rev() |
58 | .find(|&(_, &x)| x <= ub) |
59 | .unwrap() |
60 | .0; |
61 | let len = end - start; |
62 | |
63 | let kde_xs_sample = Sample::new(&kde_xs); |
64 | |
65 | let mut figure = Figure::new(); |
66 | figure |
67 | .set(Font(DEFAULT_FONT)) |
68 | .set(size.unwrap_or(SIZE)) |
69 | .set(Title(format!( |
70 | "{}: {}" , |
71 | gnuplot_escape(id.as_title()), |
72 | statistic |
73 | ))) |
74 | .configure(Axis::BottomX, |a| { |
75 | a.set(Label(format!("Average time ({})" , unit))) |
76 | .set(Range::Limits(kde_xs_sample.min(), kde_xs_sample.max())) |
77 | }) |
78 | .configure(Axis::LeftY, |a| a.set(Label("Density (a.u.)" ))) |
79 | .configure(Key, |k| { |
80 | k.set(Justification::Left) |
81 | .set(Order::SampleText) |
82 | .set(Position::Outside(Vertical::Top, Horizontal::Right)) |
83 | }) |
84 | .plot( |
85 | Lines { |
86 | x: &*kde_xs, |
87 | y: &*ys, |
88 | }, |
89 | |c| { |
90 | c.set(DARK_BLUE) |
91 | .set(LINEWIDTH) |
92 | .set(Label("Bootstrap distribution" )) |
93 | .set(LineType::Solid) |
94 | }, |
95 | ) |
96 | .plot( |
97 | FilledCurve { |
98 | x: kde_xs.iter().skip(start).take(len), |
99 | y1: ys.iter().skip(start), |
100 | y2: zero, |
101 | }, |
102 | |c| { |
103 | c.set(DARK_BLUE) |
104 | .set(Label("Confidence interval" )) |
105 | .set(Opacity(0.25)) |
106 | }, |
107 | ) |
108 | .plot( |
109 | Lines { |
110 | x: &[point, point], |
111 | y: &[0., y_point], |
112 | }, |
113 | |c| { |
114 | c.set(DARK_BLUE) |
115 | .set(LINEWIDTH) |
116 | .set(Label("Point estimate" )) |
117 | .set(LineType::Dash) |
118 | }, |
119 | ); |
120 | |
121 | let path = context.report_path(id, &format!("{}.svg" , statistic)); |
122 | debug_script(&path, &figure); |
123 | figure.set(Output(path)).draw().unwrap() |
124 | } |
125 | |
126 | pub(crate) fn abs_distributions( |
127 | id: &BenchmarkId, |
128 | context: &ReportContext, |
129 | formatter: &dyn ValueFormatter, |
130 | measurements: &MeasurementData<'_>, |
131 | size: Option<Size>, |
132 | ) -> Vec<Child> { |
133 | crate::plot::REPORT_STATS |
134 | .iter() |
135 | .filter_map(|stat| { |
136 | measurements.distributions.get(*stat).and_then(|dist| { |
137 | measurements |
138 | .absolute_estimates |
139 | .get(*stat) |
140 | .map(|est| (*stat, dist, est)) |
141 | }) |
142 | }) |
143 | .map(|(statistic, distribution, estimate)| { |
144 | abs_distribution( |
145 | id, |
146 | context, |
147 | formatter, |
148 | statistic, |
149 | distribution, |
150 | estimate, |
151 | size, |
152 | ) |
153 | }) |
154 | .collect::<Vec<_>>() |
155 | } |
156 | |
157 | fn rel_distribution( |
158 | id: &BenchmarkId, |
159 | context: &ReportContext, |
160 | statistic: Statistic, |
161 | distribution: &Distribution<f64>, |
162 | estimate: &Estimate, |
163 | noise_threshold: f64, |
164 | size: Option<Size>, |
165 | ) -> Child { |
166 | let ci = &estimate.confidence_interval; |
167 | let (lb, ub) = (ci.lower_bound, ci.upper_bound); |
168 | |
169 | let start = lb - (ub - lb) / 9.; |
170 | let end = ub + (ub - lb) / 9.; |
171 | let (xs, ys) = kde::sweep(distribution, KDE_POINTS, Some((start, end))); |
172 | let xs_ = Sample::new(&xs); |
173 | |
174 | // interpolate between two points of the KDE sweep to find the Y position at the point estimate. |
175 | let point = estimate.point_estimate; |
176 | let n_point = xs |
177 | .iter() |
178 | .position(|&x| x >= point) |
179 | .unwrap_or(ys.len() - 1) |
180 | .max(1); |
181 | let slope = (ys[n_point] - ys[n_point - 1]) / (xs[n_point] - xs[n_point - 1]); |
182 | let y_point = ys[n_point - 1] + (slope * (point - xs[n_point - 1])); |
183 | |
184 | let one = iter::repeat(1); |
185 | let zero = iter::repeat(0); |
186 | |
187 | let start = xs.iter().enumerate().find(|&(_, &x)| x >= lb).unwrap().0; |
188 | let end = xs |
189 | .iter() |
190 | .enumerate() |
191 | .rev() |
192 | .find(|&(_, &x)| x <= ub) |
193 | .unwrap() |
194 | .0; |
195 | let len = end - start; |
196 | |
197 | let x_min = xs_.min(); |
198 | let x_max = xs_.max(); |
199 | |
200 | let (fc_start, fc_end) = if noise_threshold < x_min || -noise_threshold > x_max { |
201 | let middle = (x_min + x_max) / 2.; |
202 | |
203 | (middle, middle) |
204 | } else { |
205 | ( |
206 | if -noise_threshold < x_min { |
207 | x_min |
208 | } else { |
209 | -noise_threshold |
210 | }, |
211 | if noise_threshold > x_max { |
212 | x_max |
213 | } else { |
214 | noise_threshold |
215 | }, |
216 | ) |
217 | }; |
218 | |
219 | let mut figure = Figure::new(); |
220 | |
221 | figure |
222 | .set(Font(DEFAULT_FONT)) |
223 | .set(size.unwrap_or(SIZE)) |
224 | .configure(Axis::LeftY, |a| a.set(Label("Density (a.u.)" ))) |
225 | .configure(Key, |k| { |
226 | k.set(Justification::Left) |
227 | .set(Order::SampleText) |
228 | .set(Position::Outside(Vertical::Top, Horizontal::Right)) |
229 | }) |
230 | .set(Title(format!( |
231 | "{}: {}" , |
232 | gnuplot_escape(id.as_title()), |
233 | statistic |
234 | ))) |
235 | .configure(Axis::BottomX, |a| { |
236 | a.set(Label("Relative change (%)" )) |
237 | .set(Range::Limits(x_min * 100., x_max * 100.)) |
238 | .set(ScaleFactor(100.)) |
239 | }) |
240 | .plot(Lines { x: &*xs, y: &*ys }, |c| { |
241 | c.set(DARK_BLUE) |
242 | .set(LINEWIDTH) |
243 | .set(Label("Bootstrap distribution" )) |
244 | .set(LineType::Solid) |
245 | }) |
246 | .plot( |
247 | FilledCurve { |
248 | x: xs.iter().skip(start).take(len), |
249 | y1: ys.iter().skip(start), |
250 | y2: zero.clone(), |
251 | }, |
252 | |c| { |
253 | c.set(DARK_BLUE) |
254 | .set(Label("Confidence interval" )) |
255 | .set(Opacity(0.25)) |
256 | }, |
257 | ) |
258 | .plot( |
259 | Lines { |
260 | x: &[point, point], |
261 | y: &[0., y_point], |
262 | }, |
263 | |c| { |
264 | c.set(DARK_BLUE) |
265 | .set(LINEWIDTH) |
266 | .set(Label("Point estimate" )) |
267 | .set(LineType::Dash) |
268 | }, |
269 | ) |
270 | .plot( |
271 | FilledCurve { |
272 | x: &[fc_start, fc_end], |
273 | y1: one, |
274 | y2: zero, |
275 | }, |
276 | |c| { |
277 | c.set(Axes::BottomXRightY) |
278 | .set(DARK_RED) |
279 | .set(Label("Noise threshold" )) |
280 | .set(Opacity(0.1)) |
281 | }, |
282 | ); |
283 | |
284 | let path = context.report_path(id, &format!("change/{}.svg" , statistic)); |
285 | debug_script(&path, &figure); |
286 | figure.set(Output(path)).draw().unwrap() |
287 | } |
288 | |
289 | pub(crate) fn rel_distributions( |
290 | id: &BenchmarkId, |
291 | context: &ReportContext, |
292 | _measurements: &MeasurementData<'_>, |
293 | comparison: &ComparisonData, |
294 | size: Option<Size>, |
295 | ) -> Vec<Child> { |
296 | crate::plot::CHANGE_STATS |
297 | .iter() |
298 | .map(|&statistic| { |
299 | rel_distribution( |
300 | id, |
301 | context, |
302 | statistic, |
303 | comparison.relative_distributions.get(statistic), |
304 | comparison.relative_estimates.get(statistic), |
305 | comparison.noise_threshold, |
306 | size, |
307 | ) |
308 | }) |
309 | .collect::<Vec<_>>() |
310 | } |
311 | |