Skip to main content

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}