1use crate::simulatable::Simulatable;
2use crate::types::{SimulationConfig, SimulationResult};
3use rand::rngs::StdRng;
4use rand::{Rng, SeedableRng};
5use rand_distr::StandardNormal;
6use rayon::prelude::*;
7
8pub struct SimulationRunner<S: Simulatable> {
15 model: S,
16 config: SimulationConfig,
17}
18
19impl<S: Simulatable> SimulationRunner<S> {
20 pub fn new(model: S, config: SimulationConfig) -> Self {
21 assert!(config.n_paths > 0, "n_paths must be positive");
22 assert!(config.n_steps > 0, "n_steps must be positive");
23 assert!(config.dt > 0.0, "dt must be positive");
24 Self { model, config }
25 }
26
27 pub fn run(&self) -> SimulationResult<S::Output> {
29 let n = self.config.n_paths;
30 let steps = self.config.n_steps;
31 let dt = self.config.dt;
32 let dim = self.model.dim();
33
34 let total_randoms = n * steps * dim;
36 let mut randoms = Vec::with_capacity(total_randoms);
37 let mut rng = StdRng::seed_from_u64(self.config.seed);
38 for _ in 0..total_randoms {
39 randoms.push(rng.sample::<f64, _>(StandardNormal) * dt.sqrt());
40 }
41
42 let outputs: Vec<S::Output> = (0..n)
44 .into_par_iter()
45 .flat_map(|path_idx| {
46 let mut model = self.model.clone();
47 let mut path_rng = StdRng::seed_from_u64(self.config.seed + path_idx as u64 + 1);
48 let initial = model.current();
49 let mut path_outputs = Vec::with_capacity(steps);
50 path_outputs.push(initial);
51 for step in 0..steps {
52 let offset = path_idx * steps * dim + step * dim;
53 let dw = &randoms[offset..offset + dim];
54 let out = model.step(dt, dw, &mut path_rng);
55 path_outputs.push(out);
56 }
57 path_outputs
58 })
59 .collect();
60
61 SimulationResult {
62 outputs,
63 n_paths: n,
64 n_steps: steps,
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::simulatable::Simulatable;
73
74 #[derive(Clone)]
76 struct CountingModel {
77 dim: usize,
78 count: f64,
79 }
80
81 impl Simulatable for CountingModel {
82 type Output = f64;
83
84 fn dim(&self) -> usize {
85 self.dim
86 }
87
88 fn step(&mut self, _dt: f64, _dw: &[f64], _rng: &mut impl Rng) -> f64 {
89 self.count += 1.0;
90 self.count
91 }
92
93 fn current(&self) -> f64 {
94 self.count
95 }
96 }
97
98 #[test]
99 fn test_runner_output_shape() {
100 let model = CountingModel { dim: 1, count: 0.0 };
101 let config = SimulationConfig {
102 n_paths: 10,
103 n_steps: 5,
104 dt: 1.0 / 252.0,
105 seed: 42,
106 };
107 let runner = SimulationRunner::new(model, config);
108 let result = runner.run();
109 assert_eq!(result.n_paths, 10);
110 assert_eq!(result.n_steps, 5);
111 assert_eq!(result.outputs.len(), 10 * 6);
113 }
114
115 #[test]
116 #[should_panic(expected = "n_paths must be positive")]
117 fn test_runner_rejects_zero_paths() {
118 let model = CountingModel { dim: 1, count: 0.0 };
119 let config = SimulationConfig {
120 n_paths: 0,
121 n_steps: 100,
122 dt: 1.0 / 252.0,
123 seed: 42,
124 };
125 SimulationRunner::new(model, config);
126 }
127
128 #[test]
129 fn test_result_indexing() {
130 let model = CountingModel { dim: 1, count: 0.0 };
131 let config = SimulationConfig {
132 n_paths: 3,
133 n_steps: 2,
134 dt: 0.1,
135 seed: 7,
136 };
137 let runner = SimulationRunner::new(model, config);
138 let result = runner.run();
139
140 assert_eq!(result.outputs.len(), 9);
142
143 assert!(result.get(0, 0).is_some());
144 assert!(result.get(2, 1).is_some());
145 assert!(result.get(3, 0).is_none());
146 assert!(result.get(0, 3).is_none());
148
149 let path0 = result.path(0).unwrap();
150 assert_eq!(path0.len(), 3);
151 assert!(result.path(3).is_none());
152 }
153}