#![forbid(unsafe_code)]
use anyhow::{Context, Result};
use clap::Parser;
use std::f64::consts::PI;
use std::fs;
use std::path::PathBuf;
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed.wrapping_mul(6364136223846793005) ^ 0x9E3779B97F4A7C15)
}
fn next_u64(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0
}
fn next_unit(&mut self) -> f64 {
let raw = self.next_u64() >> 11;
(raw as f64) / ((1u64 << 53) as f64)
}
fn next_range(&mut self, n: usize) -> usize {
((self.next_u64() >> 33) as usize) % n.max(1)
}
fn next_normal(&mut self) -> f64 {
let u1 = self.next_unit().max(f64::MIN_POSITIVE);
let u2 = self.next_unit();
(-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
}
}
#[derive(Clone, Copy)]
enum Source {
BetaLikeF1,
GammaLikeTtd,
LogNormalLikeFar,
}
impl Source {
fn name(self) -> &'static str {
match self {
Source::BetaLikeF1 => "beta_like_f1",
Source::GammaLikeTtd => "gamma_like_ttd",
Source::LogNormalLikeFar => "lognormal_like_far",
}
}
fn draw(self, rng: &mut Lcg) -> f64 {
match self {
Source::BetaLikeF1 => {
let g1 = gamma_int(rng, 8);
let g2 = gamma_int(rng, 2);
g1 / (g1 + g2)
}
Source::GammaLikeTtd => {
let theta = 0.3_f64;
gamma_int(rng, 2) * theta
}
Source::LogNormalLikeFar => {
let mu = 2.0_f64;
let sigma = 1.0_f64;
(mu + sigma * rng.next_normal()).exp()
}
}
}
fn true_mean(self) -> f64 {
match self {
Source::BetaLikeF1 => 8.0 / (8.0 + 2.0),
Source::GammaLikeTtd => 2.0 * 0.3,
Source::LogNormalLikeFar => (2.0_f64 + 0.5).exp(),
}
}
}
fn gamma_int(rng: &mut Lcg, k: usize) -> f64 {
let mut acc = 0.0;
for _ in 0..k {
let u = rng.next_unit().max(f64::MIN_POSITIVE);
acc -= u.ln();
}
acc
}
fn bootstrap_ci(sample: &[f64], b: usize, alpha: f64, rng: &mut Lcg) -> (f64, f64) {
if sample.is_empty() {
return (0.0, 0.0);
}
let mut boots = Vec::with_capacity(b);
let n = sample.len();
for _ in 0..b {
let resample_mean: f64 = (0..n).map(|_| sample[rng.next_range(n)]).sum::<f64>() / n as f64;
boots.push(resample_mean);
}
boots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let lo_idx = ((alpha / 2.0) * b as f64) as usize;
let hi_idx = (((1.0 - alpha / 2.0) * b as f64) as usize).min(b - 1);
(boots[lo_idx], boots[hi_idx])
}
#[derive(Parser)]
#[command(
name = "bootstrap_coverage",
about = "Monte-Carlo coverage of the percentile-bootstrap 95% CI at small n.",
version
)]
struct Cli {
#[arg(long, value_delimiter = ',', default_values_t = vec![5, 10, 20, 50])]
n: Vec<usize>,
#[arg(long, default_value_t = 2000)]
n_mc: usize,
#[arg(long, default_value_t = 1000)]
bootstrap_b: usize,
#[arg(long, default_value_t = 0.05)]
alpha: f64,
#[arg(long, default_value_t = 42)]
seed: u64,
#[arg(long, default_value = "out/bootstrap_coverage")]
out: PathBuf,
}
fn main() -> Result<()> {
let cli = Cli::parse();
fs::create_dir_all(&cli.out).with_context(|| format!("mkdir {}", cli.out.display()))?;
let mut rng = Lcg::new(cli.seed);
let sources = [
Source::BetaLikeF1,
Source::GammaLikeTtd,
Source::LogNormalLikeFar,
];
let mut buf = String::new();
buf.push_str(
"distribution,n,n_mc,B,alpha_nominal,true_mean,empirical_coverage,mean_ci_lo,mean_ci_hi\n",
);
for src in sources {
let true_mean = src.true_mean();
for &n in &cli.n {
let mut covered = 0_usize;
let mut sum_lo = 0.0_f64;
let mut sum_hi = 0.0_f64;
for _ in 0..cli.n_mc {
let sample: Vec<f64> = (0..n).map(|_| src.draw(&mut rng)).collect();
let (lo, hi) = bootstrap_ci(&sample, cli.bootstrap_b, cli.alpha, &mut rng);
if lo <= true_mean && true_mean <= hi {
covered += 1;
}
sum_lo += lo;
sum_hi += hi;
}
let coverage = covered as f64 / cli.n_mc as f64;
let mean_lo = sum_lo / cli.n_mc as f64;
let mean_hi = sum_hi / cli.n_mc as f64;
buf.push_str(&format!(
"{},{},{},{},{:.4},{:.6},{:.4},{:.6},{:.6}\n",
src.name(),
n,
cli.n_mc,
cli.bootstrap_b,
cli.alpha,
true_mean,
coverage,
mean_lo,
mean_hi
));
eprintln!(
" {:>20} | n={:>3} | true={:.3} | coverage={:.3} | CI ≈ [{:.3}, {:.3}]",
src.name(),
n,
true_mean,
coverage,
mean_lo,
mean_hi
);
}
}
let csv_path = cli.out.join("coverage.csv");
fs::write(&csv_path, buf).with_context(|| format!("writing {}", csv_path.display()))?;
eprintln!("wrote {}", csv_path.display());
Ok(())
}