1use std::str::FromStr;
20
21use rayon::prelude::*;
22
23use super::brownian_bridge::BrownianBridge;
24use super::halton::QmcSequence;
25use super::process::{DiscretizationScheme, StochasticProcess, StochasticProcess1D};
26use super::rng::path_normals;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Sampler {
35 Sobol,
36 PseudoRandom,
37}
38
39impl FromStr for Sampler {
40 type Err = String;
41 fn from_str(s: &str) -> Result<Self, Self::Err> {
42 match s.trim().to_lowercase().as_str() {
43 "sobol" | "quasi" => Ok(Sampler::Sobol),
44 "pseudo" | "pseudorandom" | "pseudo_random" => Ok(Sampler::PseudoRandom),
45 other => Err(format!("Invalid sampler '{other}'")),
46 }
47 }
48}
49
50pub enum PathDraws {
55 Pseudo { seed: u64, sqrt_dt: f64 },
56 Qmc { seq: QmcSequence, bridge: BrownianBridge },
57}
58
59impl PathDraws {
60 pub fn new(sampler: Sampler, seed: u64, steps: usize, dt: f64) -> Self {
61 match sampler {
62 Sampler::Sobol => PathDraws::Qmc {
63 seq: QmcSequence::new(steps, seed),
64 bridge: BrownianBridge::new(steps, dt),
65 },
66 Sampler::PseudoRandom => PathDraws::Pseudo { seed, sqrt_dt: dt.sqrt() },
67 }
68 }
69
70 pub fn pseudo(seed: u64, dt: f64) -> Self {
71 PathDraws::Pseudo { seed, sqrt_dt: dt.sqrt() }
72 }
73
74 pub fn fill(&self, index: usize, z: &mut [f64], w: &mut [f64], dw: &mut [f64]) {
77 match self {
78 PathDraws::Pseudo { seed, sqrt_dt } => {
79 path_normals(*seed, (index / 2) as u64, z);
80 let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
81 for (d, zi) in dw.iter_mut().zip(z.iter()) {
82 *d = sign * sqrt_dt * zi;
83 }
84 }
85 PathDraws::Qmc { seq, bridge } => {
86 seq.normals(index as u64 + 1, z);
87 bridge.increments(z, w, dw);
88 }
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy)]
95pub struct SampleConfig {
96 pub paths: usize,
98 pub steps: usize,
100 pub horizon: f64,
102 pub sampler: Sampler,
103 pub seed: u64,
104}
105
106pub struct Paths {
109 steps: usize,
110 data: Vec<f64>,
111}
112
113impl Paths {
114 pub fn n_paths(&self) -> usize {
115 self.data.len() / self.steps
116 }
117
118 pub fn steps(&self) -> usize {
119 self.steps
120 }
121
122 pub fn path(&self, i: usize) -> &[f64] {
124 &self.data[i * self.steps..(i + 1) * self.steps]
125 }
126
127 pub fn iter(&self) -> impl Iterator<Item = &[f64]> {
128 self.data.chunks(self.steps)
129 }
130}
131
132pub struct MultiPaths {
135 steps: usize,
136 dim: usize,
137 data: Vec<f64>,
138}
139
140impl MultiPaths {
141 pub fn n_paths(&self) -> usize {
142 self.data.len() / (self.steps * self.dim)
143 }
144
145 pub fn steps(&self) -> usize {
146 self.steps
147 }
148
149 pub fn dim(&self) -> usize {
150 self.dim
151 }
152
153 pub fn path(&self, i: usize) -> &[f64] {
155 let stride = self.steps * self.dim;
156 &self.data[i * stride..(i + 1) * stride]
157 }
158
159 pub fn state(&self, i: usize, j: usize) -> &[f64] {
161 let at = (i * self.steps + j) * self.dim;
162 &self.data[at..at + self.dim]
163 }
164}
165
166pub fn sample_paths_1d<P: StochasticProcess1D>(
169 process: &P,
170 x0: f64,
171 scheme: DiscretizationScheme,
172 cfg: &SampleConfig,
173) -> Paths {
174 let steps = cfg.steps.max(1);
175 let dt = cfg.horizon / steps as f64;
176 let draws = PathDraws::new(cfg.sampler, cfg.seed, steps, dt);
177 let mut data = vec![0.0; cfg.paths * steps];
178 data.par_chunks_mut(steps).enumerate().for_each_init(
179 || (vec![0.0; steps], vec![0.0; steps], vec![0.0; steps]),
180 |(z, w, dw), (i, out)| {
181 draws.fill(i, z, w, dw);
182 let mut x = x0;
183 for (j, d) in dw.iter().enumerate() {
184 x = process.evolve(scheme, j as f64 * dt, x, dt, *d);
185 out[j] = x;
186 }
187 },
188 );
189 Paths { steps, data }
190}
191
192pub enum MultiDraws {
197 Pseudo { seed: u64, sqrt_dt: f64 },
198 Qmc { seq: QmcSequence, bridge: BrownianBridge },
202}
203
204impl MultiDraws {
205 pub fn new(sampler: Sampler, seed: u64, factors: usize, steps: usize, dt: f64) -> Self {
206 match sampler {
207 Sampler::Sobol => MultiDraws::Qmc {
208 seq: QmcSequence::new(factors * steps, seed),
209 bridge: BrownianBridge::new(steps, dt),
210 },
211 Sampler::PseudoRandom => MultiDraws::Pseudo { seed, sqrt_dt: dt.sqrt() },
212 }
213 }
214
215 pub fn fill(
216 &self,
217 index: usize,
218 factors: usize,
219 steps: usize,
220 scratch: &mut FactorScratch,
221 dw: &mut [f64],
222 ) {
223 match self {
224 MultiDraws::Pseudo { seed, sqrt_dt } => {
225 path_normals(*seed, (index / 2) as u64, &mut scratch.z);
226 let sign = if index % 2 == 0 { 1.0 } else { -1.0 };
227 for (d, zi) in dw.iter_mut().zip(scratch.z.iter()) {
228 *d = sign * sqrt_dt * zi;
229 }
230 }
231 MultiDraws::Qmc { seq, bridge } => {
232 seq.normals(index as u64 + 1, &mut scratch.z);
233 for f in 0..factors {
234 bridge.increments(
235 &scratch.z[f * steps..(f + 1) * steps],
236 &mut scratch.w,
237 &mut scratch.dwf,
238 );
239 for (j, d) in scratch.dwf.iter().enumerate() {
240 dw[j * factors + f] = *d;
241 }
242 }
243 }
244 }
245 }
246}
247
248pub struct FactorScratch {
250 z: Vec<f64>,
251 w: Vec<f64>,
252 dwf: Vec<f64>,
253}
254
255impl FactorScratch {
256 pub fn new(factors: usize, steps: usize) -> Self {
257 FactorScratch {
258 z: vec![0.0; factors * steps],
259 w: vec![0.0; steps],
260 dwf: vec![0.0; steps],
261 }
262 }
263}
264
265pub fn sample_paths<P: StochasticProcess>(process: &P, x0: &[f64], cfg: &SampleConfig) -> MultiPaths {
271 let (dim, factors) = (process.dim(), process.factors());
272 assert_eq!(x0.len(), dim, "initial state must have process.dim() entries");
273 let steps = cfg.steps.max(1);
274 let dt = cfg.horizon / steps as f64;
275 let draws = MultiDraws::new(cfg.sampler, cfg.seed, factors, steps, dt);
276 let mut data = vec![0.0; cfg.paths * steps * dim];
277 data.par_chunks_mut(steps * dim).enumerate().for_each_init(
278 || {
279 (
280 FactorScratch::new(factors, steps),
281 vec![0.0; factors * steps], vec![0.0; dim], vec![0.0; dim], )
285 },
286 |(scratch, dw, x, x_next), (i, out)| {
287 draws.fill(i, factors, steps, scratch, dw);
288 x.copy_from_slice(x0);
289 for j in 0..steps {
290 process.evolve(j as f64 * dt, x, dt, &dw[j * factors..(j + 1) * factors], x_next);
291 x.copy_from_slice(x_next);
292 out[j * dim..(j + 1) * dim].copy_from_slice(x);
293 }
294 },
295 );
296 MultiPaths { steps, dim, data }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 struct Gbm {
304 mu: f64,
305 sigma: f64,
306 }
307
308 impl StochasticProcess1D for Gbm {
309 fn drift(&self, _t: f64, x: f64) -> f64 {
310 self.mu * x
311 }
312 fn diffusion(&self, _t: f64, x: f64) -> f64 {
313 self.sigma * x
314 }
315 fn exact_step(&self, _t: f64, x: f64, dt: f64, dw: f64) -> Option<f64> {
316 Some(x * ((self.mu - 0.5 * self.sigma * self.sigma) * dt + self.sigma * dw).exp())
317 }
318 }
319
320 fn cfg(sampler: Sampler) -> SampleConfig {
321 SampleConfig { paths: 20_000, steps: 12, horizon: 1.0, sampler, seed: 7 }
322 }
323
324 #[test]
325 fn terminal_moments_match_the_lognormal_law() {
326 let p = Gbm { mu: 0.05, sigma: 0.2 };
327 for sampler in [Sampler::Sobol, Sampler::PseudoRandom] {
328 let paths = sample_paths_1d(&p, 100.0, DiscretizationScheme::Exact, &cfg(sampler));
329 let n = paths.n_paths() as f64;
330 let mean: f64 = paths.iter().map(|path| path[path.len() - 1]).sum::<f64>() / n;
331 let log_var: f64 = paths
332 .iter()
333 .map(|path| {
334 let l = (path[path.len() - 1] / 100.0).ln();
335 (l - (0.05 - 0.02)) * (l - (0.05 - 0.02))
336 })
337 .sum::<f64>()
338 / n;
339 let target = 100.0 * (0.05_f64).exp();
340 assert!((mean - target).abs() / target < 0.01, "{sampler:?}: mean {mean}");
341 assert!((log_var - 0.04).abs() / 0.04 < 0.05, "{sampler:?}: log-var {log_var}");
342 }
343 }
344
345 #[test]
346 fn pseudo_paths_come_in_antithetic_pairs() {
347 let p = Gbm { mu: 0.05, sigma: 0.2 };
350 let paths = sample_paths_1d(&p, 100.0, DiscretizationScheme::Exact, &cfg(Sampler::PseudoRandom));
351 let dt = 1.0 / 12.0;
352 for j in 0..paths.steps() {
353 let drift = (0.05 - 0.02) * dt * (j + 1) as f64;
354 let sum_logs = (paths.path(0)[j] / 100.0).ln() + (paths.path(1)[j] / 100.0).ln();
355 assert!((sum_logs - 2.0 * drift).abs() < 1e-12, "step {j}");
356 }
357 }
358
359 #[test]
360 fn same_seed_reproduces_and_different_seed_differs() {
361 let p = Gbm { mu: 0.02, sigma: 0.3 };
362 let a = sample_paths_1d(&p, 50.0, DiscretizationScheme::Exact, &cfg(Sampler::Sobol));
363 let b = sample_paths_1d(&p, 50.0, DiscretizationScheme::Exact, &cfg(Sampler::Sobol));
364 assert_eq!(a.path(123), b.path(123));
365 let other = SampleConfig { seed: 8, ..cfg(Sampler::Sobol) };
366 let c = sample_paths_1d(&p, 50.0, DiscretizationScheme::Exact, &other);
367 assert_ne!(a.path(123), c.path(123));
368 }
369
370 struct TwoGbm;
372
373 impl StochasticProcess for TwoGbm {
374 fn dim(&self) -> usize {
375 2
376 }
377 fn factors(&self) -> usize {
378 2
379 }
380 fn drift(&self, _t: f64, x: &[f64], out: &mut [f64]) {
381 out[0] = 0.05 * x[0];
382 out[1] = 0.01 * x[1];
383 }
384 fn diffusion(&self, _t: f64, x: &[f64], out: &mut [f64]) {
385 out.copy_from_slice(&[0.2 * x[0], 0.0, 0.0, 0.3 * x[1]]);
386 }
387 }
388
389 #[test]
390 fn multi_state_terminal_means_track_their_drifts() {
391 for sampler in [Sampler::Sobol, Sampler::PseudoRandom] {
392 let cfg = SampleConfig { paths: 40_000, steps: 50, horizon: 1.0, sampler, seed: 3 };
393 let paths = sample_paths(&TwoGbm, &[100.0, 200.0], &cfg);
394 assert_eq!((paths.n_paths(), paths.steps(), paths.dim()), (40_000, 50, 2));
395 let n = paths.n_paths() as f64;
396 let (mut m0, mut m1) = (0.0, 0.0);
397 for i in 0..paths.n_paths() {
398 let last = paths.state(i, paths.steps() - 1);
399 m0 += last[0];
400 m1 += last[1];
401 }
402 let (t0, t1) = (100.0 * (0.05_f64).exp(), 200.0 * (0.01_f64).exp());
403 assert!((m0 / n - t0).abs() / t0 < 0.01, "{sampler:?}: {m0}");
405 assert!((m1 / n - t1).abs() / t1 < 0.01, "{sampler:?}: {m1}");
406 }
407 }
408}