1#![forbid(unsafe_code)]
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
14pub enum Distribution {
15 Uniform { min: f64, max: f64 },
17 Normal { mean: f64, std_dev: f64 },
19 Exponential { lambda: f64 },
21 Triangular { min: f64, mode: f64, max: f64 },
23 Constant(f64),
25}
26
27impl Distribution {
28 #[must_use]
30 pub fn sample(&self, u: f64) -> f64 {
31 match *self {
32 Self::Uniform { min, max } => u.mul_add(max - min, min),
33 Self::Normal { mean, std_dev } => {
34 let u1 = u.max(1e-10);
36 let u2 = (u * 1.618033988749895).fract();
37 let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
38 std_dev.mul_add(z, mean)
39 }
40 Self::Exponential { lambda } => {
41 let u = u.max(1e-10);
42 -u.ln() / lambda
43 }
44 Self::Triangular { min, mode, max } => {
45 let fc = (mode - min) / (max - min);
46 if u < fc {
47 min + (u * (max - min) * (mode - min)).sqrt()
48 } else {
49 max - ((1.0 - u) * (max - min) * (max - mode)).sqrt()
50 }
51 }
52 Self::Constant(v) => v,
53 }
54 }
55
56 #[must_use]
58 pub const fn mean(&self) -> f64 {
59 match *self {
60 Self::Uniform { min, max } => f64::midpoint(min, max),
61 Self::Normal { mean, .. } => mean,
62 Self::Exponential { lambda } => 1.0 / lambda,
63 Self::Triangular { min, mode, max } => (min + mode + max) / 3.0,
64 Self::Constant(v) => v,
65 }
66 }
67
68 #[must_use]
70 pub const fn name(&self) -> &'static str {
71 match self {
72 Self::Uniform { .. } => "uniform",
73 Self::Normal { .. } => "normal",
74 Self::Exponential { .. } => "exponential",
75 Self::Triangular { .. } => "triangular",
76 Self::Constant(_) => "constant",
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct McConfig {
86 pub n_samples: usize,
88 pub seed: u64,
90 pub quasi_mc: bool,
92}
93
94impl Default for McConfig {
95 fn default() -> Self {
96 Self {
97 n_samples: 10_000,
98 seed: 42,
99 quasi_mc: false,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct McResult {
109 pub mean: f64,
111 pub std_dev: f64,
113 pub min: f64,
115 pub max: f64,
117 pub p5: f64,
119 pub p25: f64,
121 pub p50: f64,
123 pub p75: f64,
125 pub p95: f64,
127 pub n_samples: usize,
129}
130
131impl McResult {
132 #[must_use]
134 pub fn ci95_half_width(&self) -> f64 {
135 1.96 * self.std_dev / (self.n_samples as f64).sqrt()
136 }
137
138 #[must_use]
140 pub fn ci95_lower(&self) -> f64 {
141 self.mean - self.ci95_half_width()
142 }
143
144 #[must_use]
146 pub fn ci95_upper(&self) -> f64 {
147 self.mean + self.ci95_half_width()
148 }
149
150 #[must_use]
152 pub fn to_json(&self) -> serde_json::Value {
153 serde_json::json!({
154 "mean": self.mean,
155 "std_dev": self.std_dev,
156 "min": self.min,
157 "max": self.max,
158 "p5": self.p5,
159 "p25": self.p25,
160 "p50": self.p50,
161 "p75": self.p75,
162 "p95": self.p95,
163 "n_samples": self.n_samples,
164 "ci95_lower": self.ci95_lower(),
165 "ci95_upper": self.ci95_upper(),
166 })
167 }
168}
169
170const fn xorshift64(state: &mut u64) -> u64 {
174 let mut x = *state;
175 if x == 0 {
176 x = 0x9E37_79B9_7F4A_7C15;
177 }
178 x ^= x << 13;
179 x ^= x >> 7;
180 x ^= x << 17;
181 *state = x;
182 x
183}
184
185fn rand_u01(state: &mut u64) -> f64 {
187 let r = xorshift64(state);
188 (r >> 11) as f64 / (1u64 << 53) as f64
189}
190
191fn sobol_point(index: usize, dim: usize) -> f64 {
193 let _ = dim;
195 let mut result = 0.0;
196 let mut f = 0.5;
197 let mut i = index + 1;
198 while i > 0 {
199 if i & 1 != 0 {
200 result += f;
201 }
202 i >>= 1;
203 f *= 0.5;
204 }
205 result
206}
207
208pub struct MonteCarloSimulator {
210 config: McConfig,
211 rng_state: u64,
212}
213
214impl Default for MonteCarloSimulator {
215 fn default() -> Self {
216 Self::new(McConfig::default())
217 }
218}
219
220impl std::fmt::Debug for MonteCarloSimulator {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 f.debug_struct("MonteCarloSimulator")
223 .field("config", &self.config)
224 .finish_non_exhaustive()
225 }
226}
227
228impl MonteCarloSimulator {
229 #[must_use]
231 pub fn new(config: McConfig) -> Self {
232 Self {
233 rng_state: if config.seed == 0 {
234 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(42) as u64
235 } else {
236 config.seed
237 },
238 config,
239 }
240 }
241
242 pub fn simulate<F>(&mut self, distributions: &[Distribution], model: F) -> McResult
245 where
246 F: Fn(&[f64]) -> f64,
247 {
248 let n = self.config.n_samples;
249 let mut outputs = Vec::with_capacity(n);
250
251 for i in 0..n {
252 let inputs: Vec<f64> = distributions
253 .iter()
254 .enumerate()
255 .map(|(d, dist)| {
256 if self.config.quasi_mc {
257 let u = sobol_point(i, d);
258 dist.sample(u)
259 } else {
260 let u = rand_u01(&mut self.rng_state);
261 dist.sample(u)
262 }
263 })
264 .collect();
265
266 outputs.push(model(&inputs));
267 }
268
269 Self::compute_stats(&outputs)
270 }
271
272 #[must_use]
274 pub fn compute_stats(samples: &[f64]) -> McResult {
275 let n = samples.len();
276 if n == 0 {
277 return McResult {
278 mean: 0.0,
279 std_dev: 0.0,
280 min: 0.0,
281 max: 0.0,
282 p5: 0.0,
283 p25: 0.0,
284 p50: 0.0,
285 p75: 0.0,
286 p95: 0.0,
287 n_samples: 0,
288 };
289 }
290
291 let mut sorted = samples.to_vec();
292 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
293
294 let mean = sorted.iter().sum::<f64>() / n as f64;
295 let variance = sorted
296 .iter()
297 .map(|x| {
298 let d = x - mean;
299 d * d
300 })
301 .sum::<f64>()
302 / n as f64;
303 let std_dev = variance.sqrt();
304
305 let percentile = |p: f64| -> f64 {
306 let idx = ((p / 100.0) * (n - 1) as f64).round() as usize;
307 sorted[idx.min(n - 1)]
308 };
309
310 McResult {
311 mean,
312 std_dev,
313 min: sorted[0],
314 max: sorted[n - 1],
315 p5: percentile(5.0),
316 p25: percentile(25.0),
317 p50: percentile(50.0),
318 p75: percentile(75.0),
319 p95: percentile(95.0),
320 n_samples: n,
321 }
322 }
323
324 pub fn integrate<F>(&mut self, bounds: &[(f64, f64)], f: F) -> McResult
326 where
327 F: Fn(&[f64]) -> f64,
328 {
329 let distributions: Vec<Distribution> = bounds
330 .iter()
331 .map(|(min, max)| Distribution::Uniform {
332 min: *min,
333 max: *max,
334 })
335 .collect();
336
337 let volume: f64 = bounds.iter().map(|(min, max)| max - min).product();
338
339 self.simulate(&distributions, |inputs| f(inputs) * volume)
340 }
341
342 #[must_use]
344 pub const fn config(&self) -> &McConfig {
345 &self.config
346 }
347}
348
349#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn distribution_uniform_sample() {
357 let d = Distribution::Uniform {
358 min: 0.0,
359 max: 10.0,
360 };
361 let s = d.sample(0.5);
362 assert!((s - 5.0).abs() < 0.001);
363 }
364
365 #[test]
366 fn distribution_normal_sample() {
367 let d = Distribution::Normal {
368 mean: 0.0,
369 std_dev: 1.0,
370 };
371 let s = d.sample(0.5);
372 assert!(s.is_finite());
374 }
375
376 #[test]
377 fn distribution_exponential_sample() {
378 let d = Distribution::Exponential { lambda: 1.0 };
379 let s = d.sample(0.5);
380 assert!(s > 0.0);
381 }
382
383 #[test]
384 fn distribution_triangular_sample() {
385 let d = Distribution::Triangular {
386 min: 0.0,
387 mode: 0.5,
388 max: 1.0,
389 };
390 let s = d.sample(0.5);
391 assert!((0.0..=1.0).contains(&s));
392 }
393
394 #[test]
395 fn distribution_constant_sample() {
396 let d = Distribution::Constant(42.0);
397 assert!((d.sample(0.5) - 42.0).abs() < 0.001);
398 }
399
400 #[test]
401 fn distribution_mean() {
402 assert!(
403 (Distribution::Uniform {
404 min: 0.0,
405 max: 10.0
406 }
407 .mean()
408 - 5.0)
409 .abs()
410 < 0.001
411 );
412 assert!(
413 (Distribution::Normal {
414 mean: 3.0,
415 std_dev: 1.0
416 }
417 .mean()
418 - 3.0)
419 .abs()
420 < 0.001
421 );
422 assert!((Distribution::Constant(42.0).mean() - 42.0).abs() < 0.001);
423 }
424
425 #[test]
426 fn distribution_name() {
427 assert_eq!(
428 Distribution::Uniform { min: 0.0, max: 1.0 }.name(),
429 "uniform"
430 );
431 assert_eq!(
432 Distribution::Normal {
433 mean: 0.0,
434 std_dev: 1.0
435 }
436 .name(),
437 "normal"
438 );
439 assert_eq!(Distribution::Constant(0.0).name(), "constant");
440 }
441
442 #[test]
443 fn mc_simulate_constant_model() {
444 let mut sim = MonteCarloSimulator::new(McConfig {
445 n_samples: 1000,
446 seed: 42,
447 quasi_mc: false,
448 });
449 let dists = vec![Distribution::Uniform { min: 0.0, max: 1.0 }];
450 let result = sim.simulate(&dists, |_| 42.0);
451 assert!((result.mean - 42.0).abs() < 0.001);
452 assert!((result.std_dev - 0.0).abs() < 0.001);
453 }
454
455 #[test]
456 fn mc_simulate_identity_model() {
457 let mut sim = MonteCarloSimulator::new(McConfig {
458 n_samples: 10_000,
459 seed: 42,
460 quasi_mc: false,
461 });
462 let dists = vec![Distribution::Uniform {
463 min: 0.0,
464 max: 10.0,
465 }];
466 let result = sim.simulate(&dists, |inputs| inputs[0]);
467 assert!((result.mean - 5.0).abs() < 0.5);
469 }
470
471 #[test]
472 fn mc_simulate_sum_model() {
473 let mut sim = MonteCarloSimulator::new(McConfig {
474 n_samples: 10_000,
475 seed: 42,
476 quasi_mc: false,
477 });
478 let dists = vec![
479 Distribution::Uniform { min: 0.0, max: 1.0 },
480 Distribution::Uniform { min: 0.0, max: 1.0 },
481 ];
482 let result = sim.simulate(&dists, |inputs| inputs[0] + inputs[1]);
483 assert!((result.mean - 1.0).abs() < 0.2);
485 }
486
487 #[test]
488 fn mc_quasi_mc_mode() {
489 let mut sim = MonteCarloSimulator::new(McConfig {
490 n_samples: 1000,
491 seed: 42,
492 quasi_mc: true,
493 });
494 let dists = vec![Distribution::Uniform { min: 0.0, max: 1.0 }];
495 let result = sim.simulate(&dists, |inputs| inputs[0]);
496 assert!((result.mean - 0.5).abs() < 0.1);
498 }
499
500 #[test]
501 fn mc_result_percentiles() {
502 let samples: Vec<f64> = (0..100).map(f64::from).collect();
503 let result = MonteCarloSimulator::compute_stats(&samples);
504 assert!((result.mean - 49.5).abs() < 0.001);
505 assert!((result.min - 0.0).abs() < 0.001);
506 assert!((result.max - 99.0).abs() < 0.001);
507 assert!((result.p50 - 49.0).abs() <= 2.0);
508 }
509
510 #[test]
511 fn mc_result_ci95() {
512 let samples: Vec<f64> = vec![1.0; 100];
513 let result = MonteCarloSimulator::compute_stats(&samples);
514 assert!((result.ci95_half_width() - 0.0).abs() < 0.001);
515 }
516
517 #[test]
518 fn mc_result_to_json() {
519 let result = McResult {
520 mean: 5.0,
521 std_dev: 1.0,
522 min: 0.0,
523 max: 10.0,
524 p5: 1.0,
525 p25: 3.0,
526 p50: 5.0,
527 p75: 7.0,
528 p95: 9.0,
529 n_samples: 100,
530 };
531 let json = result.to_json();
532 assert_eq!(json["mean"], 5.0);
533 assert_eq!(json["n_samples"], 100);
534 }
535
536 #[test]
537 fn mc_integrate_constant() {
538 let mut sim = MonteCarloSimulator::new(McConfig {
539 n_samples: 10_000,
540 seed: 42,
541 quasi_mc: false,
542 });
543 let result = sim.integrate(&[(0.0, 2.0)], |_| 1.0);
545 assert!((result.mean - 2.0).abs() < 0.2);
546 }
547
548 #[test]
549 fn mc_integrate_identity() {
550 let mut sim = MonteCarloSimulator::new(McConfig {
551 n_samples: 10_000,
552 seed: 42,
553 quasi_mc: false,
554 });
555 let result = sim.integrate(&[(0.0, 1.0)], |inputs| inputs[0]);
557 assert!((result.mean - 0.5).abs() < 0.1);
558 }
559
560 #[test]
561 fn mc_compute_stats_empty() {
562 let result = MonteCarloSimulator::compute_stats(&[]);
563 assert_eq!(result.n_samples, 0);
564 assert!((result.mean - 0.0).abs() < 0.001);
565 }
566
567 #[test]
568 fn mc_simulate_with_normal() {
569 let mut sim = MonteCarloSimulator::new(McConfig {
570 n_samples: 50_000,
571 seed: 42,
572 quasi_mc: false,
573 });
574 let dists = vec![Distribution::Normal {
575 mean: 10.0,
576 std_dev: 2.0,
577 }];
578 let result = sim.simulate(&dists, |inputs| inputs[0]);
579 assert!((result.mean - 10.0).abs() < 1.0);
581 assert!((result.std_dev - 2.0).abs() < 1.0);
583 }
584
585 #[test]
586 fn mc_config_default() {
587 let config = McConfig::default();
588 assert_eq!(config.n_samples, 10_000);
589 assert_eq!(config.seed, 42);
590 assert!(!config.quasi_mc);
591 }
592}