1 | use super::*; |
2 | use crate::estimate::Estimate; |
3 | use crate::estimate::Statistic; |
4 | use crate::measurement::ValueFormatter; |
5 | use crate::report::{BenchmarkId, MeasurementData, ReportContext}; |
6 | use crate::stats::Distribution; |
7 | |
8 | fn 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 | |
127 | pub(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 | |
157 | fn 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 | |
291 | pub(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 | } |
310 | |