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 
abs_distribution( id: &BenchmarkId, context: &ReportContext, formatter: &dyn ValueFormatter, statistic: Statistic, distribution: &Distribution<f64>, estimate: &Estimate, size: Option<Size>, ) -> Child15 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 
abs_distributions( id: &BenchmarkId, context: &ReportContext, formatter: &dyn ValueFormatter, measurements: &MeasurementData<'_>, size: Option<Size>, ) -> Vec<Child>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 
rel_distribution( id: &BenchmarkId, context: &ReportContext, statistic: Statistic, distribution: &Distribution<f64>, estimate: &Estimate, noise_threshold: f64, size: Option<Size>, ) -> Child157 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 
rel_distributions( id: &BenchmarkId, context: &ReportContext, _measurements: &MeasurementData<'_>, comparison: &ComparisonData, size: Option<Size>, ) -> Vec<Child>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