1 | use std::iter; |
---|---|

2 | use std::process::Child; |

3 | |

4 | use crate::stats::univariate::Sample; |

5 | use crate::stats::Distribution; |

6 | use criterion_plot::prelude::*; |

7 | |

8 | use super::*; |

9 | use crate::estimate::Estimate; |

10 | use crate::estimate::Statistic; |

11 | use crate::kde; |

12 | use crate::measurement::ValueFormatter; |

13 | use crate::report::{BenchmarkId, ComparisonData, MeasurementData, ReportContext}; |

14 | |

15 | fn 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 | |

126 | pub(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 | |

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

289 | pub(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 |