use super::rng::MonteCarloRng;
#[derive(Debug, Clone, Default)]
pub enum VarianceReduction {
None,
#[default]
Antithetic,
Stratified {
strata: usize,
},
LatinHypercube {
samples: usize,
},
}
impl VarianceReduction {
#[must_use]
pub fn generate_uniforms(&self, n: usize, rng: &mut MonteCarloRng) -> Vec<f64> {
match self {
Self::None => (0..n).map(|_| rng.uniform()).collect(),
Self::Antithetic => {
let mut result = Vec::with_capacity(n);
let pairs = n / 2;
for _ in 0..pairs {
let u = rng.uniform();
result.push(u);
result.push(1.0 - u);
}
if n % 2 == 1 {
result.push(rng.uniform());
}
result
}
Self::Stratified { strata } => {
let k = *strata;
if k == 0 {
return Vec::new();
}
let samples_per_stratum = n / k;
let extra = n % k;
let mut result = Vec::with_capacity(n);
for i in 0..k {
let n_samples = samples_per_stratum + usize::from(i < extra);
let low = i as f64 / k as f64;
let high = (i + 1) as f64 / k as f64;
for _ in 0..n_samples {
let u = rng.uniform();
result.push(low + u * (high - low));
}
}
result
}
Self::LatinHypercube { samples } => {
let n_samples = *samples.min(&n);
if n_samples == 0 {
return Vec::new();
}
let mut indices: Vec<usize> = (0..n_samples).collect();
for i in (1..n_samples).rev() {
let j = (rng.uniform() * (i + 1) as f64) as usize;
indices.swap(i, j);
}
let mut result = Vec::with_capacity(n_samples);
for (i, _) in indices.iter().enumerate().take(n_samples) {
let u = rng.uniform();
result.push((i as f64 + u) / n_samples as f64);
}
for _ in n_samples..n {
result.push(rng.uniform());
}
result
}
}
}
#[must_use]
pub fn generate_normals(&self, n: usize, rng: &mut MonteCarloRng) -> Vec<f64> {
match self {
Self::None => (0..n).map(|_| rng.standard_normal()).collect(),
Self::Antithetic => {
let mut result = Vec::with_capacity(n);
let pairs = n / 2;
for _ in 0..pairs {
let z = rng.standard_normal();
result.push(z);
result.push(-z);
}
if n % 2 == 1 {
result.push(rng.standard_normal());
}
result
}
Self::Stratified { .. } | Self::LatinHypercube { .. } => {
let uniforms = self.generate_uniforms(n, rng);
uniforms.iter().map(|&u| inverse_normal_cdf(u)).collect()
}
}
}
#[must_use]
pub fn estimate_variance_ratio(&self) -> f64 {
match self {
Self::None => 1.0,
Self::Antithetic => 0.5, Self::Stratified { strata } => {
1.0 / (*strata as f64).max(1.0)
}
Self::LatinHypercube { samples } => {
1.0 / (*samples as f64).sqrt().max(1.0)
}
}
}
}
#[must_use]
#[allow(clippy::excessive_precision)]
pub fn inverse_normal_cdf(p: f64) -> f64 {
let p = p.clamp(1e-15, 1.0 - 1e-15);
const A: [f64; 6] = [
-3.969_683_028_665_376e1,
2.209_460_984_245_205e2,
-2.759_285_104_469_687e2,
1.383_577_518_672_690e2,
-3.066_479_806_614_716e1,
2.506_628_277_459_239,
];
const B: [f64; 5] = [
-5.447_609_879_822_406e1,
1.615_858_368_580_409e2,
-1.556_989_798_598_866e2,
6.680_131_188_771_972e1,
-1.328_068_155_288_572e1,
];
const C: [f64; 6] = [
-7.784_894_002_430_293e-3,
-3.223_964_580_411_365e-1,
-2.400_758_277_161_838,
-2.549_732_539_343_734,
4.374_664_141_464_968,
2.938_163_982_698_783,
];
const D: [f64; 4] = [
7.784_695_709_041_462e-3,
3.224_671_290_700_398e-1,
2.445_134_137_142_996,
3.754_408_661_907_416,
];
const P_LOW: f64 = 0.02425;
const P_HIGH: f64 = 1.0 - P_LOW;
if p < P_LOW {
let q = (-2.0 * p.ln()).sqrt();
(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
} else if p <= P_HIGH {
let q = p - 0.5;
let r = q * q;
(((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
/ (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
} else {
let q = (-2.0 * (1.0 - p).ln()).sqrt();
-(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
}
}
#[must_use]
pub fn empirical_variance_reduction(standard_samples: &[f64], reduced_samples: &[f64]) -> f64 {
if standard_samples.is_empty() || reduced_samples.is_empty() {
return 1.0;
}
let var_standard = variance(standard_samples);
let var_reduced = variance(reduced_samples);
if var_standard > 0.0 {
var_reduced / var_standard
} else {
1.0
}
}
fn variance(values: &[f64]) -> f64 {
if values.len() < 2 {
return 0.0;
}
let n = values.len() as f64;
let mean = values.iter().sum::<f64>() / n;
values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n
}
#[path = "variance_tests.rs"]
mod variance_tests;