Skip to main content

oxiphysics_core/random/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::f64::consts::PI;
6
7use super::types::Xoshiro256;
8
9/// Box-Muller transform: return two independent N(0,1) values.
10pub fn box_muller(rng: &mut Xoshiro256) -> (f64, f64) {
11    let u1 = rng.next_f64().max(1e-300);
12    let u2 = rng.next_f64();
13    let r = (-2.0 * u1.ln()).sqrt();
14    let theta = 2.0 * PI * u2;
15    (r * theta.cos(), r * theta.sin())
16}
17/// Sample from Gamma(shape, scale) using Marsaglia-Tsang method.
18pub fn sample_gamma(rng: &mut Xoshiro256, shape: f64, scale: f64) -> f64 {
19    if shape < 1.0 {
20        let u = rng.next_f64().max(1e-300);
21        return sample_gamma(rng, shape + 1.0, scale) * u.powf(1.0 / shape);
22    }
23    let d = shape - 1.0 / 3.0;
24    let c = 1.0 / (9.0 * d).sqrt();
25    loop {
26        let (z, _) = box_muller(rng);
27        let v_raw = 1.0 + c * z;
28        if v_raw <= 0.0 {
29            continue;
30        }
31        let v = v_raw.powi(3);
32        let u = rng.next_f64().max(1e-300);
33        if u < 1.0 - 0.0331 * (z * z) * (z * z) {
34            return d * v * scale;
35        }
36        if u.ln() < 0.5 * z * z + d * (1.0 - v + v.ln()) {
37            return d * v * scale;
38        }
39    }
40}
41/// Choose an index from a probability vector given uniform variate `u`.
42pub fn weighted_choice_index(probs: &[f64], u: f64) -> usize {
43    let mut cumsum = 0.0;
44    for (i, &p) in probs.iter().enumerate() {
45        cumsum += p;
46        if u < cumsum {
47            return i;
48        }
49    }
50    probs.len() - 1
51}
52/// Compute the 1D Halton sequence value at index `i` with base `base`.
53pub fn halton_1d(mut i: usize, base: usize) -> f64 {
54    let mut f = 1.0f64;
55    let mut r = 0.0f64;
56    while i > 0 {
57        f /= base as f64;
58        r += f * (i % base) as f64;
59        i /= base;
60    }
61    r
62}
63/// Return the first `n` prime numbers.
64pub fn first_n_primes(n: usize) -> Vec<usize> {
65    let mut primes = Vec::with_capacity(n);
66    let mut candidate = 2usize;
67    while primes.len() < n {
68        if primes.iter().all(|&p| !candidate.is_multiple_of(p)) {
69            primes.push(candidate);
70        }
71        candidate += 1;
72    }
73    primes
74}
75/// Fisher-Yates shuffle of a mutable slice in-place.
76pub fn shuffle_fisher_yates<T>(slice: &mut [T], rng: &mut Xoshiro256) {
77    let n = slice.len();
78    for i in (1..n).rev() {
79        let j = (rng.next_u64() as usize) % (i + 1);
80        slice.swap(i, j);
81    }
82}
83/// Sample `k` distinct indices from `0..n` without replacement.
84pub fn sample_without_replacement(rng: &mut Xoshiro256, n: usize, k: usize) -> Vec<usize> {
85    let k = k.min(n);
86    let mut pool: Vec<usize> = (0..n).collect();
87    shuffle_fisher_yates(&mut pool, rng);
88    pool.truncate(k);
89    pool
90}
91/// Weighted random choice from a slice of `(value, weight)` pairs.
92pub fn weighted_choice<T: Clone>(rng: &mut Xoshiro256, choices: &[(T, f64)]) -> Option<T> {
93    if choices.is_empty() {
94        return None;
95    }
96    let total: f64 = choices.iter().map(|(_, w)| w).sum();
97    let u = rng.next_f64() * total;
98    let mut cumsum = 0.0;
99    for (val, w) in choices {
100        cumsum += w;
101        if u <= cumsum {
102            return Some(val.clone());
103        }
104    }
105    choices.last().map(|c| c.0.clone())
106}
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    use crate::random::DirichletDist;
112
113    use crate::random::ExponentialDist;
114
115    use crate::random::MultinomialDist;
116    use crate::random::NormalDist;
117
118    use crate::random::Pcg64;
119    use crate::random::PoissonDist;
120    use crate::random::RandomSampler;
121
122    use crate::random::SplitMix64;
123
124    use crate::random::UniformDist;
125
126    use crate::random::Xoshiro256;
127
128    #[test]
129    fn splitmix64_deterministic() {
130        let mut a = SplitMix64::new(42);
131        let mut b = SplitMix64::new(42);
132        assert_eq!(a.next_u64(), b.next_u64());
133    }
134    #[test]
135    fn splitmix64_different_seeds() {
136        let mut a = SplitMix64::new(1);
137        let mut b = SplitMix64::new(2);
138        assert_ne!(a.next_u64(), b.next_u64());
139    }
140    #[test]
141    fn splitmix64_f64_range() {
142        let mut rng = SplitMix64::new(123);
143        for _ in 0..1000 {
144            let v = rng.next_f64();
145            assert!((0.0..1.0).contains(&v));
146        }
147    }
148    #[test]
149    fn pcg64_deterministic() {
150        let mut a = Pcg64::new(99, 1);
151        let mut b = Pcg64::new(99, 1);
152        for _ in 0..10 {
153            assert_eq!(a.next_u64(), b.next_u64());
154        }
155    }
156    #[test]
157    fn pcg64_f64_range() {
158        let mut rng = Pcg64::new(42, 1);
159        for _ in 0..1000 {
160            let v = rng.next_f64();
161            assert!((0.0..1.0).contains(&v), "v={v}");
162        }
163    }
164    #[test]
165    fn pcg64_range() {
166        let mut rng = Pcg64::new(7, 3);
167        for _ in 0..1000 {
168            let v = rng.next_f64_range(2.0, 5.0);
169            assert!((2.0..5.0).contains(&v), "v={v}");
170        }
171    }
172    #[test]
173    fn pcg64_different_streams() {
174        let mut a = Pcg64::new(0, 0);
175        let mut b = Pcg64::new(0, 1);
176        let va: Vec<u64> = (0..10).map(|_| a.next_u64()).collect();
177        let vb: Vec<u64> = (0..10).map(|_| b.next_u64()).collect();
178        assert_ne!(va, vb);
179    }
180    #[test]
181    fn xoshiro256_deterministic() {
182        let mut a = Xoshiro256::new(55);
183        let mut b = Xoshiro256::new(55);
184        for _ in 0..20 {
185            assert_eq!(a.next_u64(), b.next_u64());
186        }
187    }
188    #[test]
189    fn xoshiro256_f64_range() {
190        let mut rng = Xoshiro256::new(1);
191        for _ in 0..1000 {
192            let v = rng.next_f64();
193            assert!((0.0..1.0).contains(&v));
194        }
195    }
196    #[test]
197    fn xoshiro256_jump_changes_state() {
198        let mut a = Xoshiro256::new(10);
199        let mut b = Xoshiro256::new(10);
200        b.jump();
201        assert_ne!(a.next_u64(), b.next_u64());
202    }
203    #[test]
204    fn xoshiro256_long_jump_changes_state() {
205        let mut a = Xoshiro256::new(10);
206        let mut b = Xoshiro256::new(10);
207        b.long_jump();
208        assert_ne!(a.next_u64(), b.next_u64());
209    }
210    #[test]
211    fn uniform_dist_range() {
212        let dist = UniformDist::new(-3.0, 7.0);
213        let mut rng = Xoshiro256::new(1);
214        for _ in 0..1000 {
215            let v = dist.sample(&mut rng);
216            assert!((-3.0..7.0).contains(&v), "v={v}");
217        }
218    }
219    #[test]
220    fn uniform_dist_sample_n_count() {
221        let dist = UniformDist::new(0.0, 1.0);
222        let mut rng = Xoshiro256::new(2);
223        let samples = dist.sample_n(&mut rng, 50);
224        assert_eq!(samples.len(), 50);
225    }
226    #[test]
227    fn normal_dist_mean_approx() {
228        let mut dist = NormalDist::new(5.0, 1.0);
229        let mut rng = Xoshiro256::new(3);
230        let samples = dist.sample_n(&mut rng, 10000);
231        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
232        assert!((mean - 5.0).abs() < 0.1, "mean={mean}");
233    }
234    #[test]
235    fn normal_dist_std_approx() {
236        let mut dist = NormalDist::new(0.0, 2.0);
237        let mut rng = Xoshiro256::new(4);
238        let samples = dist.sample_n(&mut rng, 10000);
239        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
240        let var = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
241        assert!((var.sqrt() - 2.0).abs() < 0.1, "std={}", var.sqrt());
242    }
243    #[test]
244    fn poisson_small_lambda_mean() {
245        let dist = PoissonDist::new(3.0);
246        let mut rng = Xoshiro256::new(5);
247        let samples: Vec<u64> = (0..10000).map(|_| dist.sample(&mut rng)).collect();
248        let mean = samples.iter().sum::<u64>() as f64 / samples.len() as f64;
249        assert!((mean - 3.0).abs() < 0.2, "mean={mean}");
250    }
251    #[test]
252    fn poisson_large_lambda_mean() {
253        let dist = PoissonDist::new(100.0);
254        let mut rng = Xoshiro256::new(6);
255        let samples: Vec<u64> = (0..5000).map(|_| dist.sample(&mut rng)).collect();
256        let mean = samples.iter().sum::<u64>() as f64 / samples.len() as f64;
257        assert!((mean - 100.0).abs() < 2.0, "mean={mean}");
258    }
259    #[test]
260    fn exponential_dist_mean() {
261        let dist = ExponentialDist::new(2.0);
262        let mut rng = Xoshiro256::new(7);
263        let samples = dist.sample_n(&mut rng, 10000);
264        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
265        assert!((mean - 0.5).abs() < 0.05, "mean={mean}");
266    }
267    #[test]
268    fn exponential_dist_all_positive() {
269        let dist = ExponentialDist::new(1.0);
270        let mut rng = Xoshiro256::new(8);
271        let samples = dist.sample_n(&mut rng, 1000);
272        assert!(samples.iter().all(|&v| v > 0.0));
273    }
274    #[test]
275    fn dirichlet_sums_to_one() {
276        let dist = DirichletDist::new(vec![1.0, 2.0, 3.0]);
277        let mut rng = Xoshiro256::new(9);
278        for _ in 0..100 {
279            let s = dist.sample(&mut rng);
280            let sum: f64 = s.iter().sum();
281            assert!((sum - 1.0).abs() < 1e-10, "sum={sum}");
282        }
283    }
284    #[test]
285    fn dirichlet_all_positive() {
286        let dist = DirichletDist::new(vec![0.5, 0.5, 0.5]);
287        let mut rng = Xoshiro256::new(10);
288        for _ in 0..100 {
289            let s = dist.sample(&mut rng);
290            assert!(s.iter().all(|&v| v > 0.0));
291        }
292    }
293    #[test]
294    fn dirichlet_uniform_mean() {
295        let dist = DirichletDist::new(vec![1.0, 1.0, 1.0]);
296        let mut rng = Xoshiro256::new(11);
297        let n = 5000usize;
298        let samples = dist.sample_n(&mut rng, n);
299        let mean0 = samples.iter().map(|s| s[0]).sum::<f64>() / n as f64;
300        assert!((mean0 - 1.0 / 3.0).abs() < 0.02, "mean0={mean0}");
301    }
302    #[test]
303    fn multinomial_counts_sum_to_n() {
304        let dist = MultinomialDist::new(vec![0.3, 0.4, 0.3]);
305        let mut rng = Xoshiro256::new(12);
306        let counts = dist.sample(&mut rng, 100);
307        assert_eq!(counts.iter().sum::<usize>(), 100);
308    }
309    #[test]
310    fn multinomial_distribution_approx() {
311        let probs = vec![0.5, 0.3, 0.2];
312        let dist = MultinomialDist::new(probs.clone());
313        let mut rng = Xoshiro256::new(13);
314        let n = 10000usize;
315        let counts = dist.sample(&mut rng, n);
316        let freq: Vec<f64> = counts.iter().map(|&c| c as f64 / n as f64).collect();
317        for (f, p) in freq.iter().zip(probs.iter()) {
318            assert!((f - p).abs() < 0.02, "freq={f}, prob={p}");
319        }
320    }
321    #[test]
322    fn stratified_sampling_covers_interval() {
323        let mut rng = Xoshiro256::new(14);
324        let samples = RandomSampler::stratified_1d(&mut rng, 10);
325        assert_eq!(samples.len(), 10);
326        for (i, &s) in samples.iter().enumerate() {
327            let lo = i as f64 / 10.0;
328            let hi = (i + 1) as f64 / 10.0;
329            assert!(s >= lo && s < hi, "sample[{i}]={s} outside [{lo},{hi})");
330        }
331    }
332    #[test]
333    fn latin_hypercube_dimensions() {
334        let mut rng = Xoshiro256::new(15);
335        let samples = RandomSampler::latin_hypercube(&mut rng, 20, 3);
336        assert_eq!(samples.len(), 20);
337        assert_eq!(samples[0].len(), 3);
338    }
339    #[test]
340    fn latin_hypercube_range() {
341        let mut rng = Xoshiro256::new(16);
342        let samples = RandomSampler::latin_hypercube(&mut rng, 10, 2);
343        for row in &samples {
344            for &v in row {
345                assert!((0.0..1.0).contains(&v), "v={v}");
346            }
347        }
348    }
349    #[test]
350    fn halton_sequence_dimensions() {
351        let samples = RandomSampler::halton(100, 3, 0);
352        assert_eq!(samples.len(), 100);
353        assert_eq!(samples[0].len(), 3);
354    }
355    #[test]
356    fn halton_1d_base2_known_values() {
357        assert!((halton_1d(1, 2) - 0.5).abs() < 1e-10);
358        assert!((halton_1d(2, 2) - 0.25).abs() < 1e-10);
359        assert!((halton_1d(3, 2) - 0.75).abs() < 1e-10);
360    }
361    #[test]
362    fn sobol_dimensions() {
363        let samples = RandomSampler::sobol(50, 4);
364        assert_eq!(samples.len(), 50);
365        assert_eq!(samples[0].len(), 4);
366    }
367    #[test]
368    fn sobol_range() {
369        let samples = RandomSampler::sobol(100, 2);
370        for row in &samples {
371            for &v in row {
372                assert!((0.0..=1.0).contains(&v), "v={v}");
373            }
374        }
375    }
376    #[test]
377    fn fisher_yates_shuffle_is_permutation() {
378        let mut rng = Xoshiro256::new(20);
379        let mut v: Vec<usize> = (0..20).collect();
380        let original = v.clone();
381        shuffle_fisher_yates(&mut v, &mut rng);
382        let mut sorted = v.clone();
383        sorted.sort_unstable();
384        assert_eq!(sorted, original);
385    }
386    #[test]
387    fn sample_without_replacement_count() {
388        let mut rng = Xoshiro256::new(21);
389        let s = sample_without_replacement(&mut rng, 100, 10);
390        assert_eq!(s.len(), 10);
391    }
392    #[test]
393    fn sample_without_replacement_unique() {
394        let mut rng = Xoshiro256::new(22);
395        let mut s = sample_without_replacement(&mut rng, 50, 20);
396        s.sort_unstable();
397        s.dedup();
398        assert_eq!(s.len(), 20);
399    }
400    #[test]
401    fn sample_without_replacement_k_gt_n() {
402        let mut rng = Xoshiro256::new(23);
403        let s = sample_without_replacement(&mut rng, 5, 10);
404        assert_eq!(s.len(), 5);
405    }
406    #[test]
407    fn weighted_choice_selects_valid() {
408        let mut rng = Xoshiro256::new(24);
409        let choices = vec![("a", 1.0), ("b", 2.0), ("c", 3.0)];
410        for _ in 0..100 {
411            let v = weighted_choice(&mut rng, &choices).unwrap();
412            assert!(matches!(v, "a" | "b" | "c"));
413        }
414    }
415    #[test]
416    fn weighted_choice_empty_none() {
417        let mut rng = Xoshiro256::new(25);
418        let choices: Vec<(i32, f64)> = vec![];
419        assert!(weighted_choice(&mut rng, &choices).is_none());
420    }
421    #[test]
422    fn box_muller_produces_two_values() {
423        let mut rng = Xoshiro256::new(26);
424        let (z0, z1) = box_muller(&mut rng);
425        assert!(z0.is_finite());
426        assert!(z1.is_finite());
427    }
428    #[test]
429    fn first_n_primes_correct() {
430        let p = first_n_primes(5);
431        assert_eq!(p, vec![2, 3, 5, 7, 11]);
432    }
433    #[test]
434    fn gamma_sample_positive() {
435        let mut rng = Xoshiro256::new(27);
436        for _ in 0..100 {
437            let v = sample_gamma(&mut rng, 2.0, 1.0);
438            assert!(v > 0.0, "gamma sample should be positive");
439        }
440    }
441    #[test]
442    fn poisson_sample_n_count() {
443        let dist = PoissonDist::new(5.0);
444        let mut rng = Xoshiro256::new(28);
445        let s = dist.sample_n(&mut rng, 30);
446        assert_eq!(s.len(), 30);
447    }
448    #[test]
449    fn weighted_choice_index_boundary() {
450        let probs = vec![0.5, 0.3, 0.2];
451        assert_eq!(weighted_choice_index(&probs, 0.0), 0);
452        assert_eq!(weighted_choice_index(&probs, 0.6), 1);
453        assert_eq!(weighted_choice_index(&probs, 0.99), 2);
454    }
455}
456/// Euler–Mascheroni constant γ ≈ 0.5772156649.
457pub const EULER_MASCHERONI: f64 = 0.577_215_664_901_532_9;
458/// Sample from a truncated normal distribution N(mu, sigma²) on \[a, b\]
459/// using rejection sampling.
460pub fn sample_truncated_normal(rng: &mut Xoshiro256, mu: f64, sigma: f64, a: f64, b: f64) -> f64 {
461    loop {
462        let u1 = rng.next_f64().max(1e-300);
463        let u2 = rng.next_f64();
464        let r = (-2.0 * u1.ln()).sqrt();
465        let theta = 2.0 * PI * u2;
466        let x = mu + sigma * r * theta.cos();
467        if x >= a && x <= b {
468            return x;
469        }
470    }
471}
472/// Compute scrambled Halton value at index i with base and permutation.
473pub fn scrambled_halton_1d(mut i: usize, base: usize, perm: &[usize]) -> f64 {
474    let mut f = 1.0f64;
475    let mut r = 0.0f64;
476    while i > 0 {
477        f /= base as f64;
478        let digit = i % base;
479        let scrambled_digit = if digit < perm.len() {
480            perm[digit]
481        } else {
482            digit
483        };
484        r += f * scrambled_digit as f64;
485        i /= base;
486    }
487    r
488}
489/// Log-gamma function using Lanczos approximation.
490///
491/// Accurate to ~10 decimal places for Re(z) > 0.
492pub fn log_gamma(z: f64) -> f64 {
493    if z <= 0.0 {
494        return f64::INFINITY;
495    }
496    if z < 0.5 {
497        return PI.ln() - (PI * z).sin().ln() - log_gamma(1.0 - z);
498    }
499    pub(super) const G: f64 = 7.0;
500    pub(super) const C: [f64; 9] = [
501        0.999_999_999_999_809_9,
502        676.5203681218851,
503        -1259.1392167224028,
504        771.323_428_777_653_1,
505        -176.615_029_162_140_6,
506        12.507343278686905,
507        -0.13857109526572012,
508        9.984_369_578_019_572e-6,
509        1.5056327351493116e-7,
510    ];
511    let zz = z - 1.0;
512    let mut x = C[0];
513    for (i, &c) in C[1..].iter().enumerate() {
514        x += c / (zz + (i + 1) as f64);
515    }
516    let t = zz + G + 0.5;
517    0.5 * (2.0 * PI).ln() + (zz + 0.5) * t.ln() - t + x.ln()
518}
519/// Gamma function Γ(z) = exp(log_gamma(z)).
520pub fn gamma_fn(z: f64) -> f64 {
521    log_gamma(z).exp()
522}
523/// Standard normal CDF Φ(x) using rational approximation.
524///
525/// Accuracy: |error| < 7.5e-8 for all x (Abramowitz & Stegun 26.2.17).
526pub fn normal_cdf(x: f64) -> f64 {
527    let t = 1.0 / (1.0 + 0.2316419 * x.abs());
528    let poly = t
529        * (0.319381530
530            + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
531    let p = 1.0 - ((-0.5 * x * x).exp() / (2.0 * PI).sqrt()) * poly;
532    if x >= 0.0 { p } else { 1.0 - p }
533}
534/// Inverse normal CDF (probit function) via rational approximation.
535///
536/// Reference: Peter Acklam's approximation (max error ~1.15e-9).
537pub fn normal_ppf(p: f64) -> f64 {
538    let p = p.clamp(1e-15, 1.0 - 1e-15);
539    pub(super) const A: [f64; 6] = [
540        -3.969683028665376e+01,
541        2.209460984245205e+02,
542        -2.759285104469687e+02,
543        1.383_577_518_672_69e2,
544        -3.066479806614716e+01,
545        2.506628277459239,
546    ];
547    pub(super) const B: [f64; 5] = [
548        -5.447609879822406e+01,
549        1.615858368580409e+02,
550        -1.556989798598866e+02,
551        6.680131188771972e+01,
552        -1.328068155288572e+01,
553    ];
554    pub(super) const C: [f64; 6] = [
555        -7.784894002430293e-03,
556        -3.223964580411365e-01,
557        -2.400758277161838,
558        -2.549732539343734,
559        4.374664141464968,
560        2.938163982698783,
561    ];
562    pub(super) const D: [f64; 4] = [
563        7.784695709041462e-03,
564        3.224671290700398e-01,
565        2.445134137142996,
566        3.754408661907416,
567    ];
568    pub(super) const P_LOW: f64 = 0.02425;
569    pub(super) const P_HIGH: f64 = 1.0 - P_LOW;
570    if p < P_LOW {
571        let q = (-2.0 * p.ln()).sqrt();
572        (C[5] + q * (C[4] + q * (C[3] + q * (C[2] + q * (C[1] + q * C[0])))))
573            / (1.0 + q * (D[3] + q * (D[2] + q * (D[1] + q * D[0]))))
574    } else if p <= P_HIGH {
575        let q = p - 0.5;
576        let r = q * q;
577        (q * (A[5] + r * (A[4] + r * (A[3] + r * (A[2] + r * (A[1] + r * A[0]))))))
578            / (1.0 + r * (B[4] + r * (B[3] + r * (B[2] + r * (B[1] + r * B[0])))))
579    } else {
580        let q = (-(2.0 * (1.0 - p).ln())).sqrt();
581        -(C[5] + q * (C[4] + q * (C[3] + q * (C[2] + q * (C[1] + q * C[0])))))
582            / (1.0 + q * (D[3] + q * (D[2] + q * (D[1] + q * D[0]))))
583    }
584}
585/// Approximate Student-t CDF using numerical integration.
586///
587/// Uses Simpson's rule for moderate ν; accurate to ~1e-6.
588pub fn student_t_cdf(x: f64, nu: f64) -> f64 {
589    if nu > 100.0 {
590        return normal_cdf(x);
591    }
592    let t_sq = x * x;
593    let z = nu / (nu + t_sq);
594    let ib = regularized_incomplete_beta(nu / 2.0, 0.5, z);
595    if x >= 0.0 { 1.0 - 0.5 * ib } else { 0.5 * ib }
596}
597/// Regularized incomplete beta function I_x(a, b).
598///
599/// Uses a continued fraction expansion (Lentz's method).
600pub fn regularized_incomplete_beta(a: f64, b: f64, x: f64) -> f64 {
601    if x <= 0.0 {
602        return 0.0;
603    }
604    if x >= 1.0 {
605        return 1.0;
606    }
607    if x > (a + 1.0) / (a + b + 2.0) {
608        return 1.0 - regularized_incomplete_beta(b, a, 1.0 - x);
609    }
610    let ln_beta = log_gamma(a) + log_gamma(b) - log_gamma(a + b);
611    let front = (a * x.ln() + b * (1.0 - x).ln() - ln_beta - a.ln()).exp();
612    let mut cf = 1.0f64;
613    for m in (1..=20).rev() {
614        let mf = m as f64;
615        let d1 = mf * (b - mf) * x / ((a + 2.0 * mf - 1.0) * (a + 2.0 * mf));
616        let d2 = -(a + mf) * (a + b + mf) * x / ((a + 2.0 * mf) * (a + 2.0 * mf + 1.0));
617        cf = 1.0 + d1 / (1.0 + d2 / cf);
618    }
619    front / (a * cf.max(1e-300))
620}
621#[cfg(test)]
622mod extended_tests {
623
624    use crate::bayesian_inference::log_gamma;
625    use crate::normal_cdf;
626    use crate::random::AntitheticVariates;
627    use crate::random::BetaDist;
628    use crate::random::BrownianBridge;
629    use crate::random::BrownianMotion;
630    use crate::random::ChiSquaredDist;
631    use crate::random::ClaytonCopula;
632
633    use crate::random::EULER_MASCHERONI;
634
635    use crate::random::FDist;
636    use crate::random::FractionalBrownianMotion;
637    use crate::random::FrankCopula;
638    use crate::random::FrechetDist;
639    use crate::random::GEVDist;
640    use crate::random::GOEMatrix;
641    use crate::random::GUEMatrix;
642    use crate::random::GammaDist;
643    use crate::random::GaussianCopula;
644    use crate::random::GumbelCopula;
645    use crate::random::GumbelDist;
646    use crate::random::LevyProcess;
647
648    use crate::random::OrnsteinUhlenbeck;
649
650    use crate::random::RejectionSampler;
651    use crate::random::ScrambledHalton;
652    use crate::random::SobolSequence;
653
654    use crate::random::StratifiedMonteCarlo;
655    use crate::random::StudentTDist;
656    use crate::random::TCopula;
657
658    use crate::random::WeibullDist;
659    use crate::random::WishartMatrix;
660    use crate::random::Xoshiro256;
661    use crate::random::ZigguratNormal;
662    use crate::random::functions::PI;
663    use crate::random::normal_ppf;
664    use crate::random::sample_truncated_normal;
665    #[test]
666    fn gamma_dist_positive_samples() {
667        let dist = GammaDist::new(2.0, 1.0);
668        let mut rng = Xoshiro256::new(100);
669        for _ in 0..200 {
670            assert!(dist.sample(&mut rng) > 0.0);
671        }
672    }
673    #[test]
674    fn gamma_dist_mean_approx() {
675        let dist = GammaDist::new(3.0, 2.0);
676        let mut rng = Xoshiro256::new(101);
677        let samples = dist.sample_n(&mut rng, 10000);
678        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
679        assert!((mean - 6.0).abs() < 0.3, "mean={mean}");
680    }
681    #[test]
682    fn gamma_dist_variance_approx() {
683        let alpha = 4.0;
684        let beta = 2.0;
685        let dist = GammaDist::new(alpha, beta);
686        let mut rng = Xoshiro256::new(102);
687        let samples = dist.sample_n(&mut rng, 10000);
688        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
689        let var = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
690        let expected_var = alpha * beta * beta;
691        assert!(
692            (var - expected_var).abs() / expected_var < 0.1,
693            "var={var}, expected={expected_var}"
694        );
695    }
696    #[test]
697    fn beta_dist_in_range() {
698        let dist = BetaDist::new(2.0, 3.0);
699        let mut rng = Xoshiro256::new(110);
700        for _ in 0..200 {
701            let v = dist.sample(&mut rng);
702            assert!(v > 0.0 && v < 1.0, "v={v}");
703        }
704    }
705    #[test]
706    fn beta_dist_mean_approx() {
707        let dist = BetaDist::new(2.0, 3.0);
708        let mut rng = Xoshiro256::new(111);
709        let samples = dist.sample_n(&mut rng, 10000);
710        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
711        let expected = 2.0 / 5.0;
712        assert!((mean - expected).abs() < 0.02, "mean={mean}");
713    }
714    #[test]
715    fn beta_log_pdf_finite() {
716        let dist = BetaDist::new(2.0, 2.0);
717        let lp = dist.log_pdf(0.5);
718        assert!(lp.is_finite(), "log_pdf={lp}");
719    }
720    #[test]
721    fn student_t_mean_approx() {
722        let dist = StudentTDist::new(10.0);
723        let mut rng = Xoshiro256::new(120);
724        let samples = dist.sample_n(&mut rng, 10000);
725        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
726        assert!(mean.abs() < 0.1, "mean={mean}");
727    }
728    #[test]
729    fn student_t_variance_approx() {
730        let nu = 10.0;
731        let dist = StudentTDist::new(nu);
732        let mut rng = Xoshiro256::new(121);
733        let samples = dist.sample_n(&mut rng, 20000);
734        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
735        let var = samples.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
736        let expected_var = nu / (nu - 2.0);
737        assert!(
738            (var - expected_var).abs() / expected_var < 0.15,
739            "var={var}"
740        );
741    }
742    #[test]
743    fn student_t_log_pdf_finite() {
744        let dist = StudentTDist::new(5.0);
745        let lp = dist.log_pdf(0.0);
746        assert!(lp.is_finite());
747    }
748    #[test]
749    fn f_dist_positive_samples() {
750        let dist = FDist::new(5.0, 10.0);
751        let mut rng = Xoshiro256::new(130);
752        for _ in 0..200 {
753            assert!(dist.sample(&mut rng) > 0.0);
754        }
755    }
756    #[test]
757    fn f_dist_mean_approx() {
758        let dist = FDist::new(2.0, 10.0);
759        let mut rng = Xoshiro256::new(131);
760        let samples = dist.sample_n(&mut rng, 10000);
761        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
762        let expected = 10.0 / (10.0 - 2.0);
763        assert!((mean - expected).abs() / expected < 0.2, "mean={mean}");
764    }
765    #[test]
766    fn chi2_positive_samples() {
767        let dist = ChiSquaredDist::new(5.0);
768        let mut rng = Xoshiro256::new(140);
769        for _ in 0..200 {
770            assert!(dist.sample(&mut rng) > 0.0);
771        }
772    }
773    #[test]
774    fn chi2_mean_approx() {
775        let k = 4.0;
776        let dist = ChiSquaredDist::new(k);
777        let mut rng = Xoshiro256::new(141);
778        let samples = dist.sample_n(&mut rng, 10000);
779        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
780        assert!((mean - k).abs() / k < 0.1, "mean={mean}, expected={k}");
781    }
782    #[test]
783    fn weibull_positive_samples() {
784        let dist = WeibullDist::new(2.0, 1.0);
785        let mut rng = Xoshiro256::new(150);
786        for _ in 0..200 {
787            assert!(dist.sample(&mut rng) > 0.0);
788        }
789    }
790    #[test]
791    fn weibull_pdf_nonneg() {
792        let dist = WeibullDist::new(2.0, 1.0);
793        for i in 1..10 {
794            assert!(dist.pdf(i as f64 * 0.1) >= 0.0);
795        }
796    }
797    #[test]
798    fn gumbel_samples_finite() {
799        let dist = GumbelDist::new(0.0, 1.0);
800        let mut rng = Xoshiro256::new(160);
801        for _ in 0..200 {
802            let v = dist.sample(&mut rng);
803            assert!(v.is_finite(), "v={v}");
804        }
805    }
806    #[test]
807    fn gumbel_mean_approx() {
808        let mu = 2.0;
809        let beta = 1.0;
810        let dist = GumbelDist::new(mu, beta);
811        let mut rng = Xoshiro256::new(161);
812        let samples = dist.sample_n(&mut rng, 10000);
813        let mean = samples.iter().sum::<f64>() / samples.len() as f64;
814        let expected = mu + beta * EULER_MASCHERONI;
815        assert!(
816            (mean - expected).abs() < 0.1,
817            "mean={mean}, expected={expected}"
818        );
819    }
820    #[test]
821    fn frechet_samples_finite() {
822        let dist = FrechetDist::new(2.0, 1.0, 0.0);
823        let mut rng = Xoshiro256::new(170);
824        for _ in 0..200 {
825            let v = dist.sample(&mut rng);
826            assert!(v.is_finite() && v > 0.0, "v={v}");
827        }
828    }
829    #[test]
830    fn frechet_pdf_nonneg() {
831        let dist = FrechetDist::new(2.0, 1.0, 0.0);
832        for i in 1..10 {
833            assert!(dist.pdf(i as f64 * 0.5) >= 0.0);
834        }
835    }
836    #[test]
837    fn gev_gumbel_samples_finite() {
838        let dist = GEVDist::new(0.0, 1.0, 0.0);
839        let mut rng = Xoshiro256::new(180);
840        for _ in 0..200 {
841            let v = dist.sample(&mut rng);
842            assert!(v.is_finite());
843        }
844    }
845    #[test]
846    fn gev_frechet_samples_finite() {
847        let dist = GEVDist::new(0.0, 1.0, 0.5);
848        let mut rng = Xoshiro256::new(181);
849        for _ in 0..200 {
850            let v = dist.sample(&mut rng);
851            assert!(v.is_finite());
852        }
853    }
854    #[test]
855    fn ziggurat_normal_samples_finite() {
856        let sampler = ZigguratNormal::new(256);
857        let mut rng = Xoshiro256::new(190);
858        for _ in 0..200 {
859            assert!(sampler.sample(&mut rng).is_finite());
860        }
861    }
862    #[test]
863    fn brownian_path_length() {
864        let bm = BrownianMotion::new(1.0, 100, 0.01);
865        let mut rng = Xoshiro256::new(200);
866        let path = bm.generate_path(&mut rng);
867        assert_eq!(path.len(), 101);
868        assert_eq!(path[0], 0.0);
869    }
870    #[test]
871    fn brownian_path_3d_finite() {
872        let bm = BrownianMotion::new(1.0, 50, 0.01);
873        let mut rng = Xoshiro256::new(201);
874        let path = bm.generate_path_3d(&mut rng);
875        for p in &path {
876            assert!(p[0].is_finite() && p[1].is_finite() && p[2].is_finite());
877        }
878    }
879    #[test]
880    fn brownian_msd_formula() {
881        let bm = BrownianMotion::new(1.0, 1, 0.01);
882        let msd = bm.theoretical_msd_3d(1);
883        assert!((msd - 0.06).abs() < 1e-10, "msd={msd}");
884    }
885    #[test]
886    fn brownian_bridge_endpoints() {
887        let bb = BrownianBridge::new(0.0, 1.0, 1.0, 100, 1.0);
888        let mut rng = Xoshiro256::new(210);
889        let path = bb.generate_path(&mut rng);
890        assert_eq!(path.len(), 101);
891        assert!((path[0] - 0.0).abs() < 1e-10);
892    }
893    #[test]
894    fn brownian_bridge_variance_midpoint() {
895        let t = 0.5;
896        let bb = BrownianBridge::new(0.0, 0.0, 1.0, 10, 1.0);
897        let var = bb.variance_at(t);
898        assert!((var - 0.25).abs() < 1e-10, "var={var}");
899    }
900    #[test]
901    fn ou_path_length() {
902        let ou = OrnsteinUhlenbeck::new(1.0, 0.0, 1.0, 100, 0.01);
903        let mut rng = Xoshiro256::new(220);
904        let path = ou.generate_path(&mut rng, 5.0);
905        assert_eq!(path.len(), 101);
906    }
907    #[test]
908    fn ou_stationary_variance() {
909        let theta = 2.0;
910        let sigma = 1.0;
911        let ou = OrnsteinUhlenbeck::new(theta, 0.0, sigma, 5000, 0.005);
912        let mut rng = Xoshiro256::new(221);
913        let path = ou.generate_path(&mut rng, 0.0);
914        let tail = &path[2000..];
915        let mean = tail.iter().sum::<f64>() / tail.len() as f64;
916        let var = tail.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / tail.len() as f64;
917        let expected_var = sigma * sigma / (2.0 * theta);
918        assert!(
919            (var - expected_var).abs() / expected_var < 0.3,
920            "var={var}, expected={expected_var}"
921        );
922    }
923    #[test]
924    fn ou_mean_reversion() {
925        let ou = OrnsteinUhlenbeck::new(1.0, 2.0, 0.5, 10, 0.1);
926        let mean_t = ou.mean_at(10.0, 5.0);
927        assert!((mean_t - 2.0).abs() < 1.0);
928    }
929    #[test]
930    fn fbm_path_length() {
931        let fbm = FractionalBrownianMotion::new(0.7, 100, 0.01);
932        let mut rng = Xoshiro256::new(230);
933        let path = fbm.generate_path(&mut rng);
934        assert_eq!(path.len(), 101);
935        assert_eq!(path[0], 0.0);
936    }
937    #[test]
938    fn fbm_hurst_variance_scaling() {
939        let fbm = FractionalBrownianMotion::new(0.8, 1, 1.0);
940        let var = fbm.theoretical_variance(1.0);
941        assert!((var - 1.0).abs() < 1e-10);
942    }
943    #[test]
944    fn goe_symmetric() {
945        let mut rng = Xoshiro256::new(240);
946        let m = GOEMatrix::new(5, &mut rng);
947        for i in 0..5 {
948            for j in 0..5 {
949                assert!(
950                    (m.get(i, j) - m.get(j, i)).abs() < 1e-15,
951                    "GOE not symmetric at ({i},{j})"
952                );
953            }
954        }
955    }
956    #[test]
957    fn goe_frobenius_positive() {
958        let mut rng = Xoshiro256::new(241);
959        let m = GOEMatrix::new(4, &mut rng);
960        assert!(m.frobenius_sq() > 0.0);
961    }
962    #[test]
963    fn gue_hermitian_real_symmetric() {
964        let mut rng = Xoshiro256::new(250);
965        let m = GUEMatrix::new(4, &mut rng);
966        for i in 0..4 {
967            for j in 0..4 {
968                assert!((m.real_part[i * 4 + j] - m.real_part[j * 4 + i]).abs() < 1e-15);
969                assert!((m.imag_part[i * 4 + j] + m.imag_part[j * 4 + i]).abs() < 1e-15);
970            }
971        }
972    }
973    #[test]
974    fn wishart_trace_approx_n_times_p() {
975        let n = 4;
976        let p = 100;
977        let mut rng = Xoshiro256::new(260);
978        let w = WishartMatrix::new(n, p, &mut rng);
979        let tr = w.trace();
980        assert!((tr / (n * p) as f64 - 1.0).abs() < 0.2, "trace={tr}");
981    }
982    #[test]
983    fn wishart_symmetric() {
984        let mut rng = Xoshiro256::new(261);
985        let w = WishartMatrix::new(3, 5, &mut rng);
986        for i in 0..3 {
987            for j in 0..3 {
988                assert!((w.get(i, j) - w.get(j, i)).abs() < 1e-10);
989            }
990        }
991    }
992    #[test]
993    fn sobol_sequence_range() {
994        let mut sobol = SobolSequence::new(3);
995        for _ in 0..100 {
996            let pt = sobol.next_point();
997            for &v in &pt {
998                assert!((0.0..=1.0).contains(&v), "v={v}");
999            }
1000        }
1001    }
1002    #[test]
1003    fn sobol_sequence_generate_count() {
1004        let mut sobol = SobolSequence::new(2);
1005        let pts = sobol.generate(50);
1006        assert_eq!(pts.len(), 50);
1007    }
1008    #[test]
1009    fn scrambled_halton_range() {
1010        let mut rng = Xoshiro256::new(270);
1011        let mut sh = ScrambledHalton::new(3, &mut rng);
1012        for _ in 0..100 {
1013            let pt = sh.next_point();
1014            for &v in &pt {
1015                assert!((0.0..=1.0).contains(&v), "v={v}");
1016            }
1017        }
1018    }
1019    #[test]
1020    fn gaussian_copula_in_unit_square() {
1021        let cop = GaussianCopula::new(0.7);
1022        let mut rng = Xoshiro256::new(280);
1023        for _ in 0..200 {
1024            let (u, v) = cop.sample(&mut rng);
1025            assert!(u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0, "u={u}, v={v}");
1026        }
1027    }
1028    #[test]
1029    fn gaussian_copula_kendall_tau() {
1030        let rho = 0.6;
1031        let cop = GaussianCopula::new(rho);
1032        let tau = cop.kendall_tau();
1033        let expected = (2.0 / PI) * rho.asin();
1034        assert!((tau - expected).abs() < 1e-10);
1035    }
1036    #[test]
1037    fn clayton_copula_in_unit_square() {
1038        let cop = ClaytonCopula::new(2.0);
1039        let mut rng = Xoshiro256::new(290);
1040        for _ in 0..200 {
1041            let (u, v) = cop.sample(&mut rng);
1042            assert!(u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0, "u={u}, v={v}");
1043        }
1044    }
1045    #[test]
1046    fn clayton_copula_kendall_tau() {
1047        let theta = 2.0;
1048        let cop = ClaytonCopula::new(theta);
1049        let tau = cop.kendall_tau();
1050        assert!((tau - theta / (theta + 2.0)).abs() < 1e-10);
1051    }
1052    #[test]
1053    fn gumbel_copula_in_unit_square() {
1054        let cop = GumbelCopula::new(2.0);
1055        let mut rng = Xoshiro256::new(300);
1056        for _ in 0..200 {
1057            let (u, v) = cop.sample(&mut rng);
1058            assert!(u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0, "u={u}, v={v}");
1059        }
1060    }
1061    #[test]
1062    fn gumbel_copula_kendall_tau() {
1063        let theta = 3.0;
1064        let cop = GumbelCopula::new(theta);
1065        assert!((cop.kendall_tau() - (1.0 - 1.0 / theta)).abs() < 1e-10);
1066    }
1067    #[test]
1068    fn frank_copula_in_unit_square() {
1069        let cop = FrankCopula::new(4.0);
1070        let mut rng = Xoshiro256::new(310);
1071        for _ in 0..200 {
1072            let (u, v) = cop.sample(&mut rng);
1073            assert!(u > 0.0 && u < 1.0 && v > 0.0 && v < 1.0, "u={u}, v={v}");
1074        }
1075    }
1076    #[test]
1077    fn antithetic_estimates_integral_linear() {
1078        let av = AntitheticVariates::new(5000);
1079        let mut rng = Xoshiro256::new(320);
1080        let est = av.estimate(&mut rng, &|x| x);
1081        assert!((est - 0.5).abs() < 0.01, "est={est}");
1082    }
1083    #[test]
1084    fn antithetic_estimates_integral_quadratic() {
1085        let av = AntitheticVariates::new(5000);
1086        let mut rng = Xoshiro256::new(321);
1087        let est = av.estimate(&mut rng, &|x| x * x);
1088        assert!((est - 1.0 / 3.0).abs() < 0.02, "est={est}");
1089    }
1090    #[test]
1091    fn stratified_mc_1d_integral() {
1092        let smc = StratifiedMonteCarlo::new(100, 1);
1093        let mut rng = Xoshiro256::new(330);
1094        let est = smc.estimate(&mut rng, &|x| x[0]);
1095        assert!((est - 0.5).abs() < 0.01, "est={est}");
1096    }
1097    #[test]
1098    fn log_gamma_known_values() {
1099        assert!(
1100            (log_gamma(1.0) - 0.0).abs() < 1e-6,
1101            "Γ(1)={}",
1102            log_gamma(1.0).exp()
1103        );
1104        assert!((log_gamma(2.0) - 0.0).abs() < 1e-6);
1105        assert!((log_gamma(3.0) - 2.0_f64.ln()).abs() < 1e-5);
1106        assert!((log_gamma(4.0) - 6.0_f64.ln()).abs() < 1e-4);
1107    }
1108    #[test]
1109    fn normal_cdf_known_values() {
1110        assert!(
1111            (normal_cdf(0.0) - 0.5).abs() < 1e-5,
1112            "Φ(0)={}",
1113            normal_cdf(0.0)
1114        );
1115        assert!((normal_cdf(1.645) - 0.95).abs() < 5e-3);
1116        assert!((normal_cdf(-1.645) - 0.05).abs() < 5e-3);
1117    }
1118    #[test]
1119    fn normal_ppf_inverse_of_cdf() {
1120        for p in [0.1, 0.25, 0.5, 0.75, 0.9] {
1121            let x = normal_ppf(p);
1122            let p2 = normal_cdf(x);
1123            assert!((p - p2).abs() < 1e-4, "p={p}, p2={p2}");
1124        }
1125    }
1126    #[test]
1127    fn truncated_normal_in_range() {
1128        let mut rng = Xoshiro256::new(340);
1129        for _ in 0..100 {
1130            let v = sample_truncated_normal(&mut rng, 0.0, 1.0, -2.0, 2.0);
1131            assert!((-2.0..=2.0).contains(&v), "v={v}");
1132        }
1133    }
1134    #[test]
1135    fn levy_process_cauchy_samples_mostly_finite() {
1136        let lp = LevyProcess::new(1.0, 0.0, 1.0, 0.0);
1137        let mut rng = Xoshiro256::new(350);
1138        let samples: Vec<f64> = (0..1000).map(|_| lp.sample(&mut rng)).collect();
1139        let count_finite = samples.iter().filter(|v| v.is_finite()).count();
1140        assert!(
1141            count_finite > 900,
1142            "too many non-finite samples: {count_finite}/1000"
1143        );
1144    }
1145    #[test]
1146    fn t_copula_in_unit_square() {
1147        let cop = TCopula::new(0.5, 5.0);
1148        let mut rng = Xoshiro256::new(360);
1149        for _ in 0..200 {
1150            let (u, v) = cop.sample(&mut rng);
1151            assert!(
1152                (0.0..=1.0).contains(&u) && (0.0..=1.0).contains(&v),
1153                "u={u}, v={v}"
1154            );
1155        }
1156    }
1157    #[test]
1158    fn rejection_sampler_produces_values_in_range() {
1159        let rs = RejectionSampler::new(2.0);
1160        let mut rng = Xoshiro256::new(370);
1161        let target = |x: f64| -> f64 {
1162            if (0.0..=1.0).contains(&x) {
1163                2.0 * (0.5 - (x - 0.5).abs())
1164            } else {
1165                0.0
1166            }
1167        };
1168        let mut propose = |r: &mut Xoshiro256| -> (f64, f64) {
1169            let x = r.next_f64();
1170            (x, 1.0)
1171        };
1172        for _ in 0..50 {
1173            let v = rs.sample(&mut rng, &mut propose, &target);
1174            assert!((0.0..=1.0).contains(&v));
1175        }
1176    }
1177}