use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::random::prelude::*;
use scirs2_core::random::{Distribution, Normal, Uniform};
#[derive(Debug, Clone)]
pub struct ImportanceSamplingResult {
pub value: f64,
pub variance: f64,
pub effective_sample_size: f64,
pub n_evals: usize,
pub normalised_weights: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct StratifiedResult {
pub value: f64,
pub std_error: f64,
pub stratum_values: Vec<f64>,
pub stratum_errors: Vec<f64>,
pub n_evals: usize,
}
pub fn effective_sample_size(weights: &[f64]) -> f64 {
if weights.is_empty() {
return 0.0;
}
let sum: f64 = weights.iter().sum();
let sum_sq: f64 = weights.iter().map(|w| w * w).sum();
if sum_sq == 0.0 {
return 0.0;
}
(sum * sum) / sum_sq
}
pub fn importance_sampling_integral<F, P, Q, Sampler>(
f: F,
target_p: P,
proposal_q: Q,
sampler: Sampler,
n_samples: usize,
seed: Option<u64>,
) -> IntegrateResult<ImportanceSamplingResult>
where
F: Fn(ArrayView1<f64>) -> f64,
P: Fn(ArrayView1<f64>) -> f64,
Q: Fn(ArrayView1<f64>) -> f64,
Sampler: Fn(&mut StdRng, usize) -> Array1<f64>,
{
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be positive".to_string(),
));
}
let mut rng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let mut os = scirs2_core::random::rng();
StdRng::from_rng(&mut os)
}
};
let probe = sampler(&mut rng, 1);
let dim = probe.len().max(1);
let mut weights: Vec<f64> = Vec::with_capacity(n_samples);
let mut weighted_f: Vec<f64> = Vec::with_capacity(n_samples);
let mut n_valid = 0usize;
{
let x = probe.view();
let pval = target_p(x);
let qval = proposal_q(x);
if qval > 1e-300 && pval.is_finite() && qval.is_finite() {
let w = pval / qval;
let fval = f(x);
if fval.is_finite() && w.is_finite() {
weights.push(w);
weighted_f.push(fval * w);
n_valid += 1;
}
}
}
for _ in 1..n_samples {
let x = sampler(&mut rng, dim);
let pval = target_p(x.view());
let qval = proposal_q(x.view());
if qval <= 1e-300 || !pval.is_finite() || !qval.is_finite() {
continue;
}
let w = pval / qval;
let fval = f(x.view());
if !fval.is_finite() || !w.is_finite() {
continue;
}
weights.push(w);
weighted_f.push(fval * w);
n_valid += 1;
}
if n_valid < 2 {
return Err(IntegrateError::ConvergenceError(
"Too few valid IS samples (fewer than 2); check that proposal covers target support"
.to_string(),
));
}
let n_f = n_valid as f64;
let sum_wf: f64 = weighted_f.iter().sum();
let value = sum_wf / n_f;
let sum_sq: f64 = weighted_f.iter().map(|v| (v - value) * (v - value)).sum();
let variance = sum_sq / (n_f * (n_f - 1.0));
let ess = effective_sample_size(&weights);
let w_sum: f64 = weights.iter().sum();
let normalised_weights: Vec<f64> = weights
.iter()
.map(|&w| if w_sum > 0.0 { w / w_sum } else { 0.0 })
.collect();
Ok(ImportanceSamplingResult {
value,
variance,
effective_sample_size: ess,
n_evals: n_valid,
normalised_weights,
})
}
pub fn self_normalized_is<F, Pi, Q, Sampler>(
f: F,
unnorm_pi: Pi,
proposal_q: Q,
sampler: Sampler,
n_samples: usize,
seed: Option<u64>,
) -> IntegrateResult<ImportanceSamplingResult>
where
F: Fn(ArrayView1<f64>) -> f64,
Pi: Fn(ArrayView1<f64>) -> f64,
Q: Fn(ArrayView1<f64>) -> f64,
Sampler: Fn(&mut StdRng, usize) -> Array1<f64>,
{
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be positive".to_string(),
));
}
let mut rng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let mut os = scirs2_core::random::rng();
StdRng::from_rng(&mut os)
}
};
let probe = sampler(&mut rng, 1);
let dim = probe.len().max(1);
let mut raw_weights: Vec<f64> = Vec::with_capacity(n_samples);
let mut wf_vals: Vec<f64> = Vec::with_capacity(n_samples);
let process = |x: ArrayView1<f64>, rw: &mut Vec<f64>, wf: &mut Vec<f64>| -> bool {
let pi_val = unnorm_pi(x);
let q_val = proposal_q(x);
if q_val <= 1e-300 || !pi_val.is_finite() || !q_val.is_finite() {
return false;
}
let w = pi_val / q_val;
let fval = f(x);
if !fval.is_finite() || !w.is_finite() {
return false;
}
rw.push(w);
wf.push(fval * w);
true
};
process(probe.view(), &mut raw_weights, &mut wf_vals);
for _ in 1..n_samples {
let x = sampler(&mut rng, dim);
process(x.view(), &mut raw_weights, &mut wf_vals);
}
let n_valid = raw_weights.len();
if n_valid < 2 {
return Err(IntegrateError::ConvergenceError(
"Too few valid SNIS samples".to_string(),
));
}
let w_sum: f64 = raw_weights.iter().sum();
if w_sum == 0.0 {
return Err(IntegrateError::ConvergenceError(
"All importance weights are zero".to_string(),
));
}
let wf_sum: f64 = wf_vals.iter().sum();
let value = wf_sum / w_sum;
let n_f = n_valid as f64;
let var_fw: f64 = wf_vals
.iter()
.map(|&x| (x / w_sum - value / n_f) * (x / w_sum - value / n_f))
.sum::<f64>()
* n_f
/ (n_f - 1.0);
let variance = var_fw / n_f;
let ess = effective_sample_size(&raw_weights);
let normalised_weights: Vec<f64> = raw_weights.iter().map(|&w| w / w_sum).collect();
Ok(ImportanceSamplingResult {
value,
variance,
effective_sample_size: ess,
n_evals: n_valid,
normalised_weights,
})
}
pub fn sequential_is<F, InitSampler, Transition, IncWeight>(
f: F,
initial_sampler: InitSampler,
transition: Transition,
incremental_weight: IncWeight,
n_particles: usize,
n_steps: usize,
resample_threshold: Option<f64>,
seed: Option<u64>,
) -> IntegrateResult<ImportanceSamplingResult>
where
F: Fn(ArrayView1<f64>) -> f64,
InitSampler: Fn(&mut StdRng) -> Array1<f64>,
Transition: Fn(ArrayView1<f64>, &mut StdRng) -> Array1<f64>,
IncWeight: Fn(ArrayView1<f64>, usize) -> f64,
{
if n_particles == 0 {
return Err(IntegrateError::ValueError(
"n_particles must be positive".to_string(),
));
}
let threshold = resample_threshold.unwrap_or(0.5) * (n_particles as f64);
let mut rng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let mut os = scirs2_core::random::rng();
StdRng::from_rng(&mut os)
}
};
let mut particles: Vec<Array1<f64>> = (0..n_particles)
.map(|_| initial_sampler(&mut rng))
.collect();
let mut log_weights: Vec<f64> = vec![-(n_particles as f64).ln(); n_particles];
for step in 0..n_steps {
let mut new_particles: Vec<Array1<f64>> = Vec::with_capacity(n_particles);
let mut new_log_w: Vec<f64> = Vec::with_capacity(n_particles);
for (i, particle) in particles.iter().enumerate() {
let x_new = transition(particle.view(), &mut rng);
let lw_inc = incremental_weight(x_new.view(), step);
if lw_inc.is_nan() {
new_particles.push(x_new);
new_log_w.push(f64::NEG_INFINITY);
} else {
new_log_w.push(log_weights[i] + lw_inc);
new_particles.push(x_new);
}
}
let lw_max = new_log_w
.iter()
.cloned()
.filter(|v| v.is_finite())
.fold(f64::NEG_INFINITY, f64::max);
if !lw_max.is_finite() {
return Err(IntegrateError::ConvergenceError(
"All particle weights collapsed to zero at SIS step".to_string(),
));
}
let raw_w: Vec<f64> = new_log_w.iter().map(|&lw| (lw - lw_max).exp()).collect();
let w_sum: f64 = raw_w.iter().sum();
let norm_w: Vec<f64> = raw_w.iter().map(|&w| w / w_sum).collect();
let ess = effective_sample_size(&norm_w);
particles = new_particles;
log_weights = norm_w.iter().map(|&w| w.ln()).collect();
if ess < threshold {
let resampled = systematic_resample(&norm_w, n_particles, &mut rng);
particles = resampled
.iter()
.map(|&idx| particles[idx].clone())
.collect();
let log_uniform = -(n_particles as f64).ln();
log_weights = vec![log_uniform; n_particles];
}
}
let lw_max = log_weights
.iter()
.cloned()
.filter(|v| v.is_finite())
.fold(f64::NEG_INFINITY, f64::max);
let raw_w: Vec<f64> = log_weights.iter().map(|&lw| (lw - lw_max).exp()).collect();
let w_sum: f64 = raw_w.iter().sum();
let norm_w: Vec<f64> = raw_w.iter().map(|&w| w / w_sum).collect();
let value: f64 = particles
.iter()
.zip(norm_w.iter())
.map(|(x, &w)| w * f(x.view()))
.sum();
let variance: f64 = {
let n_f = n_particles as f64;
particles
.iter()
.zip(norm_w.iter())
.map(|(x, &w)| {
let diff = w * f(x.view()) - value / n_f;
diff * diff
})
.sum::<f64>()
/ (n_f - 1.0)
/ n_f
};
let ess = effective_sample_size(&norm_w);
Ok(ImportanceSamplingResult {
value,
variance,
effective_sample_size: ess,
n_evals: n_particles * (n_steps + 1),
normalised_weights: norm_w,
})
}
pub fn stratified_sampling<F>(
f: F,
ranges: &[(f64, f64)],
n_strata: usize,
samples_per_stratum: usize,
seed: Option<u64>,
) -> IntegrateResult<StratifiedResult>
where
F: Fn(ArrayView1<f64>) -> f64,
{
if ranges.is_empty() {
return Err(IntegrateError::ValueError(
"ranges cannot be empty".to_string(),
));
}
if n_strata == 0 || samples_per_stratum == 0 {
return Err(IntegrateError::ValueError(
"n_strata and samples_per_stratum must be positive".to_string(),
));
}
let n_dims = ranges.len();
let (a0, b0) = ranges[0];
let stratum_width = (b0 - a0) / (n_strata as f64);
let remaining_volume: f64 = ranges[1..].iter().map(|(a, b)| b - a).product();
let mut rng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let mut os = scirs2_core::random::rng();
StdRng::from_rng(&mut os)
}
};
let uniform01 = Uniform::new(0.0_f64, 1.0).expect("valid uniform bounds [0,1)");
let mut stratum_values = Vec::with_capacity(n_strata);
let mut stratum_errors = Vec::with_capacity(n_strata);
let mut total_evals = 0usize;
for k in 0..n_strata {
let lo = a0 + k as f64 * stratum_width;
let hi = lo + stratum_width;
let mut sum = 0.0_f64;
let mut sum_sq = 0.0_f64;
for _ in 0..samples_per_stratum {
let mut point = Array1::<f64>::zeros(n_dims);
point[0] = lo + (hi - lo) * uniform01.sample(&mut rng);
for (j, &(aj, bj)) in ranges[1..].iter().enumerate() {
point[j + 1] = aj + (bj - aj) * uniform01.sample(&mut rng);
}
let val = f(point.view());
sum += val;
sum_sq += val * val;
}
let n_f = samples_per_stratum as f64;
let mean = sum / n_f;
let var_mean = if samples_per_stratum > 1 {
(sum_sq - sum * sum / n_f) / (n_f * (n_f - 1.0))
} else {
0.0
};
let stratum_vol = stratum_width * remaining_volume;
stratum_values.push(mean * stratum_vol);
stratum_errors.push(var_mean.sqrt() * stratum_vol);
total_evals += samples_per_stratum;
}
let value: f64 = stratum_values.iter().sum();
let total_var: f64 = stratum_errors.iter().map(|e| e * e).sum();
let std_error = total_var.sqrt();
Ok(StratifiedResult {
value,
std_error,
stratum_values,
stratum_errors,
n_evals: total_evals,
})
}
fn systematic_resample(weights: &[f64], n: usize, rng: &mut StdRng) -> Vec<usize> {
let uniform01 = Uniform::new(0.0_f64, 1.0).expect("valid bounds");
let u0: f64 = uniform01.sample(rng);
let mut indices = Vec::with_capacity(n);
let step = 1.0 / (n as f64);
let mut cumsum = 0.0_f64;
let mut j = 0usize;
for i in 0..n {
let u = (u0 + i as f64) * step;
while cumsum < u && j < weights.len() - 1 {
cumsum += weights[j];
j += 1;
}
indices.push(j.min(weights.len() - 1));
}
indices
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_ess_uniform() {
let w = vec![1.0_f64; 100];
let ess = effective_sample_size(&w);
assert!((ess - 100.0).abs() < 1e-10);
}
#[test]
fn test_ess_degenerate() {
let mut w = vec![0.0_f64; 99];
w.push(1.0);
let ess = effective_sample_size(&w);
assert!((ess - 1.0).abs() < 1e-10);
}
#[test]
fn test_importance_sampling_integral_gaussian() {
let result = importance_sampling_integral(
|x: ArrayView1<f64>| (-x[0] * x[0]).exp(),
|_x: ArrayView1<f64>| 1.0_f64,
|x: ArrayView1<f64>| {
let z = x[0];
(-0.5 * z * z).exp() / (2.0 * PI).sqrt()
},
|rng: &mut StdRng, _d: usize| {
let n = Normal::new(0.0, 1.0).expect("valid");
Array1::from_elem(1, n.sample(rng))
},
50000,
Some(42),
)
.expect("IS failed");
assert!((result.value - PI.sqrt()).abs() < 0.05);
assert!(result.effective_sample_size > 1.0);
}
#[test]
fn test_self_normalized_is() {
let result = self_normalized_is(
|x: ArrayView1<f64>| x[0] * x[0],
|x: ArrayView1<f64>| {
let z = (x[0] - 1.0) / 0.5;
(-0.5 * z * z).exp()
},
|x: ArrayView1<f64>| {
let z = x[0];
(-0.5 * z * z).exp() / (2.0 * PI).sqrt()
},
|rng: &mut StdRng, _d: usize| {
let n = Normal::new(0.0, 1.0).expect("valid");
Array1::from_elem(1, n.sample(rng))
},
50000,
Some(7),
)
.expect("SNIS failed");
assert!((result.value - 1.25).abs() < 0.05);
}
#[test]
fn test_stratified_sampling_sin() {
let result = stratified_sampling(
|x: ArrayView1<f64>| x[0].sin(),
&[(0.0, PI)],
20,
500,
Some(13),
)
.expect("stratified failed");
assert!((result.value - 2.0).abs() < 0.01);
assert_eq!(result.stratum_values.len(), 20);
}
#[test]
fn test_sequential_is_simple() {
let n_particles = 500;
let result = sequential_is(
|x: ArrayView1<f64>| x[0] * x[0],
|rng: &mut StdRng| {
let n = Normal::new(0.0, 1.0).expect("valid");
Array1::from_elem(1, n.sample(rng))
},
|x: ArrayView1<f64>, _rng: &mut StdRng| {
x.to_owned()
},
|_x: ArrayView1<f64>, _step: usize| 0.0_f64, n_particles,
3,
Some(0.5),
Some(99),
)
.expect("SIS failed");
assert!(
(result.value - 1.0).abs() < 0.3,
"SIS estimate {} too far from 1.0",
result.value
);
}
}