Skip to main content

rustyqlib/utils/
RNG.rs

1//! Normal draw generation for Monte Carlo pricing.
2//!
3//! Two samplers:
4//! - **Seeded pseudo-random** (PCG64): reproducible across runs, with
5//!   antithetic pairing and moment matching as variance reduction.
6//! - **Low-discrepancy** (1-D Sobol, i.e. the van der Corput base-2
7//!   sequence) mapped through the inverse normal CDF — near-O(1/n)
8//!   convergence for terminal-value (single-dimension) simulation.
9//!
10//! Multi-dimensional (path-wise) draws use the seeded pseudo-random
11//! generator; proper multi-dimensional Sobol with a Brownian bridge is
12//! future work.
13
14use rand::{Rng, SeedableRng};
15use rand_distr::StandardNormal;
16use rand_pcg::Pcg64;
17
18use crate::core::utils::inv_N;
19
20/// Radical inverse in base 2 (van der Corput sequence) — the 1-D Sobol
21/// sequence. `i >= 1`; returns a value in (0, 1).
22fn van_der_corput_base2(mut i: u64) -> f64 {
23    let mut f = 0.5;
24    let mut x = 0.0;
25    while i > 0 {
26        if i & 1 == 1 {
27            x += f;
28        }
29        i >>= 1;
30        f *= 0.5;
31    }
32    x
33}
34
35/// `n` low-discrepancy standard normal draws (1-D Sobol through the
36/// inverse normal CDF). Deterministic.
37pub fn sobol_normals(n: usize) -> Vec<f64> {
38    (1..=n as u64).map(|i| inv_N(van_der_corput_base2(i))).collect()
39}
40
41/// `n` seeded pseudo-random standard normals with antithetic pairing and
42/// moment matching (mean 0, variance 1 exactly). Deterministic per seed.
43pub fn pseudo_normals(n: usize, seed: u64) -> Vec<f64> {
44    let mut rng = Pcg64::seed_from_u64(seed);
45    let mut draws = Vec::with_capacity(n + 1);
46    while draws.len() < n {
47        let z: f64 = rng.sample(StandardNormal);
48        draws.push(z);
49        draws.push(-z);
50    }
51    draws.truncate(n);
52    moment_match(&mut draws);
53    draws
54}
55
56/// `paths x steps` matrix of seeded pseudo-random standard normals for
57/// path-wise simulation. Deterministic per seed.
58pub fn pseudo_normal_matrix(paths: usize, steps: usize, seed: u64) -> Vec<Vec<f64>> {
59    let mut rng = Pcg64::seed_from_u64(seed);
60    (0..paths)
61        .map(|_| (0..steps).map(|_| rng.sample(StandardNormal)).collect())
62        .collect()
63}
64
65/// SplitMix64 finalizer — used to derive statistically independent
66/// per-path RNG streams from (seed, path index).
67fn splitmix64(mut x: u64) -> u64 {
68    x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
69    let mut z = x;
70    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
71    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
72    z ^ (z >> 31)
73}
74
75/// Deterministic, independent RNG stream for one simulation path — the
76/// basis of parallel path generation (each path seeds its own generator,
77/// so results are identical regardless of thread scheduling).
78pub fn path_rng(seed: u64, path_index: u64) -> Pcg64 {
79    Pcg64::seed_from_u64(splitmix64(seed ^ splitmix64(path_index)))
80}
81
82/// Standard normal draws for one path from its own stream.
83pub fn path_normals(seed: u64, path_index: u64, out: &mut [f64]) {
84    let mut rng = path_rng(seed, path_index);
85    for z in out.iter_mut() {
86        *z = rng.sample(StandardNormal);
87    }
88}
89
90fn first_primes(n: usize) -> Vec<u64> {
91    let mut primes: Vec<u64> = Vec::with_capacity(n);
92    let mut candidate = 2u64;
93    while primes.len() < n {
94        if primes.iter().take_while(|&&p| p * p <= candidate).all(|&p| candidate % p != 0) {
95            primes.push(candidate);
96        }
97        candidate += 1;
98    }
99    primes
100}
101
102fn radical_inverse(mut i: u64, base: u64) -> f64 {
103    let inv_base = 1.0 / base as f64;
104    let mut f = inv_base;
105    let mut x = 0.0;
106    while i > 0 {
107        x += f * (i % base) as f64;
108        i /= base;
109        f *= inv_base;
110    }
111    x
112}
113
114/// Multi-dimensional low-discrepancy sequence: Halton with prime bases and
115/// a deterministic Cranley-Patterson rotation per dimension (derived from
116/// the seed), mapped to standard normals through the inverse CDF.
117///
118/// Combined with [`BrownianBridge`] ordering, the well-distributed leading
119/// dimensions carry the coarse structure of each path. Direction-number
120/// Sobol (Joe-Kuo) is a drop-in upgrade behind this same interface.
121pub struct QmcSequence {
122    bases: Vec<u64>,
123    shifts: Vec<f64>,
124}
125
126impl QmcSequence {
127    pub fn new(dims: usize, seed: u64) -> Self {
128        let bases = first_primes(dims);
129        let shifts = (0..dims)
130            .map(|d| (splitmix64(seed ^ splitmix64(0xC0FFEE ^ d as u64)) >> 11) as f64
131                / (1u64 << 53) as f64)
132            .collect();
133        QmcSequence { bases, shifts }
134    }
135
136    /// Fill `out` with the standard normals of point `index` (1-based).
137    pub fn normals(&self, index: u64, out: &mut [f64]) {
138        for (d, z) in out.iter_mut().enumerate() {
139            let mut u = radical_inverse(index, self.bases[d]) + self.shifts[d];
140            if u >= 1.0 {
141                u -= 1.0;
142            }
143            *z = crate::core::utils::inv_N(u.clamp(1e-15, 1.0 - 1e-15));
144        }
145    }
146}
147
148/// Brownian bridge path construction: the first draw fixes the terminal
149/// value, subsequent draws fill midpoints by bisection, so low-discrepancy
150/// coordinates are spent on the dimensions that matter most. Weights are
151/// precomputed once per pricing call and shared across paths.
152///
153/// Produces per-step Brownian increments; a future multi-factor model
154/// (e.g. Heston) uses one bridge per factor.
155pub struct BrownianBridge {
156    steps: usize,
157    sqrt_t: f64,
158    /// (mid, left, right, weight_left, weight_right, stddev); left == usize::MAX
159    /// encodes the origin (t = 0, W = 0)
160    plan: Vec<(usize, usize, usize, f64, f64, f64)>,
161}
162
163impl BrownianBridge {
164    pub fn new(steps: usize, dt: f64) -> Self {
165        assert!(steps >= 1);
166        let t_at = |i: usize| {
167            if i == usize::MAX { 0.0 } else { (i + 1) as f64 * dt }
168        };
169        let mut plan = Vec::with_capacity(steps.saturating_sub(1));
170        let mut queue = std::collections::VecDeque::new();
171        queue.push_back((usize::MAX, steps - 1));
172        while let Some((l, r)) = queue.pop_front() {
173            let lo = if l == usize::MAX { 0 } else { l + 1 };
174            if r <= lo {
175                continue;
176            }
177            let mid = (lo + r) / 2;
178            let (tl, tm, tr) = (t_at(l), t_at(mid), t_at(r));
179            let wl = (tr - tm) / (tr - tl);
180            let wr = (tm - tl) / (tr - tl);
181            let sd = ((tm - tl) * (tr - tm) / (tr - tl)).sqrt();
182            plan.push((mid, l, r, wl, wr, sd));
183            queue.push_back((l, mid));
184            queue.push_back((mid, r));
185        }
186        BrownianBridge { steps, sqrt_t: (steps as f64 * dt).sqrt(), plan }
187    }
188
189    /// Consume `steps` standard normals, produce `steps` Brownian increments.
190    pub fn increments(&self, z: &[f64], w_buf: &mut [f64], out: &mut [f64]) {
191        assert!(z.len() == self.steps && w_buf.len() == self.steps && out.len() == self.steps);
192        w_buf[self.steps - 1] = self.sqrt_t * z[0];
193        for (k, &(mid, l, r, wl, wr, sd)) in self.plan.iter().enumerate() {
194            let w_l = if l == usize::MAX { 0.0 } else { w_buf[l] };
195            w_buf[mid] = wl * w_l + wr * w_buf[r] + sd * z[k + 1];
196        }
197        let mut prev = 0.0;
198        for i in 0..self.steps {
199            out[i] = w_buf[i] - prev;
200            prev = w_buf[i];
201        }
202    }
203}
204
205fn moment_match(draws: &mut [f64]) {
206    let n = draws.len() as f64;
207    let mean = draws.iter().sum::<f64>() / n;
208    let var = draws.iter().map(|z| (z - mean) * (z - mean)).sum::<f64>() / n;
209    let std = var.sqrt();
210    if std > 0.0 {
211        for z in draws.iter_mut() {
212            *z = (*z - mean) / std;
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn van_der_corput_first_values() {
223        // 1/2, 1/4, 3/4, 1/8, 5/8, ...
224        let expected = [0.5, 0.25, 0.75, 0.125, 0.625];
225        for (i, want) in expected.iter().enumerate() {
226            assert_eq!(van_der_corput_base2(i as u64 + 1), *want);
227        }
228    }
229
230    #[test]
231    fn pseudo_normals_are_reproducible_and_standardized() {
232        let a = pseudo_normals(10_000, 42);
233        let b = pseudo_normals(10_000, 42);
234        assert_eq!(a, b);
235        let mean: f64 = a.iter().sum::<f64>() / a.len() as f64;
236        let var: f64 = a.iter().map(|z| z * z).sum::<f64>() / a.len() as f64;
237        assert!(mean.abs() < 1e-12);
238        assert!((var - 1.0).abs() < 1e-12);
239    }
240
241    #[test]
242    fn path_streams_are_deterministic_and_distinct() {
243        let mut a = [0.0; 8];
244        let mut b = [0.0; 8];
245        let mut a2 = [0.0; 8];
246        path_normals(42, 0, &mut a);
247        path_normals(42, 1, &mut b);
248        path_normals(42, 0, &mut a2);
249        assert_eq!(a, a2);
250        assert_ne!(a, b);
251    }
252
253    #[test]
254    fn brownian_bridge_reproduces_marginal_variance() {
255        // increments must sum to W_T = sqrt(T) z_0 and have the right
256        // per-step variance under iid normals
257        let steps = 13;
258        let dt = 1.0 / steps as f64;
259        let bridge = BrownianBridge::new(steps, dt);
260        let mut w = vec![0.0; steps];
261        let mut inc = vec![0.0; steps];
262        let mut sum_sq = vec![0.0; steps];
263        let n = 20_000;
264        for path in 0..n {
265            let mut z = vec![0.0; steps];
266            path_normals(7, path, &mut z);
267            bridge.increments(&z, &mut w, &mut inc);
268            let total: f64 = inc.iter().sum();
269            assert!((total - z[0] * (1.0_f64).sqrt()).abs() < 1e-12);
270            for (i, d) in inc.iter().enumerate() {
271                sum_sq[i] += d * d;
272            }
273        }
274        for (i, s) in sum_sq.iter().enumerate() {
275            let var = s / n as f64;
276            assert!((var - dt).abs() < 0.02 * dt.max(0.001), "step {i}: var {var} vs {dt}");
277        }
278    }
279
280    #[test]
281    fn qmc_sequence_normals_are_standardized() {
282        let steps = 32;
283        let qmc = QmcSequence::new(steps, 42);
284        let mut z = vec![0.0; steps];
285        let n = 8192;
286        let mut mean = vec![0.0; steps];
287        let mut var = vec![0.0; steps];
288        for i in 1..=n {
289            qmc.normals(i as u64, &mut z);
290            for d in 0..steps {
291                mean[d] += z[d];
292                var[d] += z[d] * z[d];
293            }
294        }
295        for d in 0..steps {
296            let m = mean[d] / n as f64;
297            let v = var[d] / n as f64;
298            assert!(m.abs() < 0.05, "dim {d}: mean {m}");
299            assert!((v - 1.0).abs() < 0.1, "dim {d}: var {v}");
300        }
301    }
302
303    #[test]
304    fn sobol_normals_have_near_perfect_moments() {
305        let draws = sobol_normals(65_536);
306        let mean: f64 = draws.iter().sum::<f64>() / draws.len() as f64;
307        let var: f64 = draws.iter().map(|z| z * z).sum::<f64>() / draws.len() as f64;
308        assert!(mean.abs() < 1e-3, "mean {mean}");
309        assert!((var - 1.0).abs() < 1e-2, "var {var}");
310    }
311}