use super::*;
const N_SIMS: usize = 100_000;
const PIN_UNIFORM_MEAN_BITS: u64 = 4_602_673_648_210_023_252;
const PIN_UNIFORM_SE_BITS: u64 = 4_561_567_819_190_864_006;
const PIN_UNIFORM_PVALUE_BITS: u64 = 4_583_024_316_151_914_253;
#[test]
fn exposes_summaries_via_accessors() -> Result<()> {
let est = monte_carlo_estimate(4, &mut SplitMix64::new(1), |_r| 2.0)?;
assert!(
(est.mean() - 2.0).abs() < 1e-12,
"mean() was {}",
est.mean()
);
assert!(
est.std_error().abs() < 1e-12,
"std_error() was {}",
est.std_error()
);
assert_eq!(
est.n_simulations(),
4,
"n_simulations() must report the count"
);
Ok(())
}
#[test]
fn too_few_simulations_is_insufficient_data() {
let mut rng = SplitMix64::new(1);
let result = monte_carlo_estimate(1, &mut rng, SplitMix64::next_f64);
assert!(
matches!(result, Err(Error::InsufficientData)),
"expected InsufficientData, got {result:?}"
);
}
#[test]
fn same_seed_reproduces_estimate() -> Result<()> {
let a = monte_carlo_estimate(
2_000,
&mut SplitMix64::new(2024),
SplitMix64::standard_normal,
)?;
let b = monte_carlo_estimate(
2_000,
&mut SplitMix64::new(2024),
SplitMix64::standard_normal,
)?;
assert_eq!(
a.mean().to_bits(),
b.mean().to_bits(),
"mean stream diverged"
);
assert_eq!(
a.std_error().to_bits(),
b.std_error().to_bits(),
"std_error stream diverged"
);
assert_eq!(
a.n_simulations(),
b.n_simulations(),
"n_simulations diverged"
);
Ok(())
}
#[test]
fn standard_normal_mean_matches_theory() -> Result<()> {
let est = monte_carlo_estimate(N_SIMS, &mut SplitMix64::new(7), SplitMix64::standard_normal)?;
let n = count_to_f64(N_SIMS);
let theoretical_se = 1.0 / n.sqrt();
assert!(
est.mean().abs() < 4.0 * theoretical_se,
"mean {} not within 4 theoretical SE ({}) of 0",
est.mean(),
4.0 * theoretical_se
);
let se_relative_error = (est.std_error() / theoretical_se - 1.0).abs();
let se_relative_tol = 4.0 / (2.0 * (n - 1.0)).sqrt();
assert!(
se_relative_error < se_relative_tol,
"std_error {} is {se_relative_error} relative from the theoretical \
{theoretical_se}, past the {se_relative_tol} bound",
est.std_error()
);
Ok(())
}
#[test]
fn seeded_uniform_estimate_is_pinned_bit_for_bit() -> Result<()> {
let est = monte_carlo_estimate(N_SIMS, &mut SplitMix64::new(7), SplitMix64::next_f64)?;
assert_eq!(
est.mean().to_bits(),
PIN_UNIFORM_MEAN_BITS,
"seeded uniform mean drifted"
);
assert_eq!(
est.std_error().to_bits(),
PIN_UNIFORM_SE_BITS,
"seeded uniform std_error drifted"
);
assert!(
(est.mean() - 0.5).abs() < 4.0 * est.std_error(),
"uniform mean {} not within 4 SE ({}) of 0.5",
est.mean(),
4.0 * est.std_error()
);
Ok(())
}
#[test]
fn p_value_is_bounded_and_hits_the_floor() -> Result<()> {
let n = 10_000;
let p = monte_carlo_p_value(
1e9,
n,
&mut SplitMix64::new(11),
SplitMix64::standard_normal,
Alternative::Greater,
)?;
assert!(p > 0.0 && p <= 1.0, "p escaped (0, 1]: {p}");
let expected = 1.0 / (count_to_f64(n) + 1.0);
assert!(
(p - expected).abs() < 1e-12,
"floor p was {p}, expected {expected}"
);
Ok(())
}
#[test]
fn upper_tail_p_value_matches_normal_survival() -> Result<()> {
const THEORETICAL: f64 = 0.024_997_895_148_220_435;
let p = monte_carlo_p_value(
1.96,
N_SIMS,
&mut SplitMix64::new(7),
SplitMix64::standard_normal,
Alternative::Greater,
)?;
let se = (THEORETICAL * (1.0 - THEORETICAL) / count_to_f64(N_SIMS)).sqrt();
assert!(
(p - THEORETICAL).abs() < 4.0 * se,
"MC p {p} is further than 4 SE ({}) from theoretical {THEORETICAL}",
4.0 * se
);
Ok(())
}
#[test]
fn seeded_uniform_p_value_is_pinned_bit_for_bit() -> Result<()> {
const THEORETICAL: f64 = 0.025;
let p = monte_carlo_p_value(
0.975,
N_SIMS,
&mut SplitMix64::new(7),
SplitMix64::next_f64,
Alternative::Greater,
)?;
assert_eq!(
p.to_bits(),
PIN_UNIFORM_PVALUE_BITS,
"seeded uniform p-value drifted"
);
let se = (THEORETICAL * (1.0 - THEORETICAL) / count_to_f64(N_SIMS)).sqrt();
assert!(
(p - THEORETICAL).abs() < 4.0 * se,
"uniform upper-tail p {p} is further than 4 SE ({}) from {THEORETICAL}",
4.0 * se
);
Ok(())
}
#[test]
fn inherent_estimate_delegates() -> Result<()> {
let scheme = MonteCarloResampling::default();
let via_method =
scheme.estimate(1_000, &mut SplitMix64::new(3), SplitMix64::standard_normal)?;
let via_free =
monte_carlo_estimate(1_000, &mut SplitMix64::new(3), SplitMix64::standard_normal)?;
assert_eq!(
via_method.mean().to_bits(),
via_free.mean().to_bits(),
"delegation changed the mean"
);
assert_eq!(
via_method.std_error().to_bits(),
via_free.std_error().to_bits(),
"delegation changed the std_error"
);
Ok(())
}
#[test]
fn run_matches_estimate_with_equivalent_seed() -> Result<()> {
let scheme = MonteCarloResampling {
number_of_iterations: 1_000,
random_seed: 7,
..Default::default()
};
let via_run = scheme.run(SplitMix64::standard_normal)?;
let via_free =
monte_carlo_estimate(1_000, &mut SplitMix64::new(7), SplitMix64::standard_normal)?;
assert_eq!(
via_run.mean().to_bits(),
via_free.mean().to_bits(),
"run() diverged from monte_carlo_estimate mean"
);
assert_eq!(
via_run.std_error().to_bits(),
via_free.std_error().to_bits(),
"run() diverged from monte_carlo_estimate std_error"
);
assert_eq!(
via_run.n_simulations(),
1_000,
"run() must report the configured iteration count"
);
Ok(())
}
#[test]
fn run_rejects_non_positive_iterations() {
let scheme = MonteCarloResampling {
number_of_iterations: 0,
random_seed: 1,
..Default::default()
};
let result = scheme.run(SplitMix64::next_f64);
assert!(
matches!(result, Err(Error::InvalidInput(_))),
"non-positive number_of_iterations must be InvalidInput, got {result:?}"
);
}