Skip to main content

scirs2_integrate/monte_carlo/
mod.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//!
6//! ## Submodules
7//!
8//! | Submodule | Description |
9//! |-----------|-------------|
10//! | [`mod@importance_sampling`] | IS, self-normalized IS, sequential IS, ESS, stratified sampling |
11//! | [`quasi_monte_carlo`] | Halton, Sobol, lattice rules, Owen scrambling |
12//! | [`multilevel_mc`] | MLMC estimator, adaptive levels, GBM hierarchy |
13//! | [`basic_mc`] | MonteCarloIntegrator: 1-D, N-D, antithetic, control variate, stratified |
14//! | [`path_sampling`] | Feynman-Kac, diffusion bridge, Metropolis walk, AIS |
15
16pub mod basic_mc;
17pub mod importance_sampling;
18pub mod multilevel_mc;
19pub mod path_sampling;
20pub mod quasi_monte_carlo;
21
22// Re-export key items from submodules for convenience
23// basic_mc types are accessible as monte_carlo::basic_mc::{MCResult, MonteCarloIntegrator}
24// to avoid conflict with monte_carlo_advanced::MonteCarloIntegrator
25pub use importance_sampling::{
26    effective_sample_size, importance_sampling_integral, self_normalized_is, sequential_is,
27    stratified_sampling, ImportanceSamplingResult, StratifiedResult,
28};
29pub use multilevel_mc::{
30    geometric_brownian_motion_levels, mlmc_adaptive, mlmc_complexity, mlmc_integrate, MLMCLevel,
31    MLMCOptions, MLMCResult,
32};
33pub use path_sampling::{
34    annealed_importance_sampling, diffusion_bridge, metropolis_walk, AISOptions, AISResult,
35    DiffusionBridgeResult, FeynmanKacEstimator, FeynmanKacOptions, MetropolisWalkOptions,
36    MetropolisWalkResult,
37};
38pub use quasi_monte_carlo::{
39    qmc_integrate, scrambled_net, HaltonSequence, LatticeRule, QmcIntegrateOptions,
40    QmcIntegrateResult, SobolSequence,
41};
42
43use crate::error::{IntegrateError, IntegrateResult};
44use crate::IntegrateFloat;
45use scirs2_core::ndarray::{Array1, ArrayView1};
46use scirs2_core::random::prelude::*;
47use scirs2_core::random::uniform::SampleUniform;
48use scirs2_core::random::{Distribution, Uniform};
49use std::f64::consts::PI;
50use std::fmt::Debug;
51use std::marker::PhantomData;
52
53/// Options for controlling the behavior of Monte Carlo integration
54#[derive(Debug, Clone)]
55pub struct MonteCarloOptions<F: IntegrateFloat> {
56    /// Number of sample points to use
57    pub n_samples: usize,
58    /// Random number generator seed (for reproducibility)
59    pub seed: Option<u64>,
60    /// Error estimation method
61    pub error_method: ErrorEstimationMethod,
62    /// Use antithetic variates for variance reduction
63    pub use_antithetic: bool,
64    /// Phantom data for generic type
65    pub phantom: PhantomData<F>,
66}
67
68/// Method for estimating the error in Monte Carlo integration
69#[derive(Debug, Clone, Copy, PartialEq)]
70pub enum ErrorEstimationMethod {
71    /// Standard error of the mean
72    StandardError,
73    /// Batch means method
74    BatchMeans,
75}
76
77impl<F: IntegrateFloat> Default for MonteCarloOptions<F> {
78    fn default() -> Self {
79        Self {
80            n_samples: 10000,
81            seed: None,
82            error_method: ErrorEstimationMethod::StandardError,
83            use_antithetic: false,
84            phantom: PhantomData,
85        }
86    }
87}
88
89/// Result of a Monte Carlo integration
90#[derive(Debug, Clone)]
91pub struct MonteCarloResult<F: IntegrateFloat> {
92    /// Estimated value of the integral
93    pub value: F,
94    /// Estimated standard error
95    pub std_error: F,
96    /// Number of function evaluations
97    pub n_evals: usize,
98}
99
100/// Perform Monte Carlo integration of a function over a hypercube
101///
102/// # Arguments
103///
104/// * `f` - The function to integrate
105/// * `ranges` - Integration ranges (a, b) for each dimension
106/// * `options` - Optional Monte Carlo parameters
107///
108/// # Returns
109///
110/// * `IntegrateResult<MonteCarloResult<F>>` - The result of the integration
111///
112/// # Examples
113///
114/// ```
115/// use scirs2_integrate::monte_carlo::{monte_carlo, MonteCarloOptions};
116/// use scirs2_core::ndarray::ArrayView1;
117/// use std::marker::PhantomData;
118///
119/// // Integrate f(x,y) = x²+y² over [0,1]×[0,1] (exact result: 2/3)
120/// let options = MonteCarloOptions {
121///     n_samples: 100000,  // Use more samples for better accuracy
122///     phantom: PhantomData,
123///     ..Default::default()
124/// };
125///
126/// let result = monte_carlo(
127///     |x: ArrayView1<f64>| x[0] * x[0] + x[1] * x[1],
128///     &[(0.0, 1.0), (0.0, 1.0)],
129///     Some(options)
130/// ).expect("Operation failed");
131///
132/// // Should be close to 2/3, but Monte Carlo has statistical error
133/// assert!((result.value - 2.0/3.0).abs() < 0.01);
134/// ```
135#[allow(dead_code)]
136pub fn monte_carlo<F, Func>(
137    f: Func,
138    ranges: &[(F, F)],
139    options: Option<MonteCarloOptions<F>>,
140) -> IntegrateResult<MonteCarloResult<F>>
141where
142    F: IntegrateFloat + Send + Sync + SampleUniform,
143    Func: Fn(ArrayView1<F>) -> F + Sync,
144    scirs2_core::random::StandardNormal: Distribution<F>,
145{
146    let opts = options.unwrap_or_default();
147    let n_dims = ranges.len();
148
149    if n_dims == 0 {
150        return Err(IntegrateError::ValueError(
151            "Integration ranges cannot be empty".to_string(),
152        ));
153    }
154
155    if opts.n_samples == 0 {
156        return Err(IntegrateError::ValueError(
157            "Number of samples must be positive".to_string(),
158        ));
159    }
160
161    // Calculate the volume of the integration domain
162    let mut volume = F::one();
163    for &(a, b) in ranges {
164        volume *= b - a;
165    }
166
167    // Initialize random number generator
168    let mut rng = if let Some(seed) = opts.seed {
169        StdRng::seed_from_u64(seed)
170    } else {
171        let mut os_rng = scirs2_core::random::rng();
172        StdRng::from_rng(&mut os_rng)
173    };
174
175    // Create uniform distributions for each dimension
176    let distributions: Vec<_> = ranges
177        .iter()
178        .map(|&(a, b)| Uniform::new_inclusive(a, b).expect("valid range for Uniform distribution"))
179        .collect();
180
181    // Prepare to store samples
182    let n_actual_samples = if opts.use_antithetic {
183        opts.n_samples / 2 * 2 // Ensure even number for antithetic pairs
184    } else {
185        opts.n_samples
186    };
187
188    // Buffer for a single sample point
189    let mut point = Array1::zeros(n_dims);
190
191    // Sample and evaluate the function
192    let mut sum = F::zero();
193    let mut sum_sq = F::zero();
194    let mut n_evals = 0;
195
196    if opts.use_antithetic {
197        // Use antithetic sampling for variance reduction
198        for _ in 0..(n_actual_samples / 2) {
199            for (i, dist) in distributions.iter().enumerate() {
200                point[i] = dist.sample(&mut rng);
201            }
202            let value = f(point.view());
203            sum += value;
204            sum_sq += value * value;
205            n_evals += 1;
206
207            // Antithetic point: reflect around the center of the hypercube
208            for (i, &(a, b)) in ranges.iter().enumerate() {
209                point[i] = a + b - point[i];
210            }
211            let antithetic_value = f(point.view());
212            sum += antithetic_value;
213            sum_sq += antithetic_value * antithetic_value;
214            n_evals += 1;
215        }
216    } else {
217        // Standard Monte Carlo sampling
218        for _ in 0..n_actual_samples {
219            for (i, dist) in distributions.iter().enumerate() {
220                point[i] = dist.sample(&mut rng);
221            }
222            let value = f(point.view());
223            sum += value;
224            sum_sq += value * value;
225            n_evals += 1;
226        }
227    }
228
229    let mean = sum / F::from_usize(n_actual_samples).expect("n_actual_samples fits in F");
230    let integral_value = mean * volume;
231
232    let std_error = match opts.error_method {
233        ErrorEstimationMethod::StandardError | ErrorEstimationMethod::BatchMeans => {
234            let variance = (sum_sq
235                - sum * sum / F::from_usize(n_actual_samples).expect("n_actual_samples fits in F"))
236                / F::from_usize(n_actual_samples - 1).expect("n_actual_samples-1 fits in F");
237            (variance / F::from_usize(n_actual_samples).expect("n_actual_samples fits in F")).sqrt()
238                * volume
239        }
240    };
241
242    Ok(MonteCarloResult {
243        value: integral_value,
244        std_error,
245        n_evals,
246    })
247}
248
249/// Perform Monte Carlo integration with importance sampling (legacy API)
250///
251/// For the more complete importance sampling API see
252/// [`importance_sampling::importance_sampling_integral`].
253///
254/// # Examples
255///
256/// ```
257/// use scirs2_integrate::monte_carlo::{importance_sampling, MonteCarloOptions};
258/// use scirs2_core::ndarray::{Array1, ArrayView1};
259/// use scirs2_core::random::prelude::*;
260///
261/// // Uniform sampler for [0,1]
262/// let uniform_sampler = |rng: &mut StdRng, dims: usize| {
263///     let mut point = Array1::zeros(dims);
264///     for i in 0..dims {
265///         point[i] = rng.random_range(0.0..1.0);
266///     }
267///     point
268/// };
269///
270/// // Uniform PDF on [0,1]
271/// let uniform_pdf = |x: ArrayView1<f64>| -> f64 {
272///     for &xi in x.iter() {
273///         if xi < 0.0 || xi > 1.0 { return 0.0; }
274///     }
275///     1.0
276/// };
277///
278/// let options = MonteCarloOptions {
279///     n_samples: 10000,
280///     seed: Some(42),
281///     ..Default::default()
282/// };
283///
284/// let result = importance_sampling(
285///     |x: ArrayView1<f64>| x[0] * x[0],
286///     uniform_pdf,
287///     uniform_sampler,
288///     &[(0.0, 1.0)],
289///     Some(options)
290/// ).expect("Operation failed");
291///
292/// assert!((result.value - 1.0/3.0).abs() < 0.01);
293/// ```
294#[allow(dead_code)]
295pub fn importance_sampling<F, Func, Pdf, Sampler>(
296    f: Func,
297    g: Pdf,
298    sampler: Sampler,
299    ranges: &[(F, F)],
300    options: Option<MonteCarloOptions<F>>,
301) -> IntegrateResult<MonteCarloResult<F>>
302where
303    F: IntegrateFloat + Send + Sync + SampleUniform,
304    Func: Fn(ArrayView1<F>) -> F + Sync,
305    Pdf: Fn(ArrayView1<F>) -> F + Sync,
306    Sampler: Fn(&mut StdRng, usize) -> Array1<F> + Sync,
307    scirs2_core::random::StandardNormal: Distribution<F>,
308{
309    let opts = options.unwrap_or_default();
310    let n_dims = ranges.len();
311
312    if n_dims == 0 {
313        return Err(IntegrateError::ValueError(
314            "Integration ranges cannot be empty".to_string(),
315        ));
316    }
317
318    if opts.n_samples == 0 {
319        return Err(IntegrateError::ValueError(
320            "Number of samples must be positive".to_string(),
321        ));
322    }
323
324    let mut rng = if let Some(seed) = opts.seed {
325        StdRng::seed_from_u64(seed)
326    } else {
327        let mut os_rng = scirs2_core::random::rng();
328        StdRng::from_rng(&mut os_rng)
329    };
330
331    let mut sum = F::zero();
332    let mut sum_sq = F::zero();
333    let n_samples = opts.n_samples;
334    let mut valid_samples = 0;
335
336    for _ in 0..n_samples {
337        let point = sampler(&mut rng, n_dims);
338
339        let fx = f(point.view());
340        let gx = g(point.view());
341
342        if gx <= F::from_f64(1e-10).expect("1e-10 fits in F") {
343            continue;
344        }
345
346        if fx.is_nan() || fx.is_infinite() || gx.is_nan() || gx.is_infinite() {
347            continue;
348        }
349
350        let ratio = fx / gx;
351
352        if ratio.is_nan() || ratio.is_infinite() {
353            continue;
354        }
355
356        sum += ratio;
357        sum_sq += ratio * ratio;
358        valid_samples += 1;
359    }
360
361    if valid_samples < 10 {
362        return Err(IntegrateError::ConvergenceError(
363            "Too few valid samples in importance sampling".to_string(),
364        ));
365    }
366
367    let valid_samples_f = F::from_usize(valid_samples).expect("valid_samples fits in F");
368    let mean = sum / valid_samples_f;
369
370    let variance = if valid_samples > 1 {
371        (sum_sq - sum * sum / valid_samples_f)
372            / F::from_usize(valid_samples - 1).expect("valid_samples-1 fits in F")
373    } else {
374        F::zero()
375    };
376
377    let std_error = (variance / valid_samples_f).sqrt();
378
379    Ok(MonteCarloResult {
380        value: mean,
381        std_error,
382        n_evals: valid_samples,
383    })
384}
385
386/// Parallel Monte Carlo integration using multiple threads
387///
388/// # Examples
389///
390/// ```rust
391/// use scirs2_integrate::monte_carlo::{monte_carlo_parallel, MonteCarloOptions};
392/// use scirs2_core::ndarray::ArrayView1;
393/// use std::marker::PhantomData;
394///
395/// let options = MonteCarloOptions {
396///     n_samples: 100000,
397///     ..Default::default()
398/// };
399///
400/// let result = monte_carlo_parallel(
401///     |x: ArrayView1<f64>| (x[0] * x[1]).sin() * (-x[0]*x[0] - x[1]*x[1]).exp(),
402///     &[(-2.0, 2.0), (-2.0, 2.0)],
403///     Some(options),
404///     Some(4)
405/// ).expect("Operation failed");
406/// ```
407#[allow(dead_code)]
408pub fn monte_carlo_parallel<F, Func>(
409    f: Func,
410    ranges: &[(F, F)],
411    options: Option<MonteCarloOptions<F>>,
412    workers: Option<usize>,
413) -> IntegrateResult<MonteCarloResult<F>>
414where
415    F: IntegrateFloat + Send + Sync + SampleUniform,
416    Func: Fn(ArrayView1<F>) -> F + Sync + Send,
417    scirs2_core::random::StandardNormal: Distribution<F>,
418{
419    #[cfg(feature = "parallel")]
420    {
421        if workers.is_some() {
422            use crate::monte_carlo_parallel::{parallel_monte_carlo, ParallelMonteCarloOptions};
423
424            let opts = options.unwrap_or_default();
425            let parallel_opts = ParallelMonteCarloOptions {
426                n_samples: opts.n_samples,
427                seed: opts.seed,
428                error_method: opts.error_method,
429                use_antithetic: opts.use_antithetic,
430                n_threads: workers,
431                batch_size: 1000,
432                use_chunking: true,
433                phantom: PhantomData,
434            };
435
436            return parallel_monte_carlo(f, ranges, Some(parallel_opts));
437        }
438    }
439
440    let _ = workers;
441    monte_carlo(f, ranges, options)
442}
443
444#[cfg(test)]
445mod tests {
446    use crate::{importance_sampling, monte_carlo, monte_carlo_parallel, MonteCarloOptions};
447    use scirs2_core::ndarray::{Array1, ArrayView1};
448    use scirs2_core::random::prelude::StdRng;
449    use scirs2_core::random::Distribution;
450    use std::f64::consts::PI;
451    use std::marker::PhantomData;
452
453    fn is_close_enough(result: f64, expected: f64, epsilon: f64) -> bool {
454        (result - expected).abs() < epsilon
455    }
456
457    #[test]
458    fn test_monte_carlo_1d() {
459        let options = MonteCarloOptions {
460            n_samples: 100000,
461            seed: Some(12345),
462            phantom: PhantomData,
463            ..Default::default()
464        };
465
466        let result =
467            monte_carlo(|x| x[0] * x[0], &[(0.0, 1.0)], Some(options)).expect("Operation failed");
468
469        assert!(is_close_enough(result.value, 1.0 / 3.0, 0.01));
470    }
471
472    #[test]
473    fn test_monte_carlo_2d() {
474        let options = MonteCarloOptions {
475            n_samples: 100000,
476            seed: Some(12345),
477            phantom: PhantomData,
478            ..Default::default()
479        };
480
481        let result = monte_carlo(
482            |x| x[0] * x[0] + x[1] * x[1],
483            &[(0.0, 1.0), (0.0, 1.0)],
484            Some(options),
485        )
486        .expect("Failed to integrate");
487
488        assert!(is_close_enough(result.value, 2.0 / 3.0, 0.01));
489    }
490
491    #[test]
492    fn test_monte_carlo_with_antithetic() {
493        let options = MonteCarloOptions {
494            n_samples: 100000,
495            seed: Some(12345),
496            use_antithetic: true,
497            phantom: PhantomData,
498            ..Default::default()
499        };
500
501        let result =
502            monte_carlo(|x| x[0] * x[0], &[(0.0, 1.0)], Some(options)).expect("Operation failed");
503
504        assert!(is_close_enough(result.value, 1.0 / 3.0, 0.01));
505    }
506
507    #[test]
508    fn test_importance_sampling() {
509        let sampler = |rng: &mut StdRng, dims: usize| {
510            let mut point = Array1::zeros(dims);
511            let normal = scirs2_core::random::Normal::new(0.0, 1.0).expect("valid normal params");
512
513            for i in 0..dims {
514                let mut x: f64 = normal.sample(rng);
515                x = x.abs();
516                if x > 3.0 {
517                    x = 6.0 - x;
518                    if x < 0.0 {
519                        x = 0.0;
520                    }
521                }
522                point[i] = x;
523            }
524            point
525        };
526
527        let normal_pdf = |x: ArrayView1<f64>| {
528            let mut pdf_val = 1.0;
529            for &xi in x.iter() {
530                let z = xi;
531                let density = (-0.5 * z * z).exp() / (2.0f64 * PI).sqrt();
532                let folded_density = if xi < 3.0 { 2.0 * density } else { 0.0 };
533                pdf_val *= folded_density.max(1e-10);
534            }
535            pdf_val
536        };
537
538        let options = MonteCarloOptions {
539            n_samples: 100000,
540            seed: Some(12345),
541            phantom: PhantomData,
542            ..Default::default()
543        };
544
545        let exact_value = (PI).sqrt() / 2.0 * libm::erf(3.0);
546
547        let result = importance_sampling(
548            |x| (-x[0] * x[0]).exp(),
549            normal_pdf,
550            sampler,
551            &[(0.0, 3.0)],
552            Some(options),
553        )
554        .expect("Failed to integrate");
555
556        assert!(is_close_enough(result.value, exact_value, 0.1));
557        println!(
558            "Importance sampling test: got {}, expected {}",
559            result.value, exact_value
560        );
561    }
562
563    #[test]
564    fn test_monte_carlo_parallel_workers() {
565        let f = |x: ArrayView1<f64>| x[0].powi(2);
566        let ranges = vec![(0.0, 1.0)];
567        let options = MonteCarloOptions {
568            n_samples: 10000,
569            ..Default::default()
570        };
571
572        let result =
573            monte_carlo_parallel(f, &ranges, Some(options), Some(2)).expect("Operation failed");
574
575        assert!(is_close_enough(result.value, 1.0 / 3.0, 0.1));
576        assert!(result.std_error >= 0.0);
577    }
578}