use numr::dtype::DType;
use numr::error::Result;
use numr::ops::{AdvancedRandomOps, RandomOps, ReduceOps, ScalarOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
use numr::tensor::Tensor;
use crate::integrate::traits::{MonteCarloMethod, MonteCarloOptions, MonteCarloResult};
pub fn monte_carlo_impl<R, C, F>(
client: &C,
f: F,
bounds: &[(f64, f64)],
options: &MonteCarloOptions,
) -> Result<MonteCarloResult<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R>
+ ScalarOps<R>
+ RandomOps<R>
+ AdvancedRandomOps<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 n_samples = options.n_samples;
let volume: f64 = bounds.iter().map(|(a, b)| b - a).product();
match options.method {
MonteCarloMethod::Plain => {
plain_monte_carlo(client, &f, bounds, n_samples, volume, options.seed)
}
MonteCarloMethod::Stratified { n_strata } => stratified_monte_carlo(
client,
&f,
bounds,
n_samples,
volume,
n_strata,
options.seed,
),
MonteCarloMethod::Antithetic => {
antithetic_monte_carlo(client, &f, bounds, n_samples, volume, options.seed)
}
}
}
fn plain_monte_carlo<R, C, F>(
client: &C,
f: &F,
bounds: &[(f64, f64)],
n_samples: usize,
volume: f64,
seed: Option<u64>,
) -> Result<MonteCarloResult<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R>
+ ScalarOps<R>
+ RandomOps<R>
+ AdvancedRandomOps<R>
+ ReduceOps<R>
+ RuntimeClient<R>,
F: Fn(&Tensor<R>) -> Result<Tensor<R>>,
{
let device = client.device();
let n_dims = bounds.len();
let samples = match seed {
Some(s) => client.philox_uniform(&[n_samples, n_dims], s, 0, DType::F64)?,
None => client.rand(&[n_samples, n_dims], DType::F64)?,
};
let x = transform_to_bounds_tensor(client, &samples, bounds)?;
let f_values = f(&x)?;
let (mean_scalar, variance_scalar) = compute_mean_variance_tensor(client, &f_values)?;
let std_error = (variance_scalar / n_samples as f64).sqrt() * volume;
let integral = volume * mean_scalar;
let integral_tensor = Tensor::<R>::from_slice(&[integral], &[1], device);
Ok(MonteCarloResult {
integral: integral_tensor,
std_error,
n_samples,
})
}
fn stratified_monte_carlo<R, C, F>(
client: &C,
f: &F,
bounds: &[(f64, f64)],
n_samples: usize,
volume: f64,
n_strata_per_dim: usize,
seed: Option<u64>,
) -> Result<MonteCarloResult<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R>
+ ScalarOps<R>
+ RandomOps<R>
+ AdvancedRandomOps<R>
+ ReduceOps<R>
+ RuntimeClient<R>,
F: Fn(&Tensor<R>) -> Result<Tensor<R>>,
{
let device = client.device();
let n_dims = bounds.len();
let total_strata = n_strata_per_dim.pow(n_dims as u32);
let samples_per_stratum = (n_samples / total_strata).max(1);
let actual_samples = samples_per_stratum * total_strata;
let rand_samples = match seed {
Some(s) => client.philox_uniform(&[actual_samples, n_dims], s, 0, DType::F64)?,
None => client.rand(&[actual_samples, n_dims], DType::F64)?,
};
let stratum_size = 1.0 / n_strata_per_dim as f64;
let mut offsets = Vec::with_capacity(actual_samples * n_dims);
for stratum_idx in 0..total_strata {
let mut idx = stratum_idx;
for _ in 0..n_dims {
let stratum_d = idx % n_strata_per_dim;
idx /= n_strata_per_dim;
let offset = stratum_d as f64 * stratum_size;
for _ in 0..samples_per_stratum {
offsets.push(offset);
}
}
}
let mut offsets_correct = Vec::with_capacity(actual_samples * n_dims);
for sample_idx in 0..actual_samples {
let stratum_idx = sample_idx / samples_per_stratum;
let mut idx = stratum_idx;
for _ in 0..n_dims {
let stratum_d = idx % n_strata_per_dim;
idx /= n_strata_per_dim;
offsets_correct.push(stratum_d as f64 * stratum_size);
}
}
let offset_tensor =
Tensor::<R>::from_slice(&offsets_correct, &[actual_samples, n_dims], device);
let scaled_rand = client.mul_scalar(&rand_samples, stratum_size)?;
let stratified_samples = client.add(&offset_tensor, &scaled_rand)?;
let x = transform_to_bounds_tensor(client, &stratified_samples, bounds)?;
let f_values = f(&x)?;
let (mean_scalar, variance_scalar) = compute_mean_variance_tensor(client, &f_values)?;
let std_error = (variance_scalar / actual_samples as f64).sqrt() * volume;
let integral = volume * mean_scalar;
let integral_tensor = Tensor::<R>::from_slice(&[integral], &[1], device);
Ok(MonteCarloResult {
integral: integral_tensor,
std_error,
n_samples: actual_samples,
})
}
fn antithetic_monte_carlo<R, C, F>(
client: &C,
f: &F,
bounds: &[(f64, f64)],
n_samples: usize,
volume: f64,
seed: Option<u64>,
) -> Result<MonteCarloResult<R>>
where
R: Runtime<DType = DType>,
C: TensorOps<R>
+ ScalarOps<R>
+ RandomOps<R>
+ AdvancedRandomOps<R>
+ ReduceOps<R>
+ RuntimeClient<R>,
F: Fn(&Tensor<R>) -> Result<Tensor<R>>,
{
let device = client.device();
let n_dims = bounds.len();
let n_base = n_samples / 2;
let actual_samples = n_base * 2;
let base_samples = match seed {
Some(s) => client.philox_uniform(&[n_base, n_dims], s, 0, DType::F64)?,
None => client.rand(&[n_base, n_dims], DType::F64)?,
};
let x = transform_to_bounds_tensor(client, &base_samples, bounds)?;
let x_anti = transform_antithetic_tensor(client, &x, bounds)?;
let f_x = f(&x)?;
let f_x_anti = f(&x_anti)?;
let sum = client.add(&f_x, &f_x_anti)?;
let paired_avg = client.div_scalar(&sum, 2.0)?;
let (mean_scalar, variance_scalar) = compute_mean_variance_tensor(client, &paired_avg)?;
let std_error = (variance_scalar / n_base as f64).sqrt() * volume;
let integral = volume * mean_scalar;
let integral_tensor = Tensor::<R>::from_slice(&[integral], &[1], device);
Ok(MonteCarloResult {
integral: integral_tensor,
std_error,
n_samples: actual_samples,
})
}
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)
}
fn transform_antithetic_tensor<R, C>(
client: &C,
x: &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 sums: Vec<f64> = bounds.iter().map(|(a, b)| a + b).collect();
let sum_tensor = Tensor::<R>::from_slice(&sums, &[1, n_dims], device);
client.sub(&sum_tensor, x)
}
fn compute_mean_variance_tensor<R, C>(client: &C, values: &Tensor<R>) -> Result<(f64, f64)>
where
R: Runtime<DType = DType>,
C: TensorOps<R> + ScalarOps<R> + ReduceOps<R> + RuntimeClient<R>,
{
let n = values.numel();
if n == 0 {
return Ok((0.0, 0.0));
}
let sum_tensor = client.sum(values, &[0], false)?;
let mean_tensor = client.div_scalar(&sum_tensor, n as f64)?;
let mean_scalar: f64 = mean_tensor.item()?;
if n == 1 {
return Ok((mean_scalar, 0.0));
}
let mean_broadcast = client.add_scalar(
&Tensor::<R>::from_slice(&[0.0], &[1], client.device()),
mean_scalar,
)?;
let centered = client.sub(values, &mean_broadcast)?;
let squared = client.mul(¢ered, ¢ered)?;
let sum_sq = client.sum(&squared, &[0], false)?;
let variance_tensor = client.div_scalar(&sum_sq, (n - 1) as f64)?;
let variance_scalar: f64 = variance_tensor.item()?;
Ok((mean_scalar, variance_scalar))
}
#[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_monte_carlo_unit_square() {
let (device, client) = setup();
let result = monte_carlo_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)],
&MonteCarloOptions::with_samples(10000),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - 1.0).abs() < 0.1,
"integral = {}, expected 1.0",
integral[0]
);
}
#[test]
fn test_monte_carlo_circle() {
let (device, client) = setup();
let result = monte_carlo_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)],
&MonteCarloOptions::with_samples(50000),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - PI).abs() < 0.2,
"integral = {}, expected π ≈ {}",
integral[0],
PI
);
}
#[test]
fn test_stratified_monte_carlo() {
let (device, client) = setup();
let result = monte_carlo_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)],
&MonteCarloOptions::with_samples(10000)
.method(MonteCarloMethod::Stratified { n_strata: 10 }),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - 1.0).abs() < 0.1,
"integral = {}, expected 1.0",
integral[0]
);
}
#[test]
fn test_antithetic_monte_carlo() {
let (device, client) = setup();
let result = monte_carlo_impl(
&client,
|x| {
let data: Vec<f64> = x.to_vec();
let n = data.len();
Ok(Tensor::<CpuRuntime>::from_slice(&data, &[n], &device))
},
&[(0.0, 1.0)],
&MonteCarloOptions::with_samples(10000).method(MonteCarloMethod::Antithetic),
)
.unwrap();
let integral: Vec<f64> = result.integral.to_vec();
assert!(
(integral[0] - 0.5).abs() < 0.05,
"integral = {}, expected 0.5",
integral[0]
);
}
#[test]
fn test_monte_carlo_reproducibility_plain() {
let (device, client) = setup();
let seed = 42u64;
let f = |x: &Tensor<CpuRuntime>| {
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(xi * xi + yi * yi);
}
Ok(Tensor::<CpuRuntime>::from_slice(&vals, &[n], &device))
};
let result1 = monte_carlo_impl(
&client,
f,
&[(0.0, 1.0), (0.0, 1.0)],
&MonteCarloOptions::with_samples(1000).seed(seed),
)
.unwrap();
let result2 = monte_carlo_impl(
&client,
f,
&[(0.0, 1.0), (0.0, 1.0)],
&MonteCarloOptions::with_samples(1000).seed(seed),
)
.unwrap();
let integral1: f64 = result1.integral.to_vec()[0];
let integral2: f64 = result2.integral.to_vec()[0];
assert_eq!(
integral1, integral2,
"Same seed should produce identical results"
);
}
#[test]
fn test_monte_carlo_reproducibility_stratified() {
let (device, client) = setup();
let seed = 123u64;
let f = |x: &Tensor<CpuRuntime>| {
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(xi + yi);
}
Ok(Tensor::<CpuRuntime>::from_slice(&vals, &[n], &device))
};
let options = MonteCarloOptions::with_samples(1000)
.method(MonteCarloMethod::Stratified { n_strata: 5 })
.seed(seed);
let result1 = monte_carlo_impl(&client, f, &[(0.0, 1.0), (0.0, 1.0)], &options).unwrap();
let result2 = monte_carlo_impl(&client, f, &[(0.0, 1.0), (0.0, 1.0)], &options).unwrap();
let integral1: f64 = result1.integral.to_vec()[0];
let integral2: f64 = result2.integral.to_vec()[0];
assert_eq!(
integral1, integral2,
"Same seed should produce identical results for stratified"
);
}
#[test]
fn test_monte_carlo_reproducibility_antithetic() {
let (device, client) = setup();
let seed = 999u64;
let f = |x: &Tensor<CpuRuntime>| {
let data: Vec<f64> = x.to_vec();
let n = data.len();
let vals: Vec<f64> = data.iter().map(|&xi| xi.exp()).collect();
Ok(Tensor::<CpuRuntime>::from_slice(&vals, &[n], &device))
};
let options = MonteCarloOptions::with_samples(1000)
.method(MonteCarloMethod::Antithetic)
.seed(seed);
let result1 = monte_carlo_impl(&client, f, &[(0.0, 1.0)], &options).unwrap();
let result2 = monte_carlo_impl(&client, f, &[(0.0, 1.0)], &options).unwrap();
let integral1: f64 = result1.integral.to_vec()[0];
let integral2: f64 = result2.integral.to_vec()[0];
assert_eq!(
integral1, integral2,
"Same seed should produce identical results for antithetic"
);
}
}