1use std::cmp::Ordering;
18use std::fmt;
19
20use rustev_contract::decimal::Decimal;
21
22use crate::generator;
23use crate::repeat_config::{BandRule, KSd, MAX_RESAMPLES, MIN_RESAMPLES, parse_seed};
24
25pub const MIN_LEVEL_UNITS: i128 = 500_000_000;
27pub const MAX_LEVEL_UNITS: i128 = 999_000_000;
28
29pub const MAX_BOOTSTRAP_WORK: u128 = 1_000_000_000;
31pub const MAX_DENOMINATOR: u64 = u32::MAX as u64;
36pub const MAX_RUNS: usize = 64;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum Form {
42 CountRatio,
44 Mean,
46 Sample { quantile: Decimal },
48}
49
50#[derive(Debug, Clone, Copy, PartialEq)]
52pub enum Contribution {
53 Count {
54 num: u64,
55 den: u64,
56 },
57 Mean {
58 num: f64,
59 den: u64,
60 },
61 Sample(f64),
62 Unknown,
66}
67
68#[derive(Debug, Clone, PartialEq)]
71pub struct MetricCases {
72 pub name: String,
73 pub form: Form,
74 pub cases: Vec<Vec<Option<Contribution>>>,
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub enum Figure {
80 Known(f64),
81 Unknown { reason: String },
82}
83
84impl Figure {
85 fn unknown(reason: impl Into<String>) -> Figure {
86 Figure::Unknown {
87 reason: reason.into(),
88 }
89 }
90
91 fn finite(v: f64) -> Figure {
92 if v.is_finite() {
93 Figure::Known(v)
94 } else {
95 Figure::unknown("non-finite value")
96 }
97 }
98
99 pub fn value(&self) -> Option<f64> {
100 match self {
101 Figure::Known(v) => Some(*v),
102 Figure::Unknown { .. } => None,
103 }
104 }
105}
106
107#[derive(Debug, Clone, PartialEq)]
110pub struct RunStats {
111 pub runs: Vec<Figure>,
113 pub mean: Figure,
114 pub sd: Figure,
115 pub min: Figure,
116 pub max: Figure,
117 pub pooled: Figure,
119}
120
121#[derive(Debug, Clone, PartialEq)]
123pub enum Interval {
124 Known {
125 lower: f64,
126 upper: f64,
127 },
128 Unknown {
129 reason: String,
130 undefined: u64,
133 },
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum StatsError {
139 Shape(String),
141 BootstrapTooLarge { work: u128 },
143}
144
145impl fmt::Display for StatsError {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 match self {
148 StatsError::Shape(m) => write!(f, "{m}"),
149 StatsError::BootstrapTooLarge { work } => {
150 write!(f, "bootstrap-too-large: {work} draws exceed 10^9")
151 }
152 }
153 }
154}
155
156impl std::error::Error for StatsError {}
157
158pub fn check_shape(metric: &MetricCases, runs: usize) -> Result<(), StatsError> {
161 if runs == 0 || runs > MAX_RUNS {
162 return Err(StatsError::Shape("a repeat set has 1 to 64 runs".into()));
163 }
164 for (c, per_run) in metric.cases.iter().enumerate() {
165 if per_run.len() != runs {
166 return Err(StatsError::Shape(format!(
167 "metric `{}`: case {c} has {} runs, not {runs}",
168 metric.name,
169 per_run.len()
170 )));
171 }
172 for x in per_run.iter().flatten() {
173 let ok = matches!(
174 (&metric.form, x),
175 (Form::CountRatio, Contribution::Count { .. })
176 | (Form::Mean, Contribution::Mean { .. })
177 | (Form::Sample { .. }, Contribution::Sample(_))
178 | (_, Contribution::Unknown)
179 );
180 let ok = ok
181 && match x {
182 Contribution::Count { num, den } => {
185 num <= den && (1..=MAX_DENOMINATOR).contains(den)
186 }
187 Contribution::Mean { num, den } => {
188 num.is_finite() && (1..=MAX_DENOMINATOR).contains(den)
189 }
190 Contribution::Sample(v) => v.is_finite(),
191 Contribution::Unknown => true,
192 };
193 if !ok {
194 return Err(StatsError::Shape(format!(
195 "metric `{}`: case {c} has a contribution of another form, or one not finite",
196 metric.name
197 )));
198 }
199 }
200 }
201 Ok(())
202}
203
204#[derive(Debug, Clone, PartialEq)]
207enum Cluster {
208 Count {
209 num: u128,
210 den: u128,
211 },
212 Mean {
213 num: f64,
214 den: u128,
215 },
216 Samples(Vec<f64>),
217 Unknown,
219}
220
221fn cluster<'a>(form: &Form, xs: impl Iterator<Item = &'a Option<Contribution>> + Clone) -> Cluster {
222 if xs
223 .clone()
224 .flatten()
225 .any(|x| matches!(x, Contribution::Unknown))
226 {
227 return Cluster::Unknown;
228 }
229 match form {
230 Form::CountRatio => {
231 let (mut num, mut den) = (0u128, 0u128);
232 for x in xs.flatten() {
233 if let Contribution::Count { num: n, den: d } = x {
234 num += u128::from(*n);
235 den += u128::from(*d);
236 }
237 }
238 Cluster::Count { num, den }
239 }
240 Form::Mean => {
241 let (mut num, mut den) = (0.0f64, 0u128);
242 for x in xs.flatten() {
243 if let Contribution::Mean { num: n, den: d } = x {
244 num += *n;
245 den += u128::from(*d);
246 }
247 }
248 Cluster::Mean { num, den }
249 }
250 Form::Sample { .. } => Cluster::Samples(
251 xs.flatten()
252 .filter_map(|x| match x {
253 Contribution::Sample(v) => Some(*v),
254 _ => None,
255 })
256 .collect(),
257 ),
258 }
259}
260
261fn ascending(v: &mut [f64]) {
264 v.sort_by(f64::total_cmp);
265}
266
267fn nearest_rank(samples: &mut [f64], q: Decimal) -> Figure {
270 if samples.is_empty() {
271 return Figure::unknown("no sample");
272 }
273 ascending(samples);
274 let n = samples.len() as i128;
275 let one = 1_000_000_000i128;
276 let rank = ((q.units() * n + one - 1) / one).clamp(1, n) as usize;
277 Figure::Known(samples[rank - 1])
278}
279
280fn statistic<'a>(form: &Form, clusters: impl Iterator<Item = &'a Cluster> + Clone) -> Figure {
283 if clusters.clone().any(|c| matches!(c, Cluster::Unknown)) {
284 return Figure::unknown("a case's contribution is unknown");
285 }
286 match form {
287 Form::CountRatio => {
288 let (mut num, mut den) = (0u128, 0u128);
289 for c in clusters {
290 if let Cluster::Count { num: n, den: d } = c {
291 num += *n;
292 den += *d;
293 }
294 }
295 if den == 0 {
296 Figure::unknown("zero denominator")
297 } else {
298 Figure::finite(num as f64 / den as f64)
299 }
300 }
301 Form::Mean => {
302 let (mut num, mut den) = (0.0f64, 0u128);
303 for c in clusters {
304 if let Cluster::Mean { num: n, den: d } = c {
305 num += *n;
306 den += *d;
307 }
308 }
309 if den == 0 {
310 Figure::unknown("zero denominator")
311 } else {
312 Figure::finite(num / den as f64)
313 }
314 }
315 Form::Sample { quantile } => {
316 let mut all = Vec::new();
317 for c in clusters {
318 if let Cluster::Samples(s) = c {
319 all.extend_from_slice(s);
320 }
321 }
322 nearest_rank(&mut all, *quantile)
323 }
324 }
325}
326
327pub fn run_stats(
331 metric: &MetricCases,
332 runs: usize,
333 deterministic: bool,
334) -> Result<RunStats, StatsError> {
335 check_shape(metric, runs)?;
336 let values: Vec<Figure> = (0..runs)
337 .map(|r| {
338 let clusters: Vec<Cluster> = metric
339 .cases
340 .iter()
341 .map(|per_run| cluster(&metric.form, std::iter::once(&per_run[r])))
342 .collect();
343 statistic(&metric.form, clusters.iter())
344 })
345 .collect();
346 let pooled_clusters: Vec<Cluster> = metric
347 .cases
348 .iter()
349 .map(|per_run| cluster(&metric.form, per_run.iter()))
350 .collect();
351 let pooled = statistic(&metric.form, pooled_clusters.iter());
352
353 let (mean, sd, min, max) = across_runs(&values, deterministic);
354 Ok(RunStats {
355 runs: values,
356 mean,
357 sd,
358 min,
359 max,
360 pooled,
361 })
362}
363
364pub fn across_runs(values: &[Figure], deterministic: bool) -> (Figure, Figure, Figure, Figure) {
368 if values.is_empty() {
369 let u = Figure::unknown("no run");
370 return (u.clone(), u.clone(), u.clone(), u);
371 }
372 let unknown_runs: Vec<String> = values
373 .iter()
374 .enumerate()
375 .filter(|(_, v)| v.value().is_none())
376 .map(|(r, _)| r.to_string())
377 .collect();
378 let (mean, sd, min, max) = if unknown_runs.is_empty() {
379 let v: Vec<f64> = values.iter().filter_map(Figure::value).collect();
380 let mut sum = 0.0f64;
381 for x in &v {
382 sum += *x;
383 }
384 let mean = sum / values.len() as f64;
385 let sd = if values.len() == 1 {
386 if deterministic {
387 Figure::Known(0.0)
388 } else {
389 Figure::unknown("one run")
390 }
391 } else {
392 let mut ss = 0.0f64;
393 for x in &v {
394 let d = *x - mean;
395 ss += d * d;
396 }
397 Figure::finite((ss / (values.len() - 1) as f64).sqrt())
398 };
399 let min = v
400 .iter()
401 .copied()
402 .min_by(f64::total_cmp)
403 .expect("at least one run");
404 let max = v
405 .iter()
406 .copied()
407 .max_by(f64::total_cmp)
408 .expect("at least one run");
409 (
410 Figure::finite(mean),
411 sd,
412 Figure::Known(min),
413 Figure::Known(max),
414 )
415 } else {
416 let u = Figure::unknown(format!("unknown at runs {}", unknown_runs.join(", ")));
417 (u.clone(), u.clone(), u.clone(), u)
418 };
419 (mean, sd, min, max)
420}
421
422#[derive(Debug, Clone, PartialEq, Eq)]
424pub struct Bootstrap {
425 pub seed: String,
427 pub level_units: i128,
429 pub resamples: u64,
431}
432
433pub fn percentile_ranks(level_units: i128, resamples: u64) -> (u64, u64) {
436 let one = 1_000_000_000i128;
437 let b = i128::from(resamples);
438 let ceil_div = |n: i128, d: i128| (n + d - 1) / d;
439 let lower = ceil_div((one - level_units) * b, 2 * one).max(1);
440 let upper = ceil_div((one + level_units) * b, 2 * one).max(1);
441 (lower as u64, upper as u64)
442}
443
444pub fn intervals(
448 b: &Bootstrap,
449 cohort: &str,
450 runs: usize,
451 metrics: &[MetricCases],
452) -> Result<Vec<Interval>, StatsError> {
453 let cases = metrics.first().map_or(0, |m| m.cases.len());
454 for m in metrics {
455 check_shape(m, runs)?;
456 if m.cases.len() != cases {
457 return Err(StatsError::Shape(format!(
458 "metric `{}` covers {} cases, the cohort {cases}",
459 m.name,
460 m.cases.len()
461 )));
462 }
463 }
464 if !(MIN_RESAMPLES..=MAX_RESAMPLES).contains(&b.resamples)
465 || !(MIN_LEVEL_UNITS..=MAX_LEVEL_UNITS).contains(&b.level_units)
466 || parse_seed(&b.seed).is_none()
467 {
468 return Err(StatsError::Shape(
469 "the bootstrap's seed, level and resamples are outside 3.3.1".into(),
470 ));
471 }
472 let work = u128::from(b.resamples) * cases as u128 * runs as u128;
473 if work > MAX_BOOTSTRAP_WORK {
474 return Err(StatsError::BootstrapTooLarge { work });
475 }
476 if cases == 0 {
477 return Ok(metrics
478 .iter()
479 .map(|_| Interval::Unknown {
480 reason: "empty cohort".into(),
481 undefined: b.resamples,
482 })
483 .collect());
484 }
485 let clusters: Vec<Vec<Cluster>> = metrics
486 .iter()
487 .map(|m| {
488 m.cases
489 .iter()
490 .map(|per_run| cluster(&m.form, per_run.iter()))
491 .collect()
492 })
493 .collect();
494 let mut values: Vec<Vec<f64>> = vec![Vec::with_capacity(b.resamples as usize); metrics.len()];
495 let mut undefined = vec![0u64; metrics.len()];
496 let mut stream = generator::stream(&b.seed, &format!("cohort/{cohort}"));
497 let mut draws = vec![0usize; cases];
498 for _ in 0..b.resamples {
499 for d in draws.iter_mut() {
500 *d = stream.index(cases as u64) as usize;
501 }
502 for (m, metric) in metrics.iter().enumerate() {
503 let s = statistic(&metric.form, draws.iter().map(|&i| &clusters[m][i]));
504 match s {
505 Figure::Known(v) => values[m].push(v),
506 Figure::Unknown { .. } => undefined[m] += 1,
507 }
508 }
509 }
510 let (lo, hi) = percentile_ranks(b.level_units, b.resamples);
511 Ok(values
512 .into_iter()
513 .zip(undefined)
514 .map(|(mut v, u)| {
515 if u > 0 {
516 return Interval::Unknown {
517 reason: format!("{u} undefined resamples"),
518 undefined: u,
519 };
520 }
521 ascending(&mut v);
522 Interval::Known {
523 lower: v[lo as usize - 1],
524 upper: v[hi as usize - 1],
525 }
526 })
527 .collect())
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
532pub enum Basis {
533 KSd,
534 IntervalWidth,
535 DeterministicReplay,
536 Floor,
538}
539
540impl Basis {
541 pub fn name(self) -> &'static str {
542 match self {
543 Basis::KSd => "k_sd",
544 Basis::IntervalWidth => "interval_width",
545 Basis::DeterministicReplay => "deterministic_replay",
546 Basis::Floor => "floor",
547 }
548 }
549}
550
551#[derive(Debug, Clone, PartialEq)]
553pub enum Band {
554 Known { value: f64, basis: Basis },
555 Unknown { reason: String },
556}
557
558impl Band {
559 pub fn value(&self) -> Option<f64> {
560 match self {
561 Band::Known { value, .. } => Some(*value),
562 Band::Unknown { .. } => None,
563 }
564 }
565}
566
567pub fn band(
572 rule: &BandRule,
573 floor: Decimal,
574 deterministic: bool,
575 sd: &Figure,
576 interval: &Interval,
577) -> Band {
578 let floor = floor.to_f64_nearest();
579 let (derived, basis) = match rule {
580 BandRule::KSd(KSd { k }) => {
581 if deterministic {
582 return Band::Known {
583 value: floor,
584 basis: Basis::DeterministicReplay,
585 };
586 }
587 match sd {
588 Figure::Known(sd) => (k.to_f64_nearest() * sd, Basis::KSd),
589 Figure::Unknown { reason } => {
590 return Band::Unknown {
591 reason: format!("sd unknown: {reason}"),
592 };
593 }
594 }
595 }
596 BandRule::IntervalWidth => match interval {
597 Interval::Known { lower, upper } => (upper - lower, Basis::IntervalWidth),
598 Interval::Unknown { reason, .. } => {
599 return Band::Unknown {
600 reason: format!("interval unknown: {reason}"),
601 };
602 }
603 },
604 };
605 if !derived.is_finite() {
606 return Band::Unknown {
607 reason: "non-finite band".into(),
608 };
609 }
610 match floor.partial_cmp(&derived) {
611 Some(Ordering::Greater) => Band::Known {
612 value: floor,
613 basis: Basis::Floor,
614 },
615 _ => Band::Known {
616 value: derived,
617 basis,
618 },
619 }
620}