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