use crate::bayesian::rand_u01;
fn randn(state: &mut u64) -> f64 {
let u1 = rand_u01(state).max(1e-12);
let u2 = rand_u01(state).max(1e-12);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
fn phi(x: f64) -> f64 {
(-0.5 * x * x).exp() / (2.0 * std::f64::consts::PI).sqrt()
}
#[allow(clippy::too_many_arguments)]
pub fn subset_simulation<G>(
dim: usize,
n_per_level: usize,
n_levels: usize,
threshold: f64,
g: G,
seed: u64,
proposal_std: f64,
) -> SubsetResult
where
G: Fn(&[f64]) -> f64,
{
let mut rng = seed;
let p0 = 0.1_f64;
let mut samples: Vec<Vec<f64>> = (0..n_per_level)
.map(|_| (0..dim).map(|_| randn(&mut rng)).collect())
.collect();
let mut g_values: Vec<f64> = samples.iter().map(|s| g(s)).collect();
let mut level = 0usize;
while level < n_levels {
let mut sorted = g_values.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let idx = ((n_per_level as f64) * (1.0 - p0)) as usize;
let level_threshold = sorted[idx.min(n_per_level - 1)];
if level_threshold >= threshold {
break;
}
let mut seeds: Vec<(Vec<f64>, f64)> = Vec::with_capacity(n_per_level);
for (s, gv) in samples.iter().zip(g_values.iter()) {
if *gv >= level_threshold {
seeds.push((s.clone(), *gv));
}
}
if seeds.is_empty() {
let mut pairs: Vec<(Vec<f64>, f64)> = samples.into_iter().zip(g_values).collect();
pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
seeds = pairs.into_iter().take(n_per_level.max(1)).collect();
}
let mut next: Vec<Vec<f64>> = Vec::with_capacity(n_per_level);
let mut next_g: Vec<f64> = Vec::with_capacity(n_per_level);
let n_seeds = seeds.len();
for i in 0..n_per_level {
let seed_sample = seeds[i % n_seeds].0.clone();
let mut candidate = seed_sample.clone();
let mut candidate_density = density(candidate.iter().copied());
let mut accepted = false;
for _ in 0..500 {
let mut trial = candidate.clone();
for v in &mut trial {
*v = proposal_std.mul_add(randn(&mut rng), *v);
}
let g_trial = g(&trial);
if g_trial < level_threshold {
continue;
}
let trial_density = density(trial.iter().copied());
let ratio = (trial_density / candidate_density).min(1.0);
if rand_u01(&mut rng) < ratio {
candidate = trial;
candidate_density = trial_density;
accepted = true;
break;
}
}
let gv = if accepted {
g(&candidate)
} else {
seeds[i % n_seeds].1
};
next_g.push(gv);
next.push(candidate);
}
samples = next;
g_values = next_g;
level += 1;
}
let fail = g_values.iter().filter(|&&v| v >= threshold).count();
let conditional = fail as f64 / g_values.len().max(1) as f64;
let probability = p0.powf(level as f64) * conditional;
SubsetResult {
probability,
levels_used: level,
n_samples_total: n_per_level * (level + 1),
method: "subset".into(),
}
}
pub fn importance_sampling<G>(
dim: usize,
n_samples: usize,
threshold: f64,
g: G,
seed: u64,
) -> ImportanceResult
where
G: Fn(&[f64]) -> f64,
{
let mut rng = seed;
let shift = 0.5 * threshold.sqrt().min(8.0) / dim.max(1) as f64;
let mut count = 0usize;
let mut weight_sum = 0.0_f64;
for _ in 0..n_samples {
let x: Vec<f64> = (0..dim).map(|_| randn(&mut rng) + shift).collect();
let w = likelihood_ratio(&x, shift);
if g(&x) >= threshold {
count += 1;
weight_sum += w;
}
}
let mean = weight_sum / n_samples.max(1) as f64;
let cv = if mean > 1e-300 {
(count as f64).sqrt() / n_samples.max(1) as f64 / mean.max(1e-300)
} else {
0.0
};
ImportanceResult {
probability: mean,
coefficient_of_variation: cv,
hits: count,
n_samples,
method: "importance".into(),
}
}
fn likelihood_ratio(x: &[f64], shift: f64) -> f64 {
let mut lr = 1.0_f64;
for &xi in x {
lr *= (-xi).mul_add(shift, 0.5 * shift * shift).exp();
}
lr
}
fn density(x: impl Iterator<Item = f64>) -> f64 {
x.map(phi).product()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SubsetResult {
pub probability: f64,
pub levels_used: usize,
pub n_samples_total: usize,
pub method: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ImportanceResult {
pub probability: f64,
pub coefficient_of_variation: f64,
pub hits: usize,
pub n_samples: usize,
pub method: String,
}
#[cfg(test)]
mod tests {
#![allow(clippy::suboptimal_flops)] use super::*;
#[test]
fn subset_matches_chi_square_tail() {
let result = subset_simulation(
2,
2000,
3,
9.0,
|x: &[f64]| x[0] * x[0] + x[1] * x[1],
42,
1.0,
);
assert!(
(result.probability - 0.0111).abs() < 0.01,
"subset: {} (expected ~0.0111)",
result.probability
);
assert!(result.levels_used >= 1);
}
#[test]
fn importance_matches_chi_square_tail() {
let result = importance_sampling(2, 50_000, 9.0, |x: &[f64]| x[0] * x[0] + x[1] * x[1], 7);
assert!(
(result.probability - 0.0111).abs() < 0.01,
"importance: {} (expected ~0.0111)",
result.probability
);
}
#[test]
fn subset_common_event_is_close() {
let result = subset_simulation(
2,
2000,
2,
4.0,
|x: &[f64]| x[0] * x[0] + x[1] * x[1],
123,
1.0,
);
assert!(
(result.probability - 0.1353).abs() < 0.05,
"subset: {} (expected ~0.135)",
result.probability
);
}
#[test]
fn importance_rejects_no_hits_gracefully() {
let result = importance_sampling(2, 1000, 1e9, |x: &[f64]| x[0] * x[0] + x[1] * x[1], 1);
assert!(result.probability < 1e-3);
assert_eq!(result.hits, 0);
}
#[test]
fn subset_always_exceeded_is_one() {
let result = subset_simulation(
2,
500,
1,
-1.0,
|x: &[f64]| x[0] * x[0] + x[1] * x[1],
9,
1.0,
);
assert!(result.probability > 0.99);
}
}