criterion/plot/plotters_backend/
distributions.rs

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