Skip to main content

copula_core/sampling/
mod.rs

1//! Advanced sampling methods for copulas.
2//!
3//! This module provides various sampling algorithms that can be useful
4//! for copula simulation and inference:
5//! - Rejection sampling
6//! - Importance sampling
7//! - Latin hypercube sampling
8//! - Quasi-random sequences (Sobol, Halton)
9//!
10//! ## Example
11//! ```
12//! use copula_core::sampling::latin_hypercube;
13//!
14//! let mut rng = rand::rng();
15//! let samples = latin_hypercube(100, 2, &mut rng);
16//! assert_eq!(samples.nrows(), 100);
17//! assert_eq!(samples.ncols(), 2);
18//! ```
19
20use nalgebra::DMatrix;
21use rand::{Rng, RngExt};
22
23/// Generate Latin hypercube samples.
24///
25/// Latin hypercube sampling is a stratified sampling method that ensures
26/// better coverage of the parameter space than pure random sampling.
27///
28/// # Arguments
29/// * `n` - Number of samples
30/// * `d` - Dimension
31/// * `rng` - Random number generator
32///
33/// # Returns
34/// Matrix of shape (n, d) with samples in [0, 1]^d
35pub fn latin_hypercube<R: Rng + ?Sized>(n: usize, d: usize, rng: &mut R) -> DMatrix<f64> {
36    assert!(n > 0 && d > 0, "latin_hypercube requires n > 0 and d > 0");
37    let mut samples = DMatrix::<f64>::zeros(n, d);
38
39    for j in 0..d {
40        // Create permutation of 0..n
41        let mut perm: Vec<usize> = (0..n).collect();
42        // Shuffle the permutation
43        for i in (1..n).rev() {
44            let swap_idx = rng.random_range(0..=i);
45            perm.swap(i, swap_idx);
46        }
47
48        // Fill column with stratified samples
49        for i in 0..n {
50            let strata_start = perm[i] as f64 / n as f64;
51            let strata_end = (perm[i] + 1) as f64 / n as f64;
52            samples[(i, j)] = strata_start + (strata_end - strata_start) * rng.random::<f64>();
53        }
54    }
55
56    samples
57}
58
59/// Rejection sampling from a target distribution.
60///
61/// # Arguments
62/// * `target` - Target density function (unnormalized OK)
63/// * `proposal` - Proposal distribution sampler
64/// * `proposal_density` - Proposal density function
65/// * `m` - Constant such that target(x) <= M * proposal_density(x) for all x
66/// * `n` - Number of samples desired
67/// * `rng` - Random number generator
68///
69/// # Returns
70/// Vector of accepted samples
71pub fn rejection_sampling<R, P, T, D>(
72    target: T,
73    mut proposal: P,
74    proposal_density: D,
75    m: f64,
76    n: usize,
77    rng: &mut R,
78) -> Vec<f64>
79where
80    R: Rng + ?Sized,
81    P: FnMut(&mut R) -> f64,
82    T: Fn(f64) -> f64,
83    D: Fn(f64) -> f64,
84{
85    let mut accepted = Vec::with_capacity(n);
86
87    while accepted.len() < n {
88        let x = proposal(rng);
89        let u = rng.random::<f64>();
90        let acceptance_prob = target(x) / (m * proposal_density(x));
91
92        if u < acceptance_prob {
93            accepted.push(x);
94        }
95    }
96
97    accepted
98}
99
100/// Halton sequence generator for low-discrepancy sampling.
101///
102/// The Halton sequence is a deterministic low-discrepancy sequence
103/// useful for quasi-Monte Carlo methods.
104pub struct HaltonSequence {
105    dimension: usize,
106    bases: Vec<u32>,
107    index: u64,
108}
109
110impl HaltonSequence {
111    /// Create a new Halton sequence generator.
112    ///
113    /// # Arguments
114    /// * `dimension` - Number of dimensions
115    ///
116    /// # Returns
117    /// A new Halton sequence generator
118    pub fn new(dimension: usize) -> Self {
119        assert!(
120            dimension > 0 && dimension <= 16,
121            "HaltonSequence dimension must be between 1 and 16"
122        );
123        // Use first d primes as bases
124        let primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53];
125        let bases: Vec<u32> = primes.iter().take(dimension).copied().collect();
126
127        Self {
128            dimension,
129            bases,
130            index: 0,
131        }
132    }
133
134    /// Generate n points from the sequence.
135    ///
136    /// # Arguments
137    /// * `n` - Number of points to generate
138    ///
139    /// # Returns
140    /// Matrix of shape (n, dimension)
141    pub fn generate(&mut self, n: usize) -> DMatrix<f64> {
142        let mut samples = DMatrix::<f64>::zeros(n, self.dimension);
143
144        for (i, point) in self.by_ref().take(n).enumerate() {
145            for (j, value) in point.into_iter().enumerate() {
146                samples[(i, j)] = value;
147            }
148        }
149
150        samples
151    }
152}
153
154/// An unbounded iterator over successive points of the sequence.
155///
156/// Each point is a vector of length `dimension` with values in [0, 1).
157impl Iterator for HaltonSequence {
158    type Item = Vec<f64>;
159
160    fn next(&mut self) -> Option<Self::Item> {
161        let point = self
162            .bases
163            .iter()
164            .map(|&base| van_der_corput(self.index, base))
165            .collect();
166        self.index += 1;
167        Some(point)
168    }
169}
170
171/// Van der Corput sequence in a given base.
172fn van_der_corput(mut n: u64, base: u32) -> f64 {
173    let mut vdc = 0.0;
174    let mut denom = 1.0;
175
176    while n > 0 {
177        denom *= base as f64;
178        let remainder = n % base as u64;
179        n /= base as u64;
180        vdc += remainder as f64 / denom;
181    }
182
183    vdc
184}
185
186/// Sobol sequence generator (simplified 1D version).
187///
188/// For production use, consider using the sobol crate for full implementation.
189pub fn sobol_1d(n: usize) -> Vec<f64> {
190    let mut sequence = Vec::with_capacity(n);
191    for i in 0..n {
192        sequence.push(van_der_corput(i as u64, 2));
193    }
194    sequence
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_latin_hypercube() {
203        let mut rng = rand::rng();
204        let samples = latin_hypercube(50, 3, &mut rng);
205
206        // Check dimensions
207        assert_eq!(samples.nrows(), 50);
208        assert_eq!(samples.ncols(), 3);
209
210        // Check all values in [0, 1]
211        for i in 0..50 {
212            for j in 0..3 {
213                assert!(samples[(i, j)] >= 0.0 && samples[(i, j)] <= 1.0);
214            }
215        }
216    }
217
218    #[test]
219    fn test_halton_sequence() {
220        let mut halton = HaltonSequence::new(2);
221        let samples = halton.generate(10);
222
223        assert_eq!(samples.nrows(), 10);
224        assert_eq!(samples.ncols(), 2);
225
226        // Check all values in [0, 1]
227        for i in 0..10 {
228            for j in 0..2 {
229                assert!(samples[(i, j)] >= 0.0 && samples[(i, j)] <= 1.0);
230            }
231        }
232    }
233
234    #[test]
235    fn test_van_der_corput() {
236        // First few values of van der Corput sequence in base 2
237        let expected = [0.0, 0.5, 0.25, 0.75, 0.125];
238        for (i, &exp) in expected.iter().enumerate() {
239            let val = van_der_corput(i as u64, 2);
240            assert!((val - exp).abs() < 1e-10);
241        }
242    }
243
244    #[test]
245    fn test_rejection_sampling() {
246        let mut rng = rand::rng();
247
248        // Sample from a truncated normal using uniform proposal
249        let target = |x: f64| {
250            if (0.0..=1.0).contains(&x) {
251                (-x * x / 2.0).exp()
252            } else {
253                0.0
254            }
255        };
256        let proposal = |rng: &mut rand::rngs::ThreadRng| rng.random::<f64>();
257        let proposal_density = |_x: f64| 1.0;
258        let m = 1.5; // M such that target(x) <= M * proposal_density(x)
259
260        let samples = rejection_sampling(target, proposal, proposal_density, m, 100, &mut rng);
261
262        assert_eq!(samples.len(), 100);
263        for &s in &samples {
264            assert!((0.0..=1.0).contains(&s));
265        }
266    }
267
268    #[test]
269    fn test_latin_hypercube_stratification() {
270        let mut rng = rand::rng();
271        let n = 100;
272        let samples = latin_hypercube(n, 1, &mut rng);
273
274        // Each stratum [i/n, (i+1)/n] should have exactly one sample
275        let mut counts = vec![0; n];
276        for i in 0..n {
277            let stratum = (samples[(i, 0)] * n as f64).floor() as usize;
278            let stratum = stratum.min(n - 1);
279            counts[stratum] += 1;
280        }
281        for (i, &c) in counts.iter().enumerate() {
282            assert_eq!(c, 1, "stratum {} has {} samples, expected 1", i, c);
283        }
284    }
285
286    #[test]
287    fn test_latin_hypercube_single_sample() {
288        let mut rng = rand::rng();
289        let samples = latin_hypercube(1, 2, &mut rng);
290        assert_eq!(samples.nrows(), 1);
291        assert_eq!(samples.ncols(), 2);
292        assert!(samples[(0, 0)] >= 0.0 && samples[(0, 0)] <= 1.0);
293        assert!(samples[(0, 1)] >= 0.0 && samples[(0, 1)] <= 1.0);
294    }
295
296    #[test]
297    fn test_halton_iterator_matches_generate() {
298        let from_iter: Vec<Vec<f64>> = HaltonSequence::new(2).take(5).collect();
299        let generated = HaltonSequence::new(2).generate(5);
300        for (i, point) in from_iter.iter().enumerate() {
301            assert_eq!(point.len(), 2);
302            for (j, &value) in point.iter().enumerate() {
303                assert_eq!(value, generated[(i, j)]);
304            }
305        }
306    }
307
308    #[test]
309    fn test_halton_sequence_deterministic() {
310        let mut h1 = HaltonSequence::new(1);
311        let mut h2 = HaltonSequence::new(1);
312        let s1 = h1.generate(10);
313        let s2 = h2.generate(10);
314        for i in 0..10 {
315            assert!((s1[(i, 0)] - s2[(i, 0)]).abs() < 1e-15);
316        }
317    }
318
319    #[test]
320    fn test_halton_sequence_coverage() {
321        let mut halton = HaltonSequence::new(2);
322        let samples = halton.generate(100);
323        // Low-discrepancy sequences should cover [0,1]^2 well
324        let mut has_low = false;
325        let mut has_high = false;
326        for i in 0..100 {
327            if samples[(i, 0)] < 0.1 {
328                has_low = true;
329            }
330            if samples[(i, 0)] > 0.9 {
331                has_high = true;
332            }
333        }
334        assert!(has_low, "Halton sequence missing low values");
335        assert!(has_high, "Halton sequence missing high values");
336    }
337
338    #[test]
339    fn test_sobol_1d() {
340        let seq = sobol_1d(8);
341        assert_eq!(seq.len(), 8);
342        // First value should be 0.0 (van der Corput of 0)
343        assert!((seq[0] - 0.0).abs() < 1e-15);
344        // Second should be 0.5
345        assert!((seq[1] - 0.5).abs() < 1e-15);
346        for &v in &seq {
347            assert!((0.0..=1.0).contains(&v));
348        }
349    }
350
351    #[test]
352    fn test_van_der_corput_base3() {
353        // Base 3: 0, 1/3, 2/3, 1/9, 4/9, ...
354        let expected = [0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0 / 9.0];
355        for (i, &exp) in expected.iter().enumerate() {
356            let val = van_der_corput(i as u64, 3);
357            assert!(
358                (val - exp).abs() < 1e-10,
359                "vdc({}, 3) = {} expected {}",
360                i,
361                val,
362                exp
363            );
364        }
365    }
366}