scirs2_integrate/monte_carlo.rs
1//! Monte Carlo integration methods
2//!
3//! This module provides numerical integration methods based on Monte Carlo
4//! sampling, which are particularly useful for high-dimensional integrals.
5
6use crate::error::{IntegrateError, IntegrateResult};
7use crate::IntegrateFloat;
8use scirs2_core::ndarray::{Array1, ArrayView1};
9use scirs2_core::random::prelude::*;
10use scirs2_core::random::uniform::SampleUniform;
11use scirs2_core::random::{Distribution, Uniform};
12use std::f64::consts::PI;
13use std::fmt::Debug;
14use std::marker::PhantomData;
15
16/// Options for controlling the behavior of Monte Carlo integration
17#[derive(Debug, Clone)]
18pub struct MonteCarloOptions<F: IntegrateFloat> {
19 /// Number of sample points to use
20 pub n_samples: usize,
21 /// Random number generator seed (for reproducibility)
22 pub seed: Option<u64>,
23 /// Error estimation method
24 pub error_method: ErrorEstimationMethod,
25 /// Use antithetic variates for variance reduction
26 pub use_antithetic: bool,
27 /// Phantom data for generic type
28 pub phantom: PhantomData<F>,
29}
30
31/// Method for estimating the error in Monte Carlo integration
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub enum ErrorEstimationMethod {
34 /// Standard error of the mean
35 StandardError,
36 /// Batch means method
37 BatchMeans,
38}
39
40impl<F: IntegrateFloat> Default for MonteCarloOptions<F> {
41 fn default() -> Self {
42 Self {
43 n_samples: 10000,
44 seed: None,
45 error_method: ErrorEstimationMethod::StandardError,
46 use_antithetic: false,
47 phantom: PhantomData,
48 }
49 }
50}
51
52/// Result of a Monte Carlo integration
53#[derive(Debug, Clone)]
54pub struct MonteCarloResult<F: IntegrateFloat> {
55 /// Estimated value of the integral
56 pub value: F,
57 /// Estimated standard error
58 pub std_error: F,
59 /// Number of function evaluations
60 pub n_evals: usize,
61}
62
63/// Perform Monte Carlo integration of a function over a hypercube
64///
65/// # Arguments
66///
67/// * `f` - The function to integrate
68/// * `ranges` - Integration ranges (a, b) for each dimension
69/// * `options` - Optional Monte Carlo parameters
70///
71/// # Returns
72///
73/// * `IntegrateResult<MonteCarloResult<F>>` - The result of the integration
74///
75/// # Examples
76///
77/// ```
78/// use scirs2_integrate::monte_carlo::{monte_carlo, MonteCarloOptions};
79/// use scirs2_core::ndarray::ArrayView1;
80/// use std::marker::PhantomData;
81///
82/// // Integrate f(x,y) = x²+y² over [0,1]×[0,1] (exact result: 2/3)
83/// let options = MonteCarloOptions {
84/// n_samples: 100000, // Use more samples for better accuracy
85/// phantom: PhantomData,
86/// ..Default::default()
87/// };
88///
89/// let result = monte_carlo(
90/// |x: ArrayView1<f64>| x[0] * x[0] + x[1] * x[1],
91/// &[(0.0, 1.0), (0.0, 1.0)],
92/// Some(options)
93/// ).expect("Operation failed");
94///
95/// // Should be close to 2/3, but Monte Carlo has statistical error
96/// assert!((result.value - 2.0/3.0).abs() < 0.01);
97/// ```
98#[allow(dead_code)]
99pub fn monte_carlo<F, Func>(
100 f: Func,
101 ranges: &[(F, F)],
102 options: Option<MonteCarloOptions<F>>,
103) -> IntegrateResult<MonteCarloResult<F>>
104where
105 F: IntegrateFloat + Send + Sync + SampleUniform,
106 Func: Fn(ArrayView1<F>) -> F + Sync,
107 scirs2_core::random::StandardNormal: Distribution<F>,
108{
109 let opts = options.unwrap_or_default();
110 let n_dims = ranges.len();
111
112 if n_dims == 0 {
113 return Err(IntegrateError::ValueError(
114 "Integration ranges cannot be empty".to_string(),
115 ));
116 }
117
118 if opts.n_samples == 0 {
119 return Err(IntegrateError::ValueError(
120 "Number of samples must be positive".to_string(),
121 ));
122 }
123
124 // Calculate the volume of the integration domain
125 let mut volume = F::one();
126 for &(a, b) in ranges {
127 volume *= b - a;
128 }
129
130 // Initialize random number generator
131 let mut rng = if let Some(seed) = opts.seed {
132 StdRng::seed_from_u64(seed)
133 } else {
134 // In rand 0.9.0, from_entropy is replaced by building from OsRng
135 // Note: scirs2_core::random::rng() was renamed to scirs2_core::random::rng() in rand 0.9.0
136 let mut rng = scirs2_core::random::rng();
137 StdRng::from_rng(&mut rng)
138 };
139
140 // Create uniform distributions for each dimension
141 let distributions: Vec<_> = ranges
142 .iter()
143 .map(|&(a, b)| Uniform::new_inclusive(a, b).expect("Operation failed"))
144 .collect();
145
146 // Prepare to store samples
147 let n_actual_samples = if opts.use_antithetic {
148 opts.n_samples / 2 * 2 // Ensure even number for antithetic pairs
149 } else {
150 opts.n_samples
151 };
152
153 // Buffer for a single sample point
154 let mut point = Array1::zeros(n_dims);
155
156 // Sample and evaluate the function
157 let mut sum = F::zero();
158 let mut sum_sq = F::zero();
159 let mut n_evals = 0;
160
161 if opts.use_antithetic {
162 // Use antithetic sampling for variance reduction
163 for _ in 0..(n_actual_samples / 2) {
164 // Generate a random point in the hypercube
165 for (i, dist) in distributions.iter().enumerate() {
166 point[i] = dist.sample(&mut rng);
167 }
168 let value = f(point.view());
169 sum += value;
170 sum_sq += value * value;
171 n_evals += 1;
172
173 // Antithetic point: reflect around the center of the hypercube
174 for (i, &(a, b)) in ranges.iter().enumerate() {
175 point[i] = a + b - point[i]; // Reflect: a+b-x is the reflection of x relative to midpoint (a+b)/2
176 }
177 let antithetic_value = f(point.view());
178 sum += antithetic_value;
179 sum_sq += antithetic_value * antithetic_value;
180 n_evals += 1;
181 }
182 } else {
183 // Standard Monte Carlo sampling
184 for _ in 0..n_actual_samples {
185 // Generate a random point in the hypercube
186 for (i, dist) in distributions.iter().enumerate() {
187 point[i] = dist.sample(&mut rng);
188 }
189 let value = f(point.view());
190 sum += value;
191 sum_sq += value * value;
192 n_evals += 1;
193 }
194 }
195
196 // Compute the mean value and scale by the volume
197 let mean = sum / F::from_usize(n_actual_samples).expect("Operation failed");
198 let integral_value = mean * volume;
199
200 // Estimate the error based on the specified method
201 let std_error = match opts.error_method {
202 ErrorEstimationMethod::StandardError => {
203 // Standard error of the mean using sample variance
204 let variance = (sum_sq
205 - sum * sum / F::from_usize(n_actual_samples).expect("Operation failed"))
206 / F::from_usize(n_actual_samples - 1).expect("Operation failed");
207
208 (variance / F::from_usize(n_actual_samples).expect("Operation failed")).sqrt() * volume
209 }
210 ErrorEstimationMethod::BatchMeans => {
211 // Divide samples into batches and compute variance of batch means
212 // This requires re-sampling, so we'll skip the actual implementation for simplicity
213 // In a real implementation, we would compute batch means from the original samples
214
215 // For now, we'll just use a simplified estimate based on the standard error
216 let variance = (sum_sq
217 - sum * sum / F::from_usize(n_actual_samples).expect("Operation failed"))
218 / F::from_usize(n_actual_samples - 1).expect("Operation failed");
219
220 (variance / F::from_usize(n_actual_samples).expect("Operation failed")).sqrt() * volume
221 }
222 };
223
224 Ok(MonteCarloResult {
225 value: integral_value,
226 std_error,
227 n_evals,
228 })
229}
230
231/// Perform Monte Carlo integration with importance sampling
232///
233/// # Arguments
234///
235/// * `f` - The function to integrate
236/// * `g` - The probability density function to sample from
237/// * `sampler` - A function that generates samples from the PDF g
238/// * `ranges` - Integration ranges (a, b) for each dimension
239/// * `options` - Optional Monte Carlo parameters
240///
241/// # Returns
242///
243/// * `IntegrateResult<MonteCarloResult<F>>` - The result of the integration
244///
245/// # Examples
246///
247/// ```
248/// use scirs2_integrate::monte_carlo::{importance_sampling, MonteCarloOptions};
249/// use scirs2_core::ndarray::{Array1, ArrayView1};
250/// use scirs2_core::random::prelude::*;
251///
252/// // Simple example: integrate x² from 0 to 1 using uniform importance sampling
253/// // (which is the same as regular Monte Carlo in this case)
254///
255/// // Uniform sampler
256/// let uniform_sampler = |rng: &mut StdRng, dims: usize| {
257/// let mut point = Array1::zeros(dims);
258/// for i in 0..dims {
259/// point[i] = rng.random_range(0.0..1.0);
260/// }
261/// point
262/// };
263///
264/// // Uniform PDF on [0..1]
265/// let uniform_pdf = |x: ArrayView1<f64>| {
266/// let mut pdf = 1.0;
267/// for &xi in x.iter() {
268/// if xi < 0.0 || xi > 1.0 {
269/// return 0.0;
270/// }
271/// }
272/// pdf
273/// };
274///
275/// let options = MonteCarloOptions {
276/// n_samples: 10000,
277/// seed: Some(42),
278/// ..Default::default()
279/// };
280///
281/// // Integrate f(x) = x² from 0 to 1 (exact result: 1/3)
282/// let result = importance_sampling(
283/// |x: ArrayView1<f64>| x[0] * x[0],
284/// uniform_pdf,
285/// uniform_sampler,
286/// &[(0.0, 1.0)],
287/// Some(options)
288/// ).expect("Operation failed");
289///
290/// assert!((result.value - 1.0/3.0).abs() < 0.01);
291/// ```
292#[allow(dead_code)]
293pub fn importance_sampling<F, Func, Pdf, Sampler>(
294 f: Func,
295 g: Pdf,
296 sampler: Sampler,
297 ranges: &[(F, F)],
298 options: Option<MonteCarloOptions<F>>,
299) -> IntegrateResult<MonteCarloResult<F>>
300where
301 F: IntegrateFloat + Send + Sync + SampleUniform,
302 Func: Fn(ArrayView1<F>) -> F + Sync,
303 Pdf: Fn(ArrayView1<F>) -> F + Sync,
304 Sampler: Fn(&mut StdRng, usize) -> Array1<F> + Sync,
305 scirs2_core::random::StandardNormal: Distribution<F>,
306{
307 let opts = options.unwrap_or_default();
308 let n_dims = ranges.len();
309
310 if n_dims == 0 {
311 return Err(IntegrateError::ValueError(
312 "Integration ranges cannot be empty".to_string(),
313 ));
314 }
315
316 if opts.n_samples == 0 {
317 return Err(IntegrateError::ValueError(
318 "Number of samples must be positive".to_string(),
319 ));
320 }
321
322 // Initialize random number generator
323 let mut rng = if let Some(seed) = opts.seed {
324 StdRng::seed_from_u64(seed)
325 } else {
326 // In rand 0.9.0, from_entropy is replaced by building from OsRng
327 // Note: scirs2_core::random::rng() was renamed to scirs2_core::random::rng() in rand 0.9.0
328 let mut rng = scirs2_core::random::rng();
329 StdRng::from_rng(&mut rng)
330 };
331
332 // Sample and evaluate the function
333 let mut sum = F::zero();
334 let mut sum_sq = F::zero();
335 let n_samples = opts.n_samples;
336
337 // Count valid samples (where g(x) > epsilon)
338 let mut valid_samples = 0;
339
340 for _ in 0..n_samples {
341 // Generate a sample point from the importance distribution
342 let point = sampler(&mut rng, n_dims);
343
344 // Evaluate f(x) for importance sampling
345 let fx = f(point.view());
346
347 // Evaluate g(x) and handle potential numerical issues
348 let gx = g(point.view());
349
350 // Avoid division by zero or very small values that could lead to instability
351 // Use a higher threshold than just epsilon to avoid numerical instability
352 if gx <= F::from_f64(1e-10).expect("Operation failed") {
353 continue;
354 }
355
356 // Check for NaN or Infinity in either fx or gx
357 if fx.is_nan() || fx.is_infinite() || gx.is_nan() || gx.is_infinite() {
358 continue;
359 }
360
361 // Compute the ratio and weight
362 let ratio = fx / gx;
363
364 // Another check for numerical stability of the ratio
365 if ratio.is_nan() || ratio.is_infinite() {
366 continue;
367 }
368
369 // Update accumulators
370 sum += ratio;
371 sum_sq += ratio * ratio;
372 valid_samples += 1;
373 }
374
375 // Check if we have enough valid samples
376 if valid_samples < 10 {
377 return Err(IntegrateError::ConvergenceError(
378 "Too few valid samples in importance sampling".to_string(),
379 ));
380 }
381
382 // Calculate the mean and standard error using valid samples
383 let valid_samples_f = F::from_usize(valid_samples).expect("Operation failed");
384 let mean = sum / valid_samples_f;
385
386 // Compute the standard error
387 let variance = if valid_samples > 1 {
388 (sum_sq - sum * sum / valid_samples_f)
389 / F::from_usize(valid_samples - 1).expect("Operation failed")
390 } else {
391 // If we have only one valid sample, we can't estimate variance
392 F::zero()
393 };
394
395 let std_error = (variance / valid_samples_f).sqrt();
396
397 Ok(MonteCarloResult {
398 value: mean,
399 std_error,
400 n_evals: valid_samples,
401 })
402}
403
404/// Parallel Monte Carlo integration using multiple threads to speed up the computation
405///
406/// This function provides a convenient wrapper that will use parallel processing
407/// if the `parallel` feature is enabled and workers parameter is specified,
408/// otherwise falls back to the standard monte_carlo function.
409///
410/// # Arguments
411///
412/// * `f` - The function to integrate
413/// * `ranges` - The bounds for each dimension, specified as (min, max) pairs
414/// * `options` - Monte Carlo integration options (see [`MonteCarloOptions`])
415/// * `workers` - Number of worker threads to use (if None, falls back to serial)
416///
417/// # Returns
418///
419/// A [`MonteCarloResult`] containing the integral value, standard error, and number of evaluations.
420///
421/// # Examples
422///
423/// ```rust
424/// use scirs2_integrate::monte_carlo::{monte_carlo_parallel, MonteCarloOptions};
425/// use scirs2_core::ndarray::ArrayView1;
426/// use std::marker::PhantomData;
427///
428/// // Integrate an expensive function f(x,y) = sin(x*y) * exp(-x²-y²) over [-2,2]×[-2,2]
429/// let options = MonteCarloOptions {
430/// n_samples: 100000,
431/// ..Default::default()
432/// };
433///
434/// let result = monte_carlo_parallel(
435/// |x: ArrayView1<f64>| (x[0] * x[1]).sin() * (-x[0]*x[0] - x[1]*x[1]).exp(),
436/// &[(-2.0, 2.0), (-2.0, 2.0)],
437/// Some(options),
438/// Some(4)
439/// ).expect("Operation failed");
440/// ```
441#[allow(dead_code)]
442pub fn monte_carlo_parallel<F, Func>(
443 f: Func,
444 ranges: &[(F, F)],
445 options: Option<MonteCarloOptions<F>>,
446 workers: Option<usize>,
447) -> IntegrateResult<MonteCarloResult<F>>
448where
449 F: IntegrateFloat + Send + Sync + SampleUniform,
450 Func: Fn(ArrayView1<F>) -> F + Sync + Send,
451 scirs2_core::random::StandardNormal: Distribution<F>,
452{
453 // If parallel feature is enabled and workers parameter is specified, use parallel processing
454 #[cfg(feature = "parallel")]
455 {
456 if workers.is_some() {
457 use crate::monte_carlo_parallel::{parallel_monte_carlo, ParallelMonteCarloOptions};
458
459 let opts = options.unwrap_or_default();
460 let parallel_opts = ParallelMonteCarloOptions {
461 n_samples: opts.n_samples,
462 seed: opts.seed,
463 error_method: opts.error_method,
464 use_antithetic: opts.use_antithetic,
465 n_threads: workers,
466 batch_size: 1000,
467 use_chunking: true,
468 phantom: PhantomData,
469 };
470
471 return parallel_monte_carlo(f, ranges, Some(parallel_opts));
472 }
473 }
474
475 // Fall back to standard monte carlo implementation
476 let _ = workers; // Silence unused variable warning if parallel feature is not enabled
477 monte_carlo(f, ranges, options)
478}
479
480#[cfg(test)]
481mod tests {
482 use crate::{importance_sampling, monte_carlo, monte_carlo_parallel, MonteCarloOptions};
483 use scirs2_core::ndarray::{Array1, ArrayView1};
484 use scirs2_core::random::prelude::StdRng;
485 use scirs2_core::random::Distribution;
486 use std::f64::consts::PI;
487 use std::marker::PhantomData;
488
489 // Helper function to check if result is within expected error margin
490 fn is_close_enough(result: f64, expected: f64, epsilon: f64) -> bool {
491 (result - expected).abs() < epsilon
492 }
493
494 #[test]
495 fn test_monte_carlo_1d() {
496 // Integrate f(x) = x² from 0 to 1 (exact result: 1/3)
497 let options = MonteCarloOptions {
498 n_samples: 100000,
499 seed: Some(12345), // For reproducibility
500 phantom: PhantomData,
501 ..Default::default()
502 };
503
504 let result =
505 monte_carlo(|x| x[0] * x[0], &[(0.0, 1.0)], Some(options)).expect("Operation failed");
506
507 // Monte Carlo is a statistical method, so we use a loose tolerance
508 assert!(is_close_enough(result.value, 1.0 / 3.0, 0.01));
509 }
510
511 #[test]
512 fn test_monte_carlo_2d() {
513 // Integrate f(x,y) = x² + y² over [0,1]×[0,1] (exact result: 2/3)
514 let options = MonteCarloOptions {
515 n_samples: 100000,
516 seed: Some(12345), // For reproducibility
517 phantom: PhantomData,
518 ..Default::default()
519 };
520
521 let result = monte_carlo(
522 |x| x[0] * x[0] + x[1] * x[1],
523 &[(0.0, 1.0), (0.0, 1.0)],
524 Some(options),
525 )
526 .expect("Failed to integrate");
527
528 assert!(is_close_enough(result.value, 2.0 / 3.0, 0.01));
529 }
530
531 #[test]
532 fn test_monte_carlo_with_antithetic() {
533 // Test with antithetic variates for variance reduction
534 let options = MonteCarloOptions {
535 n_samples: 100000,
536 seed: Some(12345), // For reproducibility
537 use_antithetic: true,
538 phantom: PhantomData,
539 ..Default::default()
540 };
541
542 let result =
543 monte_carlo(|x| x[0] * x[0], &[(0.0, 1.0)], Some(options)).expect("Operation failed");
544
545 assert!(is_close_enough(result.value, 1.0 / 3.0, 0.01));
546
547 // The standard error with antithetic sampling should generally be lower
548 // than without it, but this is hard to test deterministically
549 }
550
551 #[test]
552 fn test_importance_sampling() {
553 // Test importance sampling with a more stable approach
554 // We'll integrate exp(-x²) from 0 to 3 and use a normal distribution
555 // centered at 0 with std dev of 1 as our sampling distribution
556
557 // The Normal distribution sampler
558 let sampler = |rng: &mut StdRng, dims: usize| {
559 let mut point = Array1::zeros(dims);
560 // Use a normal distribution centered at 0 with std dev 1
561 let normal = scirs2_core::random::Normal::new(0.0, 1.0).expect("Operation failed");
562
563 for i in 0..dims {
564 // Sample and truncate to the integration range [0, 3]
565 let mut x: f64 = normal.sample(rng);
566 // Make sure value is within integration range
567 x = x.abs(); // Fold negatives to positive domain
568 if x > 3.0 {
569 x = 6.0 - x; // Reflect values beyond 3.0 back into range
570 if x < 0.0 {
571 // If still out of range, clamp to 0
572 x = 0.0;
573 }
574 }
575 point[i] = x;
576 }
577 point
578 };
579
580 // Normal PDF
581 let normal_pdf = |x: ArrayView1<f64>| {
582 let mut pdf_val = 1.0;
583 for &xi in x.iter() {
584 // Normal density function, but folded to account for our transformation
585 let z = xi;
586 let density = (-0.5 * z * z).exp() / (2.0f64 * PI).sqrt();
587 // Fold the negative domain to account for our transformation
588 let folded_density = if xi < 3.0 {
589 2.0 * density // Double density because we folded the distribution
590 } else {
591 0.0 // Outside integration range, shouldn't happen
592 };
593 pdf_val *= folded_density.max(1e-10); // Prevent zero density
594 }
595 pdf_val
596 };
597
598 let options = MonteCarloOptions {
599 n_samples: 100000,
600 seed: Some(12345), // For reproducibility
601 phantom: PhantomData,
602 ..Default::default()
603 };
604
605 // Integrate f(x) = exp(-x²) from 0 to 3
606 // The exact value is sqrt(π)/2 * erf(3) ≈ 0.886
607 let exact_value = (PI).sqrt() / 2.0 * libm::erf(3.0);
608
609 let result = importance_sampling(
610 |x| (-x[0] * x[0]).exp(),
611 normal_pdf,
612 sampler,
613 &[(0.0, 3.0)],
614 Some(options),
615 )
616 .expect("Failed to integrate");
617
618 // Check result within reasonable error bounds
619 assert!(is_close_enough(result.value, exact_value, 0.1));
620 println!(
621 "Importance sampling test: got {}, expected {}",
622 result.value, exact_value
623 );
624 }
625
626 #[test]
627 fn test_monte_carlo_parallel_workers() {
628 // Test Monte Carlo integration with workers parameter
629 let f = |x: ArrayView1<f64>| x[0].powi(2);
630 let ranges = vec![(0.0, 1.0)];
631 let options = MonteCarloOptions {
632 n_samples: 10000,
633 ..Default::default()
634 };
635
636 // Test with 2 workers
637 let result =
638 monte_carlo_parallel(f, &ranges, Some(options), Some(2)).expect("Operation failed");
639
640 // Expected integral of x^2 over [0,1] is 1/3
641 assert!(is_close_enough(result.value, 1.0 / 3.0, 0.1));
642 assert!(result.std_error >= 0.0);
643 }
644}