use numr::dtype::DType;
use numr::error::Result;
use numr::ops::{QuasiRandomOps, ReduceOps, ScalarOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
use numr::tensor::Tensor;
use crate::integrate::traits::{QMCOptions, QMCSequence, QuadResult};
pub fn qmc_impl<R, C, F>(
client: &C,
f: F,
bounds: &[(f64, f64)],
options: &QMCOptions,
) -> Result<QuadResult<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + QuasiRandomOps<R> + ReduceOps<R> + RuntimeClient<R>,
F: Fn(&Tensor<R>) -> Result<Tensor<R>>,
{
if bounds.is_empty() {
return Err(numr::error::Error::InvalidArgument {
arg: "bounds",
reason: "Bounds cannot be empty".to_string(),
});
}
let device = client.device();
let n_dims = bounds.len();
let n_samples = options.n_samples;
let volume: f64 = bounds.iter().map(|(a, b)| b - a).product();
let samples = match options.sequence {
QMCSequence::Sobol => client.sobol(n_samples, n_dims, options.skip, DType::F64)?,
QMCSequence::Halton => client.halton(n_samples, n_dims, options.skip, DType::F64)?,
};
let x = transform_to_bounds_tensor(client, &samples, bounds)?;
let f_values = f(&x)?;
let sum_tensor = client.sum(&f_values, &[0], false)?;
let mean_tensor = client.div_scalar(&sum_tensor, n_samples as f64)?;
let mean_scalar: f64 = mean_tensor.item()?;
let integral = volume * mean_scalar;
let error = if n_samples >= 2 {
let half_n = n_samples / 2;
let f_half = f_values.narrow(0, 0, half_n)?;
let sum_half = client.sum(&f_half, &[0], false)?;
let mean_half_tensor = client.div_scalar(&sum_half, half_n as f64)?;
let mean_half: f64 = mean_half_tensor.item()?;
let integral_half = volume * mean_half;
(integral - integral_half).abs()
} else {
0.0
};
let integral_tensor = Tensor::<R>::from_slice(&[integral], &[1], device);
Ok(QuadResult {
integral: integral_tensor,
error,
neval: n_samples,
converged: true, })
}
fn transform_to_bounds_tensor<R, C>(
client: &C,
samples: &Tensor<R>,
bounds: &[(f64, f64)],
) -> Result<Tensor<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + RuntimeClient<R>,
{
let device = client.device();
let n_dims = bounds.len();
let scales: Vec<f64> = bounds.iter().map(|(a, b)| b - a).collect();
let offsets: Vec<f64> = bounds.iter().map(|(a, _)| *a).collect();
let scale_tensor = Tensor::<R>::from_slice(&scales, &[1, n_dims], device);
let offset_tensor = Tensor::<R>::from_slice(&offsets, &[1, n_dims], device);
let scaled = client.mul(samples, &scale_tensor)?;
client.add(&scaled, &offset_tensor)
}
#[cfg(test)]
mod tests {
use super::*;
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
use std::f64::consts::PI;
fn setup() -> (CpuDevice, CpuClient) {
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
(device, client)
}
#[test]
fn test_qmc_unit_cube() {
let (device, client) = setup();
let result = qmc_impl(
&client,
|_x| {
let shape = _x.shape();
let n = shape[0];
let ones = vec![1.0; n];
Ok(Tensor::<CpuRuntime>::from_slice(&ones, &[n], &device))
},
&[(0.0, 1.0), (0.0, 1.0)],
&QMCOptions::with_samples(4096),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - 1.0).abs() < 0.01,
"integral = {}, expected 1.0",
integral[0]
);
}
#[test]
fn test_qmc_sobol_1d() {
let (device, client) = setup();
let result = qmc_impl(
&client,
|x| {
let data: Vec<f64> = x.to_vec();
let sq: Vec<f64> = data.iter().map(|&xi| xi * xi).collect();
Ok(Tensor::<CpuRuntime>::from_slice(&sq, &[sq.len()], &device))
},
&[(0.0, 1.0)],
&QMCOptions::with_samples(4096).sequence(QMCSequence::Sobol),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - 1.0 / 3.0).abs() < 0.01,
"integral = {}, expected 1/3",
integral[0]
);
}
#[test]
fn test_qmc_halton_circle() {
let (device, client) = setup();
let result = qmc_impl(
&client,
|x| {
let data: Vec<f64> = x.to_vec();
let n = data.len() / 2;
let mut vals = Vec::with_capacity(n);
for i in 0..n {
let xi = data[i * 2];
let yi = data[i * 2 + 1];
vals.push(if xi * xi + yi * yi <= 1.0 { 1.0 } else { 0.0 });
}
Ok(Tensor::<CpuRuntime>::from_slice(&vals, &[n], &device))
},
&[(-1.0, 1.0), (-1.0, 1.0)],
&QMCOptions::with_samples(16384).sequence(QMCSequence::Halton),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - PI).abs() < 0.1,
"integral = {}, expected π ≈ {}",
integral[0],
PI
);
}
#[test]
fn test_qmc_high_dimension() {
let (device, client) = setup();
let result = qmc_impl(
&client,
|x| {
let shape = x.shape();
let n = shape[0];
let ones = vec![1.0; n];
Ok(Tensor::<CpuRuntime>::from_slice(&ones, &[n], &device))
},
&[(0.0, 1.0); 10],
&QMCOptions::with_samples(8192).sequence(QMCSequence::Sobol),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - 1.0).abs() < 0.01,
"integral = {}, expected 1.0",
integral[0]
);
}
#[test]
fn test_qmc_deterministic() {
let (device, client) = setup();
let opts = QMCOptions::with_samples(1000).sequence(QMCSequence::Sobol);
let result1 = qmc_impl(
&client,
|x| {
let shape = x.shape();
let n = shape[0];
let ones = vec![1.0; n];
Ok(Tensor::<CpuRuntime>::from_slice(&ones, &[n], &device))
},
&[(0.0, 1.0)],
&opts,
)
.unwrap();
let result2 = qmc_impl(
&client,
|x| {
let shape = x.shape();
let n = shape[0];
let ones = vec![1.0; n];
Ok(Tensor::<CpuRuntime>::from_slice(&ones, &[n], &device))
},
&[(0.0, 1.0)],
&opts,
)
.unwrap();
let v1: Vec<f64> = result1.integral.to_vec();
let v2: Vec<f64> = result2.integral.to_vec();
assert_eq!(v1[0], v2[0], "QMC should be deterministic");
}
}