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}
311