advanced_algorithms/optimization/
monte_carlo.rs1use crate::{AlgorithmError, Result};
23use rand::Rng;
24use rayon::prelude::*;
25
26pub 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 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 let volume: f64 = bounds.iter()
68 .map(|(min, max)| max - min)
69 .product();
70
71 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 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 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
177pub 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#[derive(Debug, Clone)]
236pub struct IntegrationResult {
237 pub value: f64,
239 pub error: f64,
241 pub n_samples: usize,
243 pub variance: f64,
245}
246
247impl IntegrationResult {
248 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 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 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 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 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}