criterion/plot/plotters_backend/
distributions.rs

1use super::*;
2use crate::estimate::Estimate;
3use crate::estimate::Statistic;
4use crate::report::ReportContext;
5use crate::stats::Distribution;
6
7fn abs_distribution(
8    id: &BenchmarkId,
9    context: &ReportContext,
10    formatter: &dyn ValueFormatter,
11    statistic: Statistic,
12    distribution: &Distribution<f64>,
13    estimate: &Estimate,
14    size: Option<(u32, u32)>,
15) {
16    let ci = &estimate.confidence_interval;
17    let typical = ci.upper_bound;
18    let mut ci_values = [ci.lower_bound, ci.upper_bound, estimate.point_estimate];
19    let unit = formatter.scale_values(typical, &mut ci_values);
20    let (lb, ub, point) = (ci_values[0], ci_values[1], ci_values[2]);
21
22    let start = lb - (ub - lb) / 9.;
23    let end = ub + (ub - lb) / 9.;
24    let mut scaled_xs: Vec<f64> = distribution.iter().cloned().collect();
25    let _ = formatter.scale_values(typical, &mut scaled_xs);
26    let scaled_xs_sample = Sample::new(&scaled_xs);
27    let (kde_xs, ys) = kde::sweep(scaled_xs_sample, KDE_POINTS, Some((start, end)));
28
29    // interpolate between two points of the KDE sweep to find the Y position at the point estimate.
30    let n_point = kde_xs
31        .iter()
32        .position(|&x| x >= point)
33        .unwrap_or(kde_xs.len() - 1)
34        .max(1); // Must be at least the second element or this will panic
35    let slope = (ys[n_point] - ys[n_point - 1]) / (kde_xs[n_point] - kde_xs[n_point - 1]);
36    let y_point = ys[n_point - 1] + (slope * (point - kde_xs[n_point - 1]));
37
38    let start = kde_xs
39        .iter()
40        .enumerate()
41        .find(|&(_, &x)| x >= lb)
42        .unwrap()
43        .0;
44    let end = kde_xs
45        .iter()
46        .enumerate()
47        .rev()
48        .find(|&(_, &x)| x <= ub)
49        .unwrap()
50        .0;
51    let len = end - start;
52
53    let kde_xs_sample = Sample::new(&kde_xs);
54
55    let path = context.report_path(id, &format!("{}.svg", statistic));
56    let root_area = SVGBackend::new(&path, size.unwrap_or(SIZE)).into_drawing_area();
57
58    let x_range = plotters::data::fitting_range(kde_xs_sample.iter());
59    let mut y_range = plotters::data::fitting_range(ys.iter());
60
61    y_range.end *= 1.1;
62
63    let mut chart = ChartBuilder::on(&root_area)
64        .margin((5).percent())
65        .caption(
66            format!("{}:{}", id.as_title(), statistic),
67            (DEFAULT_FONT, 20),
68        )
69        .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
70        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
71        .build_cartesian_2d(x_range, y_range)
72        .unwrap();
73
74    chart
75        .configure_mesh()
76        .disable_mesh()
77        .x_desc(format!("Average time ({})", unit))
78        .y_desc("Density (a.u.)")
79        .x_label_formatter(&|&v| pretty_print_float(v, true))
80        .y_label_formatter(&|&v| pretty_print_float(v, true))
81        .draw()
82        .unwrap();
83
84    chart
85        .draw_series(LineSeries::new(
86            kde_xs.iter().zip(ys.iter()).map(|(&x, &y)| (x, y)),
87            DARK_BLUE,
88        ))
89        .unwrap()
90        .label("Bootstrap distribution")
91        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
92
93    chart
94        .draw_series(AreaSeries::new(
95            kde_xs
96                .iter()
97                .zip(ys.iter())
98                .skip(start)
99                .take(len)
100                .map(|(&x, &y)| (x, y)),
101            0.0,
102            DARK_BLUE.mix(0.25).filled().stroke_width(3),
103        ))
104        .unwrap()
105        .label("Confidence interval")
106        .legend(|(x, y)| {
107            Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled())
108        });
109
110    chart
111        .draw_series(std::iter::once(PathElement::new(
112            vec![(point, 0.0), (point, y_point)],
113            DARK_BLUE.filled().stroke_width(3),
114        )))
115        .unwrap()
116        .label("Point estimate")
117        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
118
119    chart
120        .configure_series_labels()
121        .position(SeriesLabelPosition::UpperRight)
122        .draw()
123        .unwrap();
124}
125
126pub(crate) fn abs_distributions(
127    id: &BenchmarkId,
128    context: &ReportContext,
129    formatter: &dyn ValueFormatter,
130    measurements: &MeasurementData<'_>,
131    size: Option<(u32, u32)>,
132) {
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        .for_each(|(statistic, distribution, estimate)| {
144            abs_distribution(
145                id,
146                context,
147                formatter,
148                statistic,
149                distribution,
150                estimate,
151                size,
152            );
153        });
154}
155
156fn rel_distribution(
157    id: &BenchmarkId,
158    context: &ReportContext,
159    statistic: Statistic,
160    distribution: &Distribution<f64>,
161    estimate: &Estimate,
162    noise_threshold: f64,
163    size: Option<(u32, u32)>,
164) {
165    let ci = &estimate.confidence_interval;
166    let (lb, ub) = (ci.lower_bound, ci.upper_bound);
167
168    let start = lb - (ub - lb) / 9.;
169    let end = ub + (ub - lb) / 9.;
170    let (xs, ys) = kde::sweep(distribution, KDE_POINTS, Some((start, end)));
171    let xs_ = Sample::new(&xs);
172
173    // interpolate between two points of the KDE sweep to find the Y position at the point estimate.
174    let point = estimate.point_estimate;
175    let n_point = xs
176        .iter()
177        .position(|&x| x >= point)
178        .unwrap_or(ys.len() - 1)
179        .max(1);
180    let slope = (ys[n_point] - ys[n_point - 1]) / (xs[n_point] - xs[n_point - 1]);
181    let y_point = ys[n_point - 1] + (slope * (point - xs[n_point - 1]));
182
183    let start = xs.iter().enumerate().find(|&(_, &x)| x >= lb).unwrap().0;
184    let end = xs
185        .iter()
186        .enumerate()
187        .rev()
188        .find(|&(_, &x)| x <= ub)
189        .unwrap()
190        .0;
191    let len = end - start;
192
193    let x_min = xs_.min();
194    let x_max = xs_.max();
195
196    let (fc_start, fc_end) = if noise_threshold < x_min || -noise_threshold > x_max {
197        let middle = (x_min + x_max) / 2.;
198
199        (middle, middle)
200    } else {
201        (
202            if -noise_threshold < x_min {
203                x_min
204            } else {
205                -noise_threshold
206            },
207            if noise_threshold > x_max {
208                x_max
209            } else {
210                noise_threshold
211            },
212        )
213    };
214    let y_range = plotters::data::fitting_range(ys.iter());
215    let path = context.report_path(id, &format!("change/{}.svg", statistic));
216    let root_area = SVGBackend::new(&path, size.unwrap_or(SIZE)).into_drawing_area();
217
218    let mut chart = ChartBuilder::on(&root_area)
219        .margin((5).percent())
220        .caption(
221            format!("{}:{}", id.as_title(), statistic),
222            (DEFAULT_FONT, 20),
223        )
224        .set_label_area_size(LabelAreaPosition::Left, (5).percent_width().min(60))
225        .set_label_area_size(LabelAreaPosition::Bottom, (5).percent_height().min(40))
226        .build_cartesian_2d(x_min..x_max, y_range.clone())
227        .unwrap();
228
229    chart
230        .configure_mesh()
231        .disable_mesh()
232        .x_desc("Relative change (%)")
233        .y_desc("Density (a.u.)")
234        .x_label_formatter(&|&v| pretty_print_float(v, true))
235        .y_label_formatter(&|&v| pretty_print_float(v, true))
236        .draw()
237        .unwrap();
238
239    chart
240        .draw_series(LineSeries::new(
241            xs.iter().zip(ys.iter()).map(|(x, y)| (*x, *y)),
242            DARK_BLUE,
243        ))
244        .unwrap()
245        .label("Bootstrap distribution")
246        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
247
248    chart
249        .draw_series(AreaSeries::new(
250            xs.iter()
251                .zip(ys.iter())
252                .skip(start)
253                .take(len)
254                .map(|(x, y)| (*x, *y)),
255            0.0,
256            DARK_BLUE.mix(0.25).filled().stroke_width(3),
257        ))
258        .unwrap()
259        .label("Confidence interval")
260        .legend(|(x, y)| {
261            Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_BLUE.mix(0.25).filled())
262        });
263
264    chart
265        .draw_series(std::iter::once(PathElement::new(
266            vec![(point, 0.0), (point, y_point)],
267            DARK_BLUE.filled().stroke_width(3),
268        )))
269        .unwrap()
270        .label("Point estimate")
271        .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], DARK_BLUE));
272
273    chart
274        .draw_series(std::iter::once(Rectangle::new(
275            [(fc_start, y_range.start), (fc_end, y_range.end)],
276            DARK_RED.mix(0.1).filled(),
277        )))
278        .unwrap()
279        .label("Noise threshold")
280        .legend(|(x, y)| {
281            Rectangle::new([(x, y - 5), (x + 20, y + 5)], DARK_RED.mix(0.25).filled())
282        });
283    chart
284        .configure_series_labels()
285        .position(SeriesLabelPosition::UpperRight)
286        .draw()
287        .unwrap();
288}
289
290pub(crate) fn rel_distributions(
291    id: &BenchmarkId,
292    context: &ReportContext,
293    _measurements: &MeasurementData<'_>,
294    comparison: &ComparisonData,
295    size: Option<(u32, u32)>,
296) {
297    crate::plot::CHANGE_STATS.iter().for_each(|&statistic| {
298        rel_distribution(
299            id,
300            context,
301            statistic,
302            comparison.relative_distributions.get(statistic),
303            comparison.relative_estimates.get(statistic),
304            comparison.noise_threshold,
305            size,
306        );
307    });
308}