1pub fn erfc(x: f64) -> f64 {
32 if x < 0.0 {
33 return 2.0 - erfc(-x);
34 }
35 let t = 1.0 / (1.0 + 0.3275911 * x);
37 let poly = t
38 * (0.254_829_592
39 + t * (-0.284_496_736
40 + t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
41 poly * (-x * x).exp()
42}
43
44pub fn igamc(a: f64, x: f64) -> f64 {
52 if x < 0.0 || a <= 0.0 {
53 return 1.0;
54 }
55 if x < a + 1.0 {
56 1.0 - igam_series(a, x)
58 } else {
59 igam_cf(a, x)
60 }
61}
62
63fn igam_series(a: f64, x: f64) -> f64 {
65 let ln_gamma_a = ln_gamma(a);
66 let mut sum = 1.0 / a;
67 let mut term = 1.0 / a;
68 let mut n = 1.0_f64;
69 loop {
70 term *= x / (a + n);
71 sum += term;
72 if term.abs() < 1e-14 * sum.abs() {
73 break;
74 }
75 n += 1.0;
76 if n > 200.0 {
77 break;
78 }
79 }
80 sum * (-x + a * x.ln() - ln_gamma_a).exp()
81}
82
83fn igam_cf(a: f64, x: f64) -> f64 {
85 let ln_gamma_a = ln_gamma(a);
86 let fpmin = f64::MIN_POSITIVE / f64::EPSILON;
88 let mut b = x + 1.0 - a;
89 let mut c = 1.0 / fpmin;
90 let mut d = 1.0 / b;
91 let mut h = d;
92 let mut i = 1_u32;
93 loop {
94 let ai = -(i as f64) * ((i as f64) - a);
95 b += 2.0;
96 d = ai * d + b;
97 if d.abs() < fpmin {
98 d = fpmin;
99 }
100 c = b + ai / c;
101 if c.abs() < fpmin {
102 c = fpmin;
103 }
104 d = 1.0 / d;
105 h *= d * c;
106 if (d * c - 1.0).abs() < 1e-14 {
107 break;
108 }
109 i += 1;
110 if i > 200 {
111 break;
112 }
113 }
114 h * (-x + a * x.ln() - ln_gamma_a).exp()
115}
116
117fn ln_gamma(z: f64) -> f64 {
121 let c: [f64; 9] = [
123 0.999_999_999_999_809_3,
124 676.520_368_121_885_1,
125 -1_259.139_216_722_402_8,
126 771.323_428_777_653_1,
127 -176.615_029_162_140_6,
128 12.507_343_278_686_905,
129 -0.138_571_095_265_720_12,
130 9.984_369_578_019_572e-6,
131 1.505_632_735_149_311_6e-7,
132 ];
133 let x = z - 1.0;
134 let mut t = x + 7.5;
135 let mut ser = c[0];
136 for (i, ci) in c.iter().enumerate().skip(1) {
137 ser += ci / (x + i as f64);
138 }
139 t = (x + 0.5) * t.ln() - t;
140 t + (2.0 * std::f64::consts::PI).sqrt().ln() + ser.ln()
141}
142
143#[derive(Debug, Clone)]
149pub struct TestResult {
150 pub passed: bool,
152 pub statistic: f64,
154 pub p_value: f64,
156 pub description: String,
158}
159
160pub fn frequency_test(samples: &[u32]) -> Result<TestResult, &'static str> {
176 if samples.is_empty() {
177 return Err("frequency_test: samples must not be empty");
178 }
179 let n_bits = samples.len() as f64 * 32.0;
180 let ones: u64 = samples.iter().map(|&s| s.count_ones() as u64).sum();
181 let zeros = samples.len() as u64 * 32 - ones;
182
183 let s_n = (ones as f64 - zeros as f64) / n_bits.sqrt();
185 let p_value = erfc(s_n.abs() / std::f64::consts::SQRT_2);
186 let passed = p_value >= 0.01;
187
188 Ok(TestResult {
189 passed,
190 statistic: s_n,
191 p_value,
192 description: format!(
193 "NIST Frequency (Monobit): n_bits={n_bits:.0}, ones={ones}, \
194 S_n={s_n:.6}, p={p_value:.6} → {}",
195 if passed { "PASS" } else { "FAIL" }
196 ),
197 })
198}
199
200pub fn block_frequency_test(
218 samples: &[u32],
219 block_size: usize,
220) -> Result<TestResult, &'static str> {
221 if samples.is_empty() {
222 return Err("block_frequency_test: samples must not be empty");
223 }
224 if block_size == 0 {
225 return Err("block_frequency_test: block_size must be > 0");
226 }
227
228 let bits: Vec<u8> = samples
230 .iter()
231 .flat_map(|&s| (0..32).map(move |i| ((s >> i) & 1) as u8))
232 .collect();
233
234 let n_bits = bits.len();
235 let n_blocks = n_bits / block_size;
236 if n_blocks == 0 {
237 return Err("block_frequency_test: not enough bits for even one block");
238 }
239
240 let chi_sq: f64 = bits
241 .chunks_exact(block_size)
242 .take(n_blocks)
243 .map(|block| {
244 let ones: f64 = block.iter().map(|&b| b as f64).sum();
245 let pi = ones / block_size as f64;
246 (pi - 0.5) * (pi - 0.5)
247 })
248 .sum::<f64>()
249 * 4.0
250 * block_size as f64;
251
252 let p_value = igamc(n_blocks as f64 / 2.0, chi_sq / 2.0);
253 let passed = p_value >= 0.01;
254
255 Ok(TestResult {
256 passed,
257 statistic: chi_sq,
258 p_value,
259 description: format!(
260 "NIST Block Frequency: M={block_size}, N={n_blocks}, \
261 χ²={chi_sq:.4}, p={p_value:.6} → {}",
262 if passed { "PASS" } else { "FAIL" }
263 ),
264 })
265}
266
267pub fn runs_test(samples: &[u32]) -> Result<TestResult, &'static str> {
280 if samples.is_empty() {
281 return Err("runs_test: samples must not be empty");
282 }
283
284 let bits: Vec<u8> = samples
285 .iter()
286 .flat_map(|&s| (0..32).map(move |i| ((s >> i) & 1) as u8))
287 .collect();
288 let n = bits.len() as f64;
289
290 let ones: f64 = bits.iter().map(|&b| b as f64).sum();
291 let pi = ones / n;
292
293 let tau = 2.0 / n.sqrt();
295 if (pi - 0.5).abs() >= tau {
296 return Ok(TestResult {
297 passed: false,
298 statistic: 0.0,
299 p_value: 0.0,
300 description: format!(
301 "NIST Runs: prerequisite FAILED |π-0.5|={:.6} ≥ τ={tau:.6} → FAIL",
302 (pi - 0.5).abs()
303 ),
304 });
305 }
306
307 let v_n: f64 = 1.0 + bits.windows(2).filter(|w| w[0] != w[1]).count() as f64;
309
310 let numer = (v_n - 2.0 * n * pi * (1.0 - pi)).abs();
312 let denom = 2.0 * (2.0 * n).sqrt() * pi * (1.0 - pi);
313 let p_value = erfc(numer / denom);
314 let passed = p_value >= 0.01;
315
316 Ok(TestResult {
317 passed,
318 statistic: v_n,
319 p_value,
320 description: format!(
321 "NIST Runs: n={n:.0}, π={pi:.6}, V_n={v_n:.0}, p={p_value:.6} → {}",
322 if passed { "PASS" } else { "FAIL" }
323 ),
324 })
325}
326
327pub fn longest_run_test(samples: &[u32]) -> Result<TestResult, &'static str> {
344 if samples.is_empty() {
345 return Err("longest_run_test: samples must not be empty");
346 }
347
348 let bits: Vec<u8> = samples
349 .iter()
350 .flat_map(|&s| (0..32).map(move |i| ((s >> i) & 1) as u8))
351 .collect();
352
353 let n_bits = bits.len();
354
355 struct Params {
377 block_size: usize,
378 upper_bounds: &'static [usize],
381 probs: &'static [f64],
383 }
384
385 let params = if n_bits >= 6272 {
386 Params {
387 block_size: 128,
388 upper_bounds: &[4, 5, 6, 7, 8], probs: &[0.1174, 0.2430, 0.2490, 0.1752, 0.1027, 0.1127],
390 }
391 } else if n_bits >= 128 {
392 Params {
393 block_size: 8,
394 upper_bounds: &[1, 2, 3], probs: &[0.2148, 0.3672, 0.2305, 0.1875],
396 }
397 } else {
398 return Err("longest_run_test: need at least 128 bits");
399 };
400
401 let n_blocks = n_bits / params.block_size;
402 if n_blocks == 0 {
403 return Err("longest_run_test: not enough bits for one block");
404 }
405
406 let n_buckets = params.probs.len(); let mut freq = vec![0u64; n_buckets];
408
409 for block in bits.chunks_exact(params.block_size).take(n_blocks) {
410 let mut max_run = 0usize;
411 let mut cur_run = 0usize;
412 for &bit in block {
413 if bit == 1 {
414 cur_run += 1;
415 if cur_run > max_run {
416 max_run = cur_run;
417 }
418 } else {
419 cur_run = 0;
420 }
421 }
422 let bucket = params
426 .upper_bounds
427 .iter()
428 .position(|&ub| max_run <= ub)
429 .unwrap_or(n_buckets - 1);
430 freq[bucket] += 1;
431 }
432
433 let n_blocks_f = n_blocks as f64;
434 let chi_sq: f64 = freq
435 .iter()
436 .zip(params.probs.iter())
437 .map(|(&observed, &prob)| {
438 if prob > 0.0 {
439 let expected = prob * n_blocks_f;
440 let diff = observed as f64 - expected;
441 diff * diff / expected
442 } else {
443 0.0
444 }
445 })
446 .sum();
447
448 let df = (n_buckets - 1) as f64;
450 let p_value = igamc(df / 2.0, chi_sq / 2.0);
451 let passed = p_value >= 0.01;
452
453 Ok(TestResult {
454 passed,
455 statistic: chi_sq,
456 p_value,
457 description: format!(
458 "NIST Longest Run: M={}, N={n_blocks}, \
459 χ²={chi_sq:.4}, p={p_value:.6} → {}",
460 params.block_size,
461 if passed { "PASS" } else { "FAIL" }
462 ),
463 })
464}
465
466pub fn uniform_chi_squared(samples: &[u32], n_buckets: usize) -> Result<TestResult, &'static str> {
477 if samples.is_empty() {
478 return Err("uniform_chi_squared: samples must not be empty");
479 }
480 if n_buckets < 2 {
481 return Err("uniform_chi_squared: n_buckets must be >= 2");
482 }
483
484 let mut counts = vec![0u64; n_buckets];
485 let scale = 1.0 / 4_294_967_296.0_f64; for &s in samples {
488 let u = s as f64 * scale;
489 let bucket = ((u * n_buckets as f64) as usize).min(n_buckets - 1);
490 counts[bucket] += 1;
491 }
492
493 let expected = samples.len() as f64 / n_buckets as f64;
494 let chi_sq: f64 = counts
495 .iter()
496 .map(|&c| {
497 let d = c as f64 - expected;
498 d * d / expected
499 })
500 .sum();
501
502 let df = (n_buckets - 1) as f64;
503 let p_value = igamc(df / 2.0, chi_sq / 2.0);
504 let passed = p_value >= 0.01;
505
506 Ok(TestResult {
507 passed,
508 statistic: chi_sq,
509 p_value,
510 description: format!(
511 "Uniform χ²: n={}, k={n_buckets}, χ²={chi_sq:.4}, \
512 p={p_value:.6} → {}",
513 samples.len(),
514 if passed { "PASS" } else { "FAIL" }
515 ),
516 })
517}
518
519fn normal_cdf(x: f64) -> f64 {
521 0.5 * erfc(-x / std::f64::consts::SQRT_2)
522}
523
524fn box_muller(u1: f64, u2: f64) -> f64 {
528 let u1_safe = u1.max(5.96e-8_f64); let radius = (-2.0 * u1_safe.ln()).sqrt();
530 let angle = 2.0 * std::f64::consts::PI * u2;
531 radius * angle.cos()
532}
533
534pub fn normal_ks_test(samples: &[u32]) -> Result<TestResult, &'static str> {
542 if samples.len() < 2 {
543 return Err("normal_ks_test: need at least 2 samples (one u1/u2 pair)");
544 }
545 let scale = 1.0 / 4_294_967_296.0_f64;
546 let n_pairs = samples.len() / 2;
547
548 let mut normals: Vec<f64> = (0..n_pairs)
549 .map(|i| {
550 let u1 = samples[2 * i] as f64 * scale;
551 let u2 = samples[2 * i + 1] as f64 * scale;
552 box_muller(u1.max(scale), u2) })
554 .collect();
555
556 normals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
557 let n = normals.len() as f64;
558
559 let ks_stat = normals
560 .iter()
561 .enumerate()
562 .map(|(i, &x)| {
563 let theoretical = normal_cdf(x);
564 let empirical_hi = (i + 1) as f64 / n;
565 let empirical_lo = i as f64 / n;
566 (empirical_hi - theoretical)
567 .abs()
568 .max((theoretical - empirical_lo).abs())
569 })
570 .fold(0.0_f64, f64::max);
571
572 let critical = 1.628 / n.sqrt();
573 let passed = ks_stat < critical;
574
575 let lambda = (n.sqrt() + 0.12 + 0.11 / n.sqrt()) * ks_stat;
577 let p_value = {
578 let mut pv = 0.0_f64;
580 for k in 1..=50_i32 {
581 let sign = if k % 2 == 0 { -1.0 } else { 1.0 };
582 pv += sign * (-2.0 * (k as f64).powi(2) * lambda * lambda).exp();
583 }
584 (2.0 * pv).clamp(0.0, 1.0)
585 };
586
587 Ok(TestResult {
588 passed,
589 statistic: ks_stat,
590 p_value,
591 description: format!(
592 "Normal KS: n={n_pairs}, D={ks_stat:.6}, critical={critical:.6}, \
593 p≈{p_value:.6} → {}",
594 if passed { "PASS" } else { "FAIL" }
595 ),
596 })
597}
598
599pub fn independent_streams_correlation(seed: u64, n: usize) -> Result<TestResult, &'static str> {
605 use crate::engines::philox::philox_generate_u32s;
606
607 if n == 0 {
608 return Err("independent_streams_correlation: n must be > 0");
609 }
610
611 let stream_a = philox_generate_u32s(seed, n, 0);
612 let stream_b = philox_generate_u32s(seed, n, n as u64);
613
614 let scale = 1.0 / 4_294_967_296.0_f64;
615 let fa: Vec<f64> = stream_a.iter().map(|&v| v as f64 * scale).collect();
616 let fb: Vec<f64> = stream_b.iter().map(|&v| v as f64 * scale).collect();
617
618 let mean_a = fa.iter().sum::<f64>() / n as f64;
619 let mean_b = fb.iter().sum::<f64>() / n as f64;
620
621 let cov: f64 = fa
622 .iter()
623 .zip(fb.iter())
624 .map(|(&a, &b)| (a - mean_a) * (b - mean_b))
625 .sum::<f64>()
626 / n as f64;
627
628 let var_a: f64 = fa.iter().map(|&a| (a - mean_a).powi(2)).sum::<f64>() / n as f64;
629 let var_b: f64 = fb.iter().map(|&b| (b - mean_b).powi(2)).sum::<f64>() / n as f64;
630
631 let corr = if var_a > 0.0 && var_b > 0.0 {
632 cov / (var_a.sqrt() * var_b.sqrt())
633 } else {
634 0.0
635 };
636
637 let passed = corr.abs() < 0.01;
638
639 Ok(TestResult {
640 passed,
641 statistic: corr,
642 p_value: 1.0 - corr.abs(), description: format!(
644 "Independent Streams: n={n}, r={corr:.8} → {}",
645 if passed { "PASS" } else { "FAIL" }
646 ),
647 })
648}
649
650fn lcg_uniform(state: &mut u64) -> f64 {
660 const A: u64 = 16_807;
661 const M: u64 = 2_147_483_647; *state = (A * *state) % M;
663 *state as f64 / M as f64
664}
665
666pub fn box_muller_lcg_accuracy(n: usize) -> Result<TestResult, &'static str> {
677 if n < 2 {
678 return Err("box_muller_lcg_accuracy: need at least 2 samples");
679 }
680
681 let mut state: u64 = 123_456_789;
683 let mut normals: Vec<f64> = Vec::with_capacity(n);
684
685 while normals.len() < n {
686 let u1 = lcg_uniform(&mut state);
687 let u2 = lcg_uniform(&mut state);
688 let radius = (-2.0 * u1.ln()).sqrt();
690 let angle = 2.0 * std::f64::consts::PI * u2;
691 normals.push(radius * angle.cos());
692 if normals.len() < n {
693 normals.push(radius * angle.sin());
695 }
696 }
697 normals.truncate(n);
698
699 let n_f = n as f64;
700 let mean = normals.iter().sum::<f64>() / n_f;
701 let variance = normals.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n_f;
702 let std_dev = variance.sqrt();
703
704 let mean_ok = mean.abs() < 0.1;
706 let std_ok = (std_dev - 1.0).abs() < 0.1;
707 let passed = mean_ok && std_ok;
708
709 Ok(TestResult {
710 passed,
711 statistic: mean,
712 p_value: if passed { 1.0 } else { 0.0 },
713 description: format!(
714 "Box-Muller LCG: n={n}, mean={mean:.6}, std_dev={std_dev:.6} → {}",
715 if passed { "PASS" } else { "FAIL" }
716 ),
717 })
718}
719
720pub fn philox_counter_offset_independence(
731 seed: u64,
732 n: usize,
733 offset: u64,
734) -> Result<TestResult, &'static str> {
735 use crate::engines::philox::philox_generate_u32s;
736
737 if n == 0 {
738 return Err("philox_counter_offset_independence: n must be > 0");
739 }
740 if offset == 0 {
741 return Err("philox_counter_offset_independence: offset must be > 0");
742 }
743
744 let seq_a = philox_generate_u32s(seed, n, 0);
745 let seq_b = philox_generate_u32s(seed, n, offset);
746
747 let matches: usize = seq_a
751 .iter()
752 .zip(seq_b.iter())
753 .filter(|(a, b)| a == b)
754 .count();
755
756 let fraction_matching = matches as f64 / n as f64;
758 let passed = fraction_matching < 0.001;
760
761 Ok(TestResult {
762 passed,
763 statistic: fraction_matching,
764 p_value: if passed { 1.0 - fraction_matching } else { 0.0 },
765 description: format!(
766 "Philox counter offset independence: n={n}, offset={offset}, \
767 matches={matches} ({:.4}%) → {}",
768 fraction_matching * 100.0,
769 if passed { "PASS" } else { "FAIL" }
770 ),
771 })
772}
773
774#[cfg(test)]
779mod tests {
780 use super::*;
781 use crate::engines::philox::philox_generate_u32s;
782
783 const SEED: u64 = 42;
784 const N_1M: usize = 1_000_000;
785 const N_10K: usize = 10_000;
786 const N_100K: usize = 100_000;
787
788 #[test]
793 fn erfc_known_values() {
794 let v = erfc(0.0);
796 assert!((v - 1.0).abs() < 1e-6, "erfc(0) = {v}");
797 let v = erfc(10.0);
799 assert!(v < 1e-20, "erfc(10) = {v}");
800 let v = erfc(-10.0);
802 assert!((v - 2.0).abs() < 1e-20, "erfc(-10) = {v}");
803 let v = erfc(1.0);
805 assert!((v - 0.157_299_207_0).abs() < 1e-6, "erfc(1) = {v}");
806 }
807
808 #[test]
809 fn igamc_known_values() {
810 let v = igamc(1.0, 0.0);
812 assert!((v - 1.0).abs() < 1e-8, "igamc(1,0) = {v}");
813 let v = igamc(0.5, 0.0);
815 assert!((v - 1.0).abs() < 1e-8, "igamc(0.5,0) = {v}");
816 let v = igamc(1.0, 1.0);
818 assert!(
819 (v - (-1.0_f64).exp()).abs() < 1e-6,
820 "igamc(1,1) = {v}, expected {}",
821 (-1.0_f64).exp()
822 );
823 }
824
825 #[test]
830 fn test_philox_nist_frequency() {
831 let samples = philox_generate_u32s(SEED, N_1M, 0);
832 let result = frequency_test(&samples).expect("frequency_test should not error");
833 assert!(
834 result.passed,
835 "NIST Frequency test FAILED: {}",
836 result.description
837 );
838 assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
839 }
840
841 #[test]
846 fn test_philox_nist_block_frequency() {
847 let samples = philox_generate_u32s(SEED, N_1M, 0);
848 let result =
850 block_frequency_test(&samples, 128).expect("block_frequency_test should not error");
851 assert!(
852 result.passed,
853 "NIST Block Frequency test FAILED: {}",
854 result.description
855 );
856 assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
857 }
858
859 #[test]
864 fn test_philox_nist_runs() {
865 let samples = philox_generate_u32s(SEED, N_1M, 0);
866 let result = runs_test(&samples).expect("runs_test should not error");
867 assert!(
868 result.passed,
869 "NIST Runs test FAILED: {}",
870 result.description
871 );
872 assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
873 }
874
875 #[test]
880 fn test_philox_nist_longest_run() {
881 let samples = philox_generate_u32s(SEED, N_1M, 0);
883 let result = longest_run_test(&samples).expect("longest_run_test should not error");
884 assert!(
885 result.passed,
886 "NIST Longest Run test FAILED: {}",
887 result.description
888 );
889 assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
890 }
891
892 #[test]
897 fn test_uniform_chi_squared_goodness_of_fit() {
898 let samples = philox_generate_u32s(SEED, N_100K, 0);
901 let result =
902 uniform_chi_squared(&samples, 100).expect("uniform_chi_squared should not error");
903 assert!(
904 result.passed,
905 "Uniform χ² test FAILED: {}",
906 result.description
907 );
908 assert!(
910 result.statistic < 150.0,
911 "χ²={} suspiciously large for 99 df",
912 result.statistic
913 );
914 }
915
916 #[test]
921 fn test_normal_kolmogorov_smirnov() {
922 let samples = philox_generate_u32s(SEED, N_10K, 0);
924 let result = normal_ks_test(&samples).expect("normal_ks_test should not error");
925 assert!(
926 result.passed,
927 "Normal KS test FAILED: {}",
928 result.description
929 );
930 }
931
932 #[test]
937 fn test_philox_independent_streams() {
938 let result = independent_streams_correlation(SEED, N_100K)
939 .expect("correlation test should not error");
940 assert!(
941 result.passed,
942 "Independent streams correlation FAILED: {}",
943 result.description
944 );
945 assert!(
946 result.statistic.abs() < 0.01,
947 "|r|={} ≥ 0.01",
948 result.statistic
949 );
950 }
951
952 #[test]
957 fn frequency_test_rejects_empty() {
958 assert!(frequency_test(&[]).is_err());
959 }
960
961 #[test]
962 fn block_frequency_test_rejects_empty() {
963 assert!(block_frequency_test(&[], 128).is_err());
964 }
965
966 #[test]
967 fn block_frequency_test_rejects_zero_block_size() {
968 assert!(block_frequency_test(&[1, 2, 3], 0).is_err());
969 }
970
971 #[test]
972 fn runs_test_rejects_empty() {
973 assert!(runs_test(&[]).is_err());
974 }
975
976 #[test]
977 fn longest_run_test_rejects_empty() {
978 assert!(longest_run_test(&[]).is_err());
979 }
980
981 #[test]
982 fn uniform_chi_squared_rejects_empty() {
983 assert!(uniform_chi_squared(&[], 100).is_err());
984 }
985
986 #[test]
987 fn normal_ks_test_rejects_empty() {
988 assert!(normal_ks_test(&[]).is_err());
989 }
990
991 #[test]
997 fn test_box_muller_lcg_mean_accuracy() {
998 let result =
999 box_muller_lcg_accuracy(1000).expect("box_muller_lcg_accuracy should not error");
1000 assert!(
1001 result.statistic.abs() < 0.1,
1002 "Box-Muller LCG mean={:.6} not within ±0.1 of 0.0",
1003 result.statistic
1004 );
1005 }
1006
1007 #[test]
1009 fn test_box_muller_lcg_std_accuracy() {
1010 let mut state: u64 = 123_456_789;
1011 let n = 1000usize;
1012 let mut normals: Vec<f64> = Vec::with_capacity(n);
1013 while normals.len() < n {
1014 let u1 = lcg_uniform(&mut state);
1015 let u2 = lcg_uniform(&mut state);
1016 let radius = (-2.0 * u1.ln()).sqrt();
1017 let angle = 2.0 * std::f64::consts::PI * u2;
1018 normals.push(radius * angle.cos());
1019 if normals.len() < n {
1020 normals.push(radius * angle.sin());
1021 }
1022 }
1023 normals.truncate(n);
1024 let mean = normals.iter().sum::<f64>() / n as f64;
1025 let variance = normals.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
1026 let std_dev = variance.sqrt();
1027 assert!(
1028 (std_dev - 1.0).abs() < 0.1,
1029 "Box-Muller LCG std_dev={std_dev:.6} not within 10% of 1.0"
1030 );
1031 }
1032
1033 #[test]
1035 fn test_box_muller_lcg_combined_accuracy() {
1036 let result =
1037 box_muller_lcg_accuracy(1000).expect("box_muller_lcg_accuracy should not error");
1038 assert!(
1039 result.passed,
1040 "Box-Muller LCG combined accuracy FAILED: {}",
1041 result.description
1042 );
1043 }
1044
1045 #[test]
1047 fn test_box_muller_lcg_rejects_small_n() {
1048 assert!(box_muller_lcg_accuracy(0).is_err());
1049 assert!(box_muller_lcg_accuracy(1).is_err());
1050 }
1051
1052 #[test]
1058 fn test_philox_counter_offset_by_1000() {
1059 let result = philox_counter_offset_independence(SEED, 1000, 1000)
1060 .expect("philox_counter_offset_independence should not error");
1061 assert!(
1062 result.passed,
1063 "Philox offset independence FAILED: {}",
1064 result.description
1065 );
1066 assert!(
1068 result.statistic < 0.001,
1069 "too many matching values: {:.4}%",
1070 result.statistic * 100.0
1071 );
1072 }
1073
1074 #[test]
1076 fn test_philox_counter_offset_non_overlapping() {
1077 let n = 500usize;
1079 let result = philox_counter_offset_independence(SEED, n, n as u64)
1080 .expect("philox_counter_offset_independence should not error");
1081 assert!(
1082 result.passed,
1083 "Philox non-overlapping window independence FAILED: {}",
1084 result.description
1085 );
1086 }
1087
1088 #[test]
1090 fn test_philox_different_seeds_differ() {
1091 let seq_a = philox_generate_u32s(1, 1000, 0);
1092 let seq_b = philox_generate_u32s(2, 1000, 0);
1093 let matches: usize = seq_a
1094 .iter()
1095 .zip(seq_b.iter())
1096 .filter(|(a, b)| a == b)
1097 .count();
1098 let fraction = matches as f64 / 1000.0;
1099 assert!(
1100 fraction < 0.001,
1101 "Different seeds produced {:.4}% matching values — PRNG may be broken",
1102 fraction * 100.0
1103 );
1104 }
1105
1106 #[test]
1108 fn test_philox_counter_offset_rejects_zero_offset() {
1109 assert!(philox_counter_offset_independence(SEED, 100, 0).is_err());
1110 }
1111
1112 #[test]
1114 fn test_philox_counter_offset_rejects_zero_n() {
1115 assert!(philox_counter_offset_independence(SEED, 0, 10).is_err());
1116 }
1117}