criterion/plot/gnuplot_backend/
distributions.rs

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