use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::prelude::*;
use scirs2_core::random::Uniform;
use scirs2_core::Distribution;
pub trait QmcSequence {
fn next_point(&mut self) -> Vec<f64>;
fn dim(&self) -> usize;
fn reset(&mut self);
fn generate(&mut self, n: usize) -> Array2<f64> {
let d = self.dim();
let mut out = Array2::<f64>::zeros((n, d));
for i in 0..n {
let p = self.next_point();
for j in 0..d {
out[[i, j]] = p[j];
}
}
out
}
}
const PRIMES_21: [usize; 21] = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73,
];
pub struct HaltonSequence {
bases: Vec<usize>,
index: usize,
}
impl HaltonSequence {
pub fn new(dim: usize) -> Self {
let bases = if dim <= PRIMES_21.len() {
PRIMES_21[..dim].to_vec()
} else {
let mut primes = PRIMES_21.to_vec();
let mut candidate = *PRIMES_21.last().expect("table is non-empty") + 2;
while primes.len() < dim {
if is_prime(candidate) {
primes.push(candidate);
}
candidate += 2;
}
primes
};
Self { bases, index: 0 }
}
pub fn with_bases(bases: Vec<usize>) -> IntegrateResult<Self> {
if bases.is_empty() {
return Err(IntegrateError::ValueError(
"bases must be non-empty".to_string(),
));
}
Ok(Self { bases, index: 0 })
}
}
impl QmcSequence for HaltonSequence {
fn next_point(&mut self) -> Vec<f64> {
self.index += 1;
self.bases
.iter()
.map(|&b| van_der_corput(self.index, b))
.collect()
}
fn dim(&self) -> usize {
self.bases.len()
}
fn reset(&mut self) {
self.index = 0;
}
}
fn van_der_corput(mut n: usize, base: usize) -> f64 {
let mut result = 0.0_f64;
let mut denom = 1.0_f64;
while n > 0 {
denom *= base as f64;
result += (n % base) as f64 / denom;
n /= base;
}
result
}
fn is_prime(n: usize) -> bool {
if n < 2 {
return false;
}
if n == 2 {
return true;
}
if n.is_multiple_of(2) {
return false;
}
let mut d = 3;
while d * d <= n {
if n.is_multiple_of(d) {
return false;
}
d += 2;
}
true
}
pub struct SobolSequence {
dim: usize,
direction: Vec<Vec<u64>>,
x: Vec<u64>,
n: usize,
bits: usize,
}
const SOBOL_BITS: usize = 30;
impl SobolSequence {
pub fn new(dim: usize) -> IntegrateResult<Self> {
if dim == 0 {
return Err(IntegrateError::ValueError("dim must be ≥ 1".to_string()));
}
let direction = initialise_direction_numbers(dim, SOBOL_BITS);
let x = vec![0u64; dim];
Ok(Self {
dim,
direction,
x,
n: 0,
bits: SOBOL_BITS,
})
}
}
impl QmcSequence for SobolSequence {
fn next_point(&mut self) -> Vec<f64> {
let c = trailing_zeros_plus1(self.n);
self.n += 1;
let scale = (1u64 << self.bits) as f64;
let mut out = Vec::with_capacity(self.dim);
for d in 0..self.dim {
let bit = c.min(self.bits) - 1;
self.x[d] ^= self.direction[d][bit];
out.push(self.x[d] as f64 / scale);
}
out
}
fn dim(&self) -> usize {
self.dim
}
fn reset(&mut self) {
self.x = vec![0u64; self.dim];
self.n = 0;
}
}
fn trailing_zeros_plus1(n: usize) -> usize {
(!n).trailing_zeros() as usize + 1
}
fn initialise_direction_numbers(dim: usize, bits: usize) -> Vec<Vec<u64>> {
let poly_data: &[(u32, u32)] = &[
(1, 0), (2, 1), (3, 1), (3, 2), (4, 1), (4, 4), (5, 2), (5, 4), (5, 7), (5, 11), (5, 13), (5, 14), (6, 1), (6, 13), (6, 16), (6, 19), (6, 22), (6, 25), (7, 1), (7, 4), ];
let mut dirs: Vec<Vec<u64>> = Vec::with_capacity(dim);
let d0: Vec<u64> = (0..bits).map(|k| 1u64 << (bits - 1 - k)).collect();
dirs.push(d0);
for d in 1..dim {
let (degree, coeff) = if d - 1 < poly_data.len() {
poly_data[d - 1]
} else {
(1 + (d as u32 / 20), (d as u32 % 4) + 1)
};
let s = degree as usize;
let m_init: Vec<u64> = (1..=s.min(bits))
.map(|k| {
let raw = 2u64.pow(k as u32) - 1;
(raw | 1) % (1u64 << k)
})
.collect();
let mut v: Vec<u64> = vec![0u64; bits];
for k in 0..s.min(bits) {
v[k] = m_init[k] * (1u64 << (bits - 1 - k));
}
for k in s..bits {
let mut x = v[k - s] ^ (v[k - s] >> s);
for j in 1..s {
if (coeff >> (s - 1 - j)) & 1 == 1 {
x ^= v[k - j];
}
}
v[k] = x;
}
dirs.push(v);
}
dirs
}
pub struct LatticeRule {
n: usize,
dim: usize,
generator: Vec<u64>,
index: usize,
}
impl LatticeRule {
pub fn korobov(n: usize, dim: usize) -> IntegrateResult<Self> {
if n == 0 {
return Err(IntegrateError::ValueError("n must be > 0".to_string()));
}
if dim == 0 {
return Err(IntegrateError::ValueError("dim must be > 0".to_string()));
}
let phi = 0.6180339887_f64; let a = ((n as f64 * phi).round() as u64).max(1);
let mut gen = vec![0u64; dim];
gen[0] = 1;
for d in 1..dim {
gen[d] = (gen[d - 1] * a) % (n as u64);
}
Ok(Self {
n,
dim,
generator: gen,
index: 0,
})
}
pub fn with_generator(n: usize, generator: Vec<u64>) -> IntegrateResult<Self> {
if n == 0 {
return Err(IntegrateError::ValueError("n must be > 0".to_string()));
}
if generator.is_empty() {
return Err(IntegrateError::ValueError(
"generator must be non-empty".to_string(),
));
}
let dim = generator.len();
Ok(Self {
n,
dim,
generator,
index: 0,
})
}
pub fn points(&self) -> Array2<f64> {
let n_f = self.n as f64;
let mut out = Array2::<f64>::zeros((self.n, self.dim));
for k in 0..self.n {
for d in 0..self.dim {
out[[k, d]] = ((k as u64 * self.generator[d]) % (self.n as u64)) as f64 / n_f;
}
}
out
}
pub fn n_points(&self) -> usize {
self.n
}
}
impl QmcSequence for LatticeRule {
fn next_point(&mut self) -> Vec<f64> {
let k = self.index % self.n;
self.index += 1;
let n_f = self.n as f64;
self.generator
.iter()
.map(|&g| ((k as u64 * g) % (self.n as u64)) as f64 / n_f)
.collect()
}
fn dim(&self) -> usize {
self.dim
}
fn reset(&mut self) {
self.index = 0;
}
}
#[derive(Debug, Clone)]
pub struct ScrambledNetOptions {
pub n_replicates: usize,
pub seed: Option<u64>,
}
impl Default for ScrambledNetOptions {
fn default() -> Self {
Self {
n_replicates: 16,
seed: None,
}
}
}
pub fn scrambled_net<F>(
f: F,
ranges: &[(f64, f64)],
n_points: usize,
options: Option<ScrambledNetOptions>,
) -> IntegrateResult<(f64, f64)>
where
F: Fn(&[f64]) -> f64,
{
let opts = options.unwrap_or_default();
let dim = ranges.len();
if dim == 0 {
return Err(IntegrateError::ValueError(
"ranges must be non-empty".to_string(),
));
}
if n_points == 0 {
return Err(IntegrateError::ValueError(
"n_points must be positive".to_string(),
));
}
if opts.n_replicates < 2 {
return Err(IntegrateError::ValueError(
"n_replicates must be ≥ 2 for error estimation".to_string(),
));
}
let mut rng = match opts.seed {
Some(s) => StdRng::seed_from_u64(s),
None => {
let mut os = scirs2_core::random::rng();
StdRng::from_rng(&mut os)
}
};
let volume: f64 = ranges.iter().map(|(a, b)| b - a).product();
let uniform01 = Uniform::new(0.0_f64, 1.0).expect("valid bounds");
let mut replicate_estimates: Vec<f64> = Vec::with_capacity(opts.n_replicates);
for _rep in 0..opts.n_replicates {
let shifts: Vec<f64> = (0..dim).map(|_| uniform01.sample(&mut rng)).collect();
let mut seq = HaltonSequence::new(dim);
let mut sum = 0.0_f64;
for _ in 0..n_points {
let raw = seq.next_point();
let shifted: Vec<f64> = raw
.iter()
.zip(shifts.iter())
.map(|(x, s)| (x + s).fract())
.collect();
let mapped: Vec<f64> = shifted
.iter()
.zip(ranges.iter())
.map(|(u, (a, b))| a + (b - a) * u)
.collect();
sum += f(&mapped);
}
replicate_estimates.push(sum / (n_points as f64) * volume);
}
let n_r = opts.n_replicates as f64;
let mean: f64 = replicate_estimates.iter().sum::<f64>() / n_r;
let var: f64 = replicate_estimates
.iter()
.map(|&v| (v - mean) * (v - mean))
.sum::<f64>()
/ (n_r * (n_r - 1.0));
let std_error = var.sqrt();
Ok((mean, std_error))
}
#[derive(Debug, Clone)]
pub struct QmcIntegrateOptions {
pub n_points: usize,
pub seed: Option<u64>,
}
impl Default for QmcIntegrateOptions {
fn default() -> Self {
Self {
n_points: 10000,
seed: None,
}
}
}
#[derive(Debug, Clone)]
pub struct QmcIntegrateResult {
pub value: f64,
pub n_evals: usize,
}
pub fn qmc_integrate<F, S>(
f: F,
ranges: &[(f64, f64)],
seq: &mut S,
options: Option<QmcIntegrateOptions>,
) -> IntegrateResult<QmcIntegrateResult>
where
F: Fn(&[f64]) -> f64,
S: QmcSequence,
{
let opts = options.unwrap_or_default();
if ranges.is_empty() {
return Err(IntegrateError::ValueError(
"ranges must be non-empty".to_string(),
));
}
if opts.n_points == 0 {
return Err(IntegrateError::ValueError(
"n_points must be positive".to_string(),
));
}
let dim = ranges.len();
if seq.dim() != dim {
return Err(IntegrateError::DimensionMismatch(format!(
"sequence dim {} ≠ ranges len {}",
seq.dim(),
dim
)));
}
let volume: f64 = ranges.iter().map(|(a, b)| b - a).product();
let shifts: Option<Vec<f64>> = opts.seed.map(|s| {
let mut rng = StdRng::seed_from_u64(s);
let u01 = Uniform::new(0.0_f64, 1.0).expect("valid bounds");
(0..dim).map(|_| u01.sample(&mut rng)).collect()
});
seq.reset();
let mut sum = 0.0_f64;
let mut mapped = vec![0.0_f64; dim];
for _ in 0..opts.n_points {
let raw = seq.next_point();
for j in 0..dim {
let u = match &shifts {
Some(sh) => (raw[j] + sh[j]).fract(),
None => raw[j],
};
let (a, b) = ranges[j];
mapped[j] = a + (b - a) * u;
}
sum += f(&mapped);
}
Ok(QmcIntegrateResult {
value: sum / (opts.n_points as f64) * volume,
n_evals: opts.n_points,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
#[test]
fn test_halton_range() {
let mut seq = HaltonSequence::new(3);
for _ in 0..1000 {
let p = seq.next_point();
for &x in &p {
assert!((0.0..1.0).contains(&x), "Halton point out of [0,1)");
}
}
}
#[test]
fn test_halton_integrate_x_squared() {
let mut seq = HaltonSequence::new(1);
let opts = QmcIntegrateOptions {
n_points: 8192,
seed: None,
};
let res = qmc_integrate(|p| p[0] * p[0], &[(0.0, 1.0)], &mut seq, Some(opts))
.expect("Halton integrate failed");
assert!(
(res.value - 1.0 / 3.0).abs() < 0.005,
"Halton estimate {} ≠ 1/3",
res.value
);
}
#[test]
fn test_sobol_range() {
let mut seq = SobolSequence::new(4).expect("valid dim");
for _ in 0..1000 {
let p = seq.next_point();
assert_eq!(p.len(), 4);
for &x in &p {
assert!((0.0..=1.0).contains(&x), "Sobol point out of [0,1]");
}
}
}
#[test]
fn test_sobol_integrate_2d() {
let mut seq = SobolSequence::new(2).expect("valid dim");
let opts = QmcIntegrateOptions {
n_points: 4096,
seed: None,
};
let res = qmc_integrate(
|p| p[0] + p[1],
&[(0.0, 1.0), (0.0, 1.0)],
&mut seq,
Some(opts),
)
.expect("Sobol integrate failed");
assert!(
(res.value - 1.0).abs() < 0.01,
"Sobol estimate {} ≠ 1.0",
res.value
);
}
#[test]
fn test_lattice_rule_integrate() {
let lat = LatticeRule::korobov(1024, 1).expect("valid params");
let pts = lat.points();
let sum: f64 = (0..1024).map(|i| (2.0 * PI * pts[[i, 0]]).cos()).sum();
let est = sum / 1024.0;
assert!(est.abs() < 0.01, "lattice cosine integral {} ≠ 0", est);
}
#[test]
fn test_scrambled_net() {
let (est, se) =
scrambled_net(|p| p[0], &[(0.0, 1.0)], 1024, None).expect("scrambled net failed");
assert!((est - 0.5).abs() < 0.02, "scrambled net est {} ≠ 0.5", est);
assert!(se >= 0.0, "std error must be non-negative");
}
#[test]
fn test_qmc_integrate_with_shift() {
let mut seq = HaltonSequence::new(1);
let opts = QmcIntegrateOptions {
n_points: 8192,
seed: Some(1234),
};
let res = qmc_integrate(|p| (PI * p[0]).sin(), &[(0.0, 1.0)], &mut seq, Some(opts))
.expect("shifted QMC failed");
assert!(
(res.value - 2.0 / PI).abs() < 0.01,
"shifted QMC {} ≠ 2/π",
res.value
);
}
#[test]
fn test_sobol_reset() {
let mut seq = SobolSequence::new(2).expect("valid dim");
let pts_first: Vec<Vec<f64>> = (0..8).map(|_| seq.next_point()).collect();
seq.reset();
let pts_second: Vec<Vec<f64>> = (0..8).map(|_| seq.next_point()).collect();
assert_eq!(
pts_first, pts_second,
"Sobol reset should reproduce sequence"
);
}
}