rustyqlib/core/montecarlo/
halton.rs1use crate::core::utils::inv_norm_cdf;
7
8use super::rng::splitmix64;
9
10fn first_primes(n: usize) -> Vec<u64> {
11 let mut primes: Vec<u64> = Vec::with_capacity(n);
12 let mut candidate = 2u64;
13 while primes.len() < n {
14 if primes.iter().take_while(|&&p| p * p <= candidate).all(|&p| candidate % p != 0) {
15 primes.push(candidate);
16 }
17 candidate += 1;
18 }
19 primes
20}
21
22fn radical_inverse(mut i: u64, base: u64) -> f64 {
23 let inv_base = 1.0 / base as f64;
24 let mut f = inv_base;
25 let mut x = 0.0;
26 while i > 0 {
27 x += f * (i % base) as f64;
28 i /= base;
29 f *= inv_base;
30 }
31 x
32}
33
34pub struct QmcSequence {
42 bases: Vec<u64>,
43 shifts: Vec<f64>,
44}
45
46impl QmcSequence {
47 pub fn new(dims: usize, seed: u64) -> Self {
48 let bases = first_primes(dims);
49 let shifts = (0..dims)
50 .map(|d| (splitmix64(seed ^ splitmix64(0xC0FFEE ^ d as u64)) >> 11) as f64
51 / (1u64 << 53) as f64)
52 .collect();
53 QmcSequence { bases, shifts }
54 }
55
56 pub fn normals(&self, index: u64, out: &mut [f64]) {
58 for (d, z) in out.iter_mut().enumerate() {
59 let mut u = radical_inverse(index, self.bases[d]) + self.shifts[d];
60 if u >= 1.0 {
61 u -= 1.0;
62 }
63 *z = inv_norm_cdf(u.clamp(1e-15, 1.0 - 1e-15));
64 }
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn qmc_sequence_normals_are_standardized() {
74 let steps = 32;
75 let qmc = QmcSequence::new(steps, 42);
76 let mut z = vec![0.0; steps];
77 let n = 8192;
78 let mut mean = vec![0.0; steps];
79 let mut var = vec![0.0; steps];
80 for i in 1..=n {
81 qmc.normals(i as u64, &mut z);
82 for d in 0..steps {
83 mean[d] += z[d];
84 var[d] += z[d] * z[d];
85 }
86 }
87 for d in 0..steps {
88 let m = mean[d] / n as f64;
89 let v = var[d] / n as f64;
90 assert!(m.abs() < 0.05, "dim {d}: mean {m}");
91 assert!((v - 1.0).abs() < 0.1, "dim {d}: var {v}");
92 }
93 }
94}