advanced_algorithms/optimization/
monte_carlo.rs

1//! Monte Carlo Integration
2//!
3//! Uses random sampling to estimate integrals, especially useful for
4//! high-dimensional integrals that are difficult to compute analytically.
5//!
6//! Supports parallel processing for improved performance.
7//!
8//! # Examples
9//!
10//! ```
11//! use advanced_algorithms::optimization::monte_carlo;
12//!
13//! // Estimate integral of x^2 from 0 to 1
14//! let f = |x: &[f64]| x[0] * x[0];
15//! let bounds = vec![(0.0, 1.0)];
16//!
17//! let result = monte_carlo::integrate(f, &bounds, 100000).unwrap();
18//! // Should be close to 1/3 ≈ 0.333...
19//! assert!((result.value - 0.333).abs() < 0.01);
20//! ```
21
22use crate::{AlgorithmError, Result};
23use rand::Rng;
24use rayon::prelude::*;
25
26/// Performs Monte Carlo integration
27///
28/// # Arguments
29///
30/// * `f` - The function to integrate
31/// * `bounds` - Integration bounds for each dimension [(min, max), ...]
32/// * `n_samples` - Number of random samples to use
33///
34/// # Returns
35///
36/// Integration result with estimate and error
37pub fn integrate<F>(
38    f: F,
39    bounds: &[(f64, f64)],
40    n_samples: usize,
41) -> Result<IntegrationResult>
42where
43    F: Fn(&[f64]) -> f64 + Sync,
44{
45    if bounds.is_empty() {
46        return Err(AlgorithmError::InvalidInput(
47            "Must provide at least one dimension".to_string()
48        ));
49    }
50    
51    if n_samples == 0 {
52        return Err(AlgorithmError::InvalidInput(
53            "Number of samples must be positive".to_string()
54        ));
55    }
56    
57    // Validate bounds
58    for (min, max) in bounds {
59        if min >= max {
60            return Err(AlgorithmError::InvalidInput(
61                format!("Invalid bounds: {} >= {}", min, max)
62            ));
63        }
64    }
65    
66    // Compute volume
67    let volume: f64 = bounds.iter()
68        .map(|(min, max)| max - min)
69        .product();
70    
71    // Use parallel sampling for large n_samples
72    if n_samples >= 10000 {
73        integrate_parallel(f, bounds, n_samples, volume)
74    } else {
75        integrate_sequential(f, bounds, n_samples, volume)
76    }
77}
78
79fn integrate_sequential<F>(
80    f: F,
81    bounds: &[(f64, f64)],
82    n_samples: usize,
83    volume: f64,
84) -> Result<IntegrationResult>
85where
86    F: Fn(&[f64]) -> f64,
87{
88    let _dim = bounds.len();
89    let mut rng = rand::thread_rng();
90    
91    let mut sum = 0.0;
92    let mut sum_sq = 0.0;
93    
94    for _ in 0..n_samples {
95        let point: Vec<f64> = bounds.iter()
96            .map(|(min, max)| rng.gen_range(*min..*max))
97            .collect();
98        
99        let value = f(&point);
100        sum += value;
101        sum_sq += value * value;
102    }
103    
104    let mean = sum / n_samples as f64;
105    let variance = (sum_sq / n_samples as f64) - (mean * mean);
106    
107    let integral = volume * mean;
108    let error = volume * (variance / n_samples as f64).sqrt();
109    
110    Ok(IntegrationResult {
111        value: integral,
112        error,
113        n_samples,
114        variance,
115    })
116}
117
118fn integrate_parallel<F>(
119    f: F,
120    bounds: &[(f64, f64)],
121    n_samples: usize,
122    volume: f64,
123) -> Result<IntegrationResult>
124where
125    F: Fn(&[f64]) -> f64 + Sync,
126{
127    let _dim = bounds.len();
128    
129    // Divide work among threads
130    let chunk_size = (n_samples / rayon::current_num_threads()).max(1000);
131    let n_chunks = n_samples.div_ceil(chunk_size);
132    
133    let results: Vec<(f64, f64, usize)> = (0..n_chunks)
134        .into_par_iter()
135        .map(|chunk_idx| {
136            let mut rng = rand::thread_rng();
137            let start = chunk_idx * chunk_size;
138            let end = (start + chunk_size).min(n_samples);
139            let chunk_n = end - start;
140            
141            let mut sum = 0.0;
142            let mut sum_sq = 0.0;
143            
144            for _ in 0..chunk_n {
145                let point: Vec<f64> = bounds.iter()
146                    .map(|(min, max)| rng.gen_range(*min..*max))
147                    .collect();
148                
149                let value = f(&point);
150                sum += value;
151                sum_sq += value * value;
152            }
153            
154            (sum, sum_sq, chunk_n)
155        })
156        .collect();
157    
158    // Combine results
159    let total_sum: f64 = results.iter().map(|(s, _, _)| s).sum();
160    let total_sum_sq: f64 = results.iter().map(|(_, sq, _)| sq).sum();
161    let total_n: usize = results.iter().map(|(_, _, n)| n).sum();
162    
163    let mean = total_sum / total_n as f64;
164    let variance = (total_sum_sq / total_n as f64) - (mean * mean);
165    
166    let integral = volume * mean;
167    let error = volume * (variance / total_n as f64).sqrt();
168    
169    Ok(IntegrationResult {
170        value: integral,
171        error,
172        n_samples: total_n,
173        variance,
174    })
175}
176
177/// Monte Carlo integration with importance sampling
178///
179/// # Arguments
180///
181/// * `f` - The function to integrate
182/// * `proposal` - Proposal distribution for sampling
183/// * `pdf` - Probability density function of proposal distribution
184/// * `bounds` - Integration bounds
185/// * `n_samples` - Number of samples
186pub fn integrate_importance_sampling<F, P, Q>(
187    f: F,
188    proposal: P,
189    pdf: Q,
190    bounds: &[(f64, f64)],
191    n_samples: usize,
192) -> Result<IntegrationResult>
193where
194    F: Fn(&[f64]) -> f64 + Sync,
195    P: Fn() -> Vec<f64> + Sync,
196    Q: Fn(&[f64]) -> f64 + Sync,
197{
198    let _volume: f64 = bounds.iter()
199        .map(|(min, max)| max - min)
200        .product();
201    
202    let results: Vec<f64> = (0..n_samples)
203        .into_par_iter()
204        .map(|_| {
205            let point = proposal();
206            let f_val = f(&point);
207            let p_val = pdf(&point);
208            
209            if p_val > 1e-10 {
210                f_val / p_val
211            } else {
212                0.0
213            }
214        })
215        .collect();
216    
217    let sum: f64 = results.iter().sum();
218    let sum_sq: f64 = results.iter().map(|x| x * x).sum();
219    
220    let mean = sum / n_samples as f64;
221    let variance = (sum_sq / n_samples as f64) - (mean * mean);
222    
223    let integral = mean;
224    let error = (variance / n_samples as f64).sqrt();
225    
226    Ok(IntegrationResult {
227        value: integral,
228        error,
229        n_samples,
230        variance,
231    })
232}
233
234/// Result of Monte Carlo integration
235#[derive(Debug, Clone)]
236pub struct IntegrationResult {
237    /// Estimated integral value
238    pub value: f64,
239    /// Estimated error (standard deviation)
240    pub error: f64,
241    /// Number of samples used
242    pub n_samples: usize,
243    /// Variance of samples
244    pub variance: f64,
245}
246
247impl IntegrationResult {
248    /// Returns the relative error (error / value)
249    pub fn relative_error(&self) -> f64 {
250        if self.value.abs() > 1e-10 {
251            self.error / self.value.abs()
252        } else {
253            self.error
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use std::f64::consts::PI;
262    
263    #[test]
264    fn test_simple_integral() {
265        // Integrate x^2 from 0 to 1, should be 1/3
266        let f = |x: &[f64]| x[0] * x[0];
267        let bounds = vec![(0.0, 1.0)];
268        
269        let result = integrate(f, &bounds, 100000).unwrap();
270        
271        assert!((result.value - 1.0/3.0).abs() < 0.01);
272    }
273    
274    #[test]
275    fn test_multidimensional() {
276        // Integrate x*y over [0,1]×[0,1], should be 1/4
277        let f = |x: &[f64]| x[0] * x[1];
278        let bounds = vec![(0.0, 1.0), (0.0, 1.0)];
279        
280        let result = integrate(f, &bounds, 100000).unwrap();
281        
282        assert!((result.value - 0.25).abs() < 0.01);
283    }
284    
285    #[test]
286    fn test_circle_area() {
287        // Estimate π by integrating over unit circle
288        let f = |x: &[f64]| {
289            if x[0]*x[0] + x[1]*x[1] <= 1.0 {
290                1.0
291            } else {
292                0.0
293            }
294        };
295        let bounds = vec![(-1.0, 1.0), (-1.0, 1.0)];
296        
297        let result = integrate(f, &bounds, 100000).unwrap();
298        
299        // Area of unit circle is π, area of square is 4
300        // So integral should be π
301        assert!((result.value - PI).abs() < 0.1);
302    }
303    
304    #[test]
305    fn test_parallel() {
306        let f = |x: &[f64]| x[0] * x[0];
307        let bounds = vec![(0.0, 1.0)];
308        
309        let result = integrate(f, &bounds, 100000).unwrap();
310        
311        assert!((result.value - 1.0/3.0).abs() < 0.01);
312        assert!(result.error > 0.0);
313    }
314}