use super::gaussian::gauss_legendre;
use crate::error::{IntegrateError, IntegrateResult};
use std::f64::consts::PI;
pub fn monte_carlo<F>(
f: F,
bounds: &[(f64, f64)],
n_samples: usize,
seed: u64,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
{
if bounds.is_empty() {
return Err(IntegrateError::ValueError(
"bounds must not be empty".to_string(),
));
}
for (i, &(a, b)) in bounds.iter().enumerate() {
if a >= b {
return Err(IntegrateError::ValueError(format!(
"bounds[{i}]: a={a} must be < b={b}"
)));
}
}
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be at least 1".to_string(),
));
}
let dim = bounds.len();
let widths: Vec<f64> = bounds.iter().map(|&(a, b)| b - a).collect();
let lows: Vec<f64> = bounds.iter().map(|&(a, _)| a).collect();
let volume: f64 = widths.iter().product();
let mut rng = LcgRng::new(seed);
let mut sum = 0.0_f64;
let mut sum2 = 0.0_f64;
let mut point = vec![0.0_f64; dim];
for _ in 0..n_samples {
for d in 0..dim {
point[d] = lows[d] + widths[d] * rng.next_f64();
}
let v = f(&point);
sum += v;
sum2 += v * v;
}
let nf = n_samples as f64;
let mean = sum / nf;
let variance = (sum2 / nf - mean * mean).max(0.0);
let std_error = volume * (variance / nf).sqrt();
let estimate = volume * mean;
Ok((estimate, std_error))
}
pub fn quasi_monte_carlo<F>(
f: F,
bounds: &[(f64, f64)],
n_samples: usize,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
{
if bounds.is_empty() {
return Err(IntegrateError::ValueError(
"bounds must not be empty".to_string(),
));
}
let dim = bounds.len();
if dim > 21 {
return Err(IntegrateError::ValueError(
"quasi_monte_carlo: dim must be <= 21".to_string(),
));
}
for (i, &(a, b)) in bounds.iter().enumerate() {
if a >= b {
return Err(IntegrateError::ValueError(format!(
"bounds[{i}]: a={a} must be < b={b}"
)));
}
}
if n_samples == 0 {
return Err(IntegrateError::ValueError(
"n_samples must be at least 1".to_string(),
));
}
let lows: Vec<f64> = bounds.iter().map(|&(a, _)| a).collect();
let widths: Vec<f64> = bounds.iter().map(|&(a, b)| b - a).collect();
let volume: f64 = widths.iter().product();
let mut sobol1 = SobolSeq::new(dim);
let mut sobol2 = SobolSeq::new_scrambled(dim, 0xDEAD_BEEF);
let mut sum1 = 0.0_f64;
let mut sum2 = 0.0_f64;
let mut point = vec![0.0_f64; dim];
for _ in 0..n_samples {
let p1 = sobol1.next_point();
let p2 = sobol2.next_point();
for d in 0..dim {
point[d] = lows[d] + widths[d] * p1[d];
}
sum1 += f(&point);
for d in 0..dim {
point[d] = lows[d] + widths[d] * p2[d];
}
sum2 += f(&point);
}
let nf = n_samples as f64;
let est1 = volume * sum1 / nf;
let est2 = volume * sum2 / nf;
let estimate = (est1 + est2) / 2.0;
let std_error = (est1 - est2).abs() / 2.0_f64.sqrt();
Ok((estimate, std_error))
}
pub fn product_gauss<F>(
f: F,
bounds: &[(f64, f64)],
n_points_per_dim: usize,
) -> IntegrateResult<f64>
where
F: Fn(&[f64]) -> f64,
{
if bounds.is_empty() {
return Err(IntegrateError::ValueError(
"bounds must not be empty".to_string(),
));
}
for (i, &(a, b)) in bounds.iter().enumerate() {
if a >= b {
return Err(IntegrateError::ValueError(format!(
"bounds[{i}]: a={a} must be < b={b}"
)));
}
}
if n_points_per_dim == 0 {
return Err(IntegrateError::ValueError(
"n_points_per_dim must be at least 1".to_string(),
));
}
let dim = bounds.len();
let (ref_nodes, ref_weights) = gauss_legendre(n_points_per_dim)?;
let transformed: Vec<(Vec<f64>, Vec<f64>)> = bounds
.iter()
.map(|&(a, b)| {
let mid = 0.5 * (a + b);
let half = 0.5 * (b - a);
let nodes: Vec<f64> = ref_nodes.iter().map(|&t| mid + half * t).collect();
let weights: Vec<f64> = ref_weights.iter().map(|&w| w * half).collect();
(nodes, weights)
})
.collect();
let n = n_points_per_dim;
let total = n.pow(dim as u32);
let mut result = 0.0_f64;
let mut point = vec![0.0_f64; dim];
let mut idx = vec![0usize; dim];
for _ in 0..total {
let mut w_prod = 1.0_f64;
for d in 0..dim {
point[d] = transformed[d].0[idx[d]];
w_prod *= transformed[d].1[idx[d]];
}
result += w_prod * f(&point);
let mut carry = true;
for d in (0..dim).rev() {
if carry {
idx[d] += 1;
if idx[d] >= n {
idx[d] = 0;
} else {
carry = false;
}
}
}
}
Ok(result)
}
pub fn genz_malik<F>(
f: F,
bounds: &[(f64, f64)],
abs_tol: f64,
rel_tol: f64,
max_eval: usize,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
{
let dim = bounds.len();
if !(2..=7).contains(&dim) {
return Err(IntegrateError::ValueError(
"genz_malik: dim must be between 2 and 7".to_string(),
));
}
for (i, &(a, b)) in bounds.iter().enumerate() {
if a >= b {
return Err(IntegrateError::ValueError(format!(
"bounds[{i}]: a={a} must be < b={b}"
)));
}
}
let centers: Vec<f64> = bounds.iter().map(|&(a, b)| 0.5 * (a + b)).collect();
let halfs: Vec<f64> = bounds.iter().map(|&(a, b)| 0.5 * (b - a)).collect();
let volume: f64 = halfs.iter().map(|h| 2.0 * h).product();
let mut regions: Vec<GmRegion> = vec![GmRegion::new(centers.clone(), halfs.clone(), volume)];
let mut n_eval = 0usize;
let (est, err) = genz_malik_rule(&f, ®ions[0], dim, &mut n_eval);
regions[0].integral = est;
regions[0].error = err;
let mut total_est = est;
let mut total_err = err;
let max_regions = max_eval / gm_n_points(dim).max(1);
for _ in 0..max_regions {
let tol = abs_tol.max(rel_tol * total_est.abs());
if total_err <= tol {
break;
}
if n_eval >= max_eval {
break;
}
let worst_idx = (0..regions.len())
.max_by(|&a, &b| {
regions[a]
.error
.partial_cmp(®ions[b].error)
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(0);
let worst = regions[worst_idx].clone();
total_est -= worst.integral;
total_err -= worst.error;
regions.swap_remove(worst_idx);
let split_dim = gm_split_dim(&f, &worst, dim, &mut n_eval);
let mut r1 = worst.clone();
let mut r2 = worst.clone();
r1.halfs[split_dim] *= 0.5;
r2.halfs[split_dim] *= 0.5;
r1.centers[split_dim] -= r1.halfs[split_dim];
r2.centers[split_dim] += r2.halfs[split_dim];
r1.volume *= 0.5;
r2.volume *= 0.5;
let (e1, err1) = genz_malik_rule(&f, &r1, dim, &mut n_eval);
let (e2, err2) = genz_malik_rule(&f, &r2, dim, &mut n_eval);
r1.integral = e1;
r1.error = err1;
r2.integral = e2;
r2.error = err2;
total_est += e1 + e2;
total_err += err1 + err2;
regions.push(r1);
regions.push(r2);
}
Ok((total_est, total_err))
}
#[derive(Clone)]
struct GmRegion {
centers: Vec<f64>,
halfs: Vec<f64>,
volume: f64,
#[allow(dead_code)]
integral: f64,
#[allow(dead_code)]
error: f64,
}
impl Default for GmRegion {
fn default() -> Self {
Self {
centers: Vec::new(),
halfs: Vec::new(),
volume: 0.0,
integral: 0.0,
error: 0.0,
}
}
}
impl GmRegion {
fn new(centers: Vec<f64>, halfs: Vec<f64>, volume: f64) -> Self {
Self {
centers,
halfs,
volume,
integral: 0.0,
error: 0.0,
}
}
}
fn gm_n_points(dim: usize) -> usize {
(1 << dim) + 2 * dim * dim + 2 * dim + 1
}
fn genz_malik_rule<F>(f: &F, region: &GmRegion, dim: usize, n_eval: &mut usize) -> (f64, f64)
where
F: Fn(&[f64]) -> f64,
{
let lam2 = (9.0_f64 / 70.0).sqrt();
let lam3 = (9.0_f64 / 10.0).sqrt();
let lam4 = (9.0_f64 / 10.0).sqrt();
let lam5 = (9.0_f64 / 19.0).sqrt();
let d = dim as f64;
let w1 = (12824.0 - 9120.0 * d + 400.0 * d * d) / 19683.0;
let w2 = 980.0 / 6561.0;
let w3 = (1820.0 - 400.0 * d) / 19683.0;
let w4 = 200.0 / 19683.0;
let w5 = 6859.0 / 19683.0 / 2.0_f64.powi(dim as i32);
let wp1 = (729.0 - 950.0 * d + 50.0 * d * d) / 729.0;
let wp2 = 245.0 / 486.0;
let wp3 = (265.0 - 100.0 * d) / 1458.0;
let wp4 = 25.0 / 729.0;
let c = ®ion.centers;
let h = ®ion.halfs;
let mut sum7 = 0.0_f64;
let mut sum5 = 0.0_f64;
let mut point = c.to_vec();
let fc = f(&point);
sum7 += w1 * fc;
sum5 += wp1 * fc;
*n_eval += 1;
for i in 0..dim {
let hi = h[i];
point[i] = c[i] + lam2 * hi;
let fp = f(&point);
point[i] = c[i] - lam2 * hi;
let fm = f(&point);
point[i] = c[i];
sum7 += w2 * (fp + fm);
sum5 += wp2 * (fp + fm);
*n_eval += 2;
}
for i in 0..dim {
let hi = h[i];
point[i] = c[i] + lam3 * hi;
let fp = f(&point);
point[i] = c[i] - lam3 * hi;
let fm = f(&point);
point[i] = c[i];
sum7 += w3 * (fp + fm);
sum5 += wp3 * (fp + fm);
*n_eval += 2;
}
for i in 0..dim {
for j in i + 1..dim {
let hi = h[i];
let hj = h[j];
for &si in &[1.0_f64, -1.0] {
for &sj in &[1.0_f64, -1.0] {
point[i] = c[i] + si * lam4 * hi;
point[j] = c[j] + sj * lam4 * hj;
let fval = f(&point);
sum7 += w4 * fval;
sum5 += wp4 * fval;
*n_eval += 1;
}
}
point[i] = c[i];
point[j] = c[j];
}
}
{
let n_vertices = 1usize << dim;
for mask in 0..n_vertices {
for d in 0..dim {
let sign = if (mask >> d) & 1 == 0 { 1.0 } else { -1.0 };
point[d] = c[d] + sign * lam5 * h[d];
}
sum7 += w5 * f(&point);
*n_eval += 1;
}
point[..dim].copy_from_slice(&c[..dim]);
}
let vol = region.volume;
let i7 = vol * sum7;
let i5 = vol * sum5;
let err = (i7 - i5).abs();
(i7, err)
}
fn gm_split_dim<F>(f: &F, region: &GmRegion, dim: usize, n_eval: &mut usize) -> usize
where
F: Fn(&[f64]) -> f64,
{
let c = ®ion.centers;
let h = ®ion.halfs;
let lam = (9.0_f64 / 10.0).sqrt();
let mut max_diff = -1.0_f64;
let mut best = 0usize;
let mut point = c.to_vec();
let fc = {
let v = f(&point);
*n_eval += 1;
v
};
for i in 0..dim {
let hi = h[i];
point[i] = c[i] + lam * hi;
let fp = f(&point);
point[i] = c[i] - lam * hi;
let fm = f(&point);
point[i] = c[i];
*n_eval += 2;
let diff = (fp - 2.0 * fc + fm).abs();
if diff > max_diff {
max_diff = diff;
best = i;
}
}
best
}
pub fn romberg<F: Fn(f64) -> f64>(
f: F,
a: f64,
b: f64,
max_steps: usize,
tol: f64,
) -> IntegrateResult<f64> {
if a >= b {
return Err(IntegrateError::ValueError(
"romberg: a must be < b".to_string(),
));
}
if max_steps == 0 {
return Err(IntegrateError::ValueError(
"romberg: max_steps must be at least 1".to_string(),
));
}
if tol <= 0.0 {
return Err(IntegrateError::ValueError(
"romberg: tol must be positive".to_string(),
));
}
let cap = max_steps + 1;
let mut r = vec![vec![0.0_f64; cap]; cap];
let h = b - a;
r[0][0] = 0.5 * h * (f(a) + f(b));
for i in 1..cap {
let n = 1usize << i;
let step = h / n as f64;
let mut mid_sum = 0.0_f64;
let n_new = n / 2; for k in 0..n_new {
let x = a + step * (2 * k + 1) as f64;
mid_sum += f(x);
}
r[i][0] = 0.5 * r[i - 1][0] + step * mid_sum;
let mut pow4 = 4.0_f64;
for j in 1..=i {
r[i][j] = (pow4 * r[i][j - 1] - r[i - 1][j - 1]) / (pow4 - 1.0);
pow4 *= 4.0;
}
if i >= 2 {
let diff = (r[i][i] - r[i - 1][i - 1]).abs();
if diff <= tol * r[i][i].abs().max(1.0) {
return Ok(r[i][i]);
}
}
}
Ok(r[max_steps][max_steps])
}
struct LcgRng {
state: u64,
}
impl LcgRng {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
self.state
}
fn next_f64(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
}
struct SobolSeq {
dim: usize,
v: Vec<Vec<u32>>,
x: Vec<u32>,
scramble: Vec<u32>,
count: u32,
}
impl SobolSeq {
fn new(dim: usize) -> Self {
Self::new_scrambled(dim, 0)
}
fn new_scrambled(dim: usize, seed: u32) -> Self {
let v = sobol_direction_numbers(dim);
let scramble: Vec<u32> = (0..dim)
.map(|d| {
let mut h = seed.wrapping_mul(0x9e3779b9).wrapping_add(d as u32);
h ^= h >> 16;
h = h.wrapping_mul(0x45d9f3b);
h ^= h >> 16;
h
})
.collect();
let x = vec![0u32; dim];
Self {
dim,
v,
x,
scramble,
count: 0,
}
}
fn next_point(&mut self) -> Vec<f64> {
let c = self.count.trailing_zeros() as usize;
let max_bit = self.v[0].len();
let c = c.min(max_bit - 1);
for d in 0..self.dim {
self.x[d] ^= self.v[d][c];
}
self.count += 1;
let scale = 1.0 / (1u64 << 32) as f64;
self.x
.iter()
.zip(self.scramble.iter())
.map(|(&xi, &sc)| (xi ^ sc) as f64 * scale)
.collect()
}
}
fn sobol_direction_numbers(dim: usize) -> Vec<Vec<u32>> {
let max_bit = 32usize;
let poly_s: [u32; 21] = [
1, 1, 2, 3, 3, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7,
];
let m_init: [&[u32]; 21] = [
&[1],
&[1, 1],
&[1, 3, 7],
&[1, 1, 5],
&[1, 3, 1, 1],
&[1, 1, 3, 7],
&[1, 3, 3, 9, 9],
&[1, 3, 7, 13, 3],
&[1, 1, 5, 11, 27],
&[1, 3, 5, 1, 15],
&[1, 1, 7, 3, 29],
&[1, 3, 7, 7, 21, 9],
&[1, 1, 1, 9, 23, 37],
&[1, 3, 3, 5, 19, 33],
&[1, 1, 3, 13, 11, 7],
&[1, 1, 7, 13, 25, 5, 49],
&[1, 3, 5, 11, 7, 11, 19],
&[1, 1, 1, 3, 13, 11, 37],
&[1, 3, 1, 15, 21, 3, 21],
&[1, 3, 3, 9, 31, 13, 9],
&[1, 1, 5, 5, 3, 3, 17, 27],
];
let d = dim.min(21);
let mut v_all: Vec<Vec<u32>> = Vec::with_capacity(d);
for dd in 0..d {
let mut v = vec![0u32; max_bit];
if dd == 0 {
for i in 0..max_bit {
v[i] = 1u32 << (31 - i);
}
} else {
let idx = dd - 1;
let s = poly_s[idx] as usize;
let m = m_init[idx];
for i in 0..s.min(m.len()).min(max_bit) {
v[i] = m[i] << (31 - i);
}
for i in s..max_bit {
let mut new_v = v[i - s];
new_v ^= new_v >> s;
for k in 1..s {
let poly_coeff = if k < 32 {
(poly_s[idx] >> (k - 1)) & 1
} else {
0
};
if poly_coeff == 1 {
new_v ^= v[i - k];
}
}
v[i] = new_v;
}
}
v_all.push(v);
}
v_all
}
#[cfg(test)]
mod tests {
use super::*;
const PI: f64 = std::f64::consts::PI;
#[test]
fn test_monte_carlo_2d_sum() {
let (est, err) = monte_carlo(
|xy: &[f64]| xy[0] + xy[1],
&[(0.0, 1.0), (0.0, 1.0)],
500_000,
12345,
)
.expect("monte_carlo should succeed");
assert!((est - 1.0).abs() < 0.01, "est={est} std_error={err}");
}
#[test]
fn test_monte_carlo_1d_x_squared() {
let (est, _) = monte_carlo(|x: &[f64]| x[0] * x[0], &[(0.0, 1.0)], 1_000_000, 99)
.expect("monte_carlo should succeed");
assert!((est - 1.0 / 3.0).abs() < 0.005, "est={est}");
}
#[test]
fn test_monte_carlo_invalid_bounds() {
assert!(monte_carlo(|_: &[f64]| 1.0, &[(1.0, 0.0)], 100, 0).is_err());
assert!(monte_carlo(|_: &[f64]| 1.0, &[], 100, 0).is_err());
}
#[test]
fn test_qmc_2d_sum() {
let (est, _) =
quasi_monte_carlo(|xy: &[f64]| xy[0] + xy[1], &[(0.0, 1.0), (0.0, 1.0)], 4096)
.expect("quasi_monte_carlo should succeed");
assert!((est - 1.0).abs() < 0.01, "est={est}");
}
#[test]
fn test_qmc_3d() {
let (est, _) = quasi_monte_carlo(
|p: &[f64]| p[0] * p[1] * p[2],
&[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
65536,
)
.expect("quasi_monte_carlo should succeed");
assert!((est - 0.125).abs() < 0.05, "est={est}");
}
#[test]
fn test_product_gauss_xy() {
let result = product_gauss(|xy: &[f64]| xy[0] * xy[1], &[(0.0, 1.0), (0.0, 1.0)], 5)
.expect("product_gauss should succeed");
assert!((result - 0.25).abs() < 1e-12, "result={result}");
}
#[test]
fn test_product_gauss_3d_xyz() {
let result = product_gauss(
|p: &[f64]| p[0] * p[1] * p[2],
&[(0.0, 1.0), (0.0, 1.0), (0.0, 1.0)],
5,
)
.expect("product_gauss should succeed");
assert!((result - 0.125).abs() < 1e-12, "result={result}");
}
#[test]
fn test_product_gauss_polynomial_exact() {
let result = product_gauss(
|p: &[f64]| p[0].powi(3) * p[1].powi(3),
&[(-1.0, 1.0), (-1.0, 1.0)],
4,
)
.expect("product_gauss should succeed");
assert!(result.abs() < 1e-13, "result={result}");
}
#[test]
fn test_genz_malik_2d_gaussian() {
let (est, err) = genz_malik(
|xy: &[f64]| (-(xy[0] * xy[0] + xy[1] * xy[1])).exp(),
&[(0.0, 1.0), (0.0, 1.0)],
1e-8,
1e-8,
200_000,
)
.expect("genz_malik should succeed");
let exact = (PI.sqrt() / 2.0 * libm::erf(1.0)).powi(2);
assert!(
(est - exact).abs() < 1e-5,
"est={est} exact={exact} err={err}"
);
}
#[test]
fn test_genz_malik_2d_polynomial() {
let (est, _err) = genz_malik(
|xy: &[f64]| xy[0] * xy[0] + xy[1] * xy[1],
&[(0.0, 1.0), (0.0, 1.0)],
1e-10,
1e-10,
100_000,
)
.expect("genz_malik should succeed");
assert!((est - 2.0 / 3.0).abs() < 1e-8, "est={est}");
}
#[test]
fn test_genz_malik_invalid_dim() {
assert!(genz_malik(|_: &[f64]| 1.0, &[(0.0, 1.0)], 1e-6, 1e-6, 1000).is_err());
assert!(genz_malik(|_: &[f64]| 1.0, &[(0.0, 1.0); 8], 1e-6, 1e-6, 1000).is_err());
}
#[test]
fn test_romberg_polynomial_exact() {
let result =
romberg(|x: f64| x.powi(5), 0.0, 1.0, 12, 1e-12).expect("romberg should succeed");
assert!((result - 1.0 / 6.0).abs() < 1e-12, "result={result}");
}
#[test]
fn test_romberg_sin() {
let result = romberg(|x: f64| x.sin(), 0.0, PI, 15, 1e-12).expect("romberg should succeed");
assert!((result - 2.0).abs() < 1e-12, "result={result}");
}
#[test]
fn test_romberg_exp() {
let result =
romberg(|x: f64| x.exp(), 0.0, 1.0, 10, 1e-12).expect("romberg should succeed");
assert!(
(result - (std::f64::consts::E - 1.0)).abs() < 1e-12,
"result={result}"
);
}
#[test]
fn test_romberg_invalid_bounds() {
assert!(romberg(|x: f64| x, 1.0, 0.0, 10, 1e-10).is_err());
assert!(romberg(|x: f64| x, 0.0, 1.0, 0, 1e-10).is_err());
assert!(romberg(|x: f64| x, 0.0, 1.0, 10, 0.0).is_err());
}
}