criterion/plot/gnuplot_backend/
distributions.rs

1use std::iter;
2use std::process::Child;
3
4use crate::stats::univariate::Sample;
5use crate::stats::Distribution;
6use criterion_plot::prelude::*;
7
8use super::*;
9use crate::estimate::Estimate;
10use crate::estimate::Statistic;
11use crate::kde;
12use crate::measurement::ValueFormatter;
13use crate::report::{BenchmarkId, ComparisonData, MeasurementData, ReportContext};
14
15fn 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
126pub(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
157fn 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
289pub(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}