Skip to main content

stochastic_rs_distributions/
lib.rs

1//! # stochastic-rs-distributions
2//!
3//! Probability distributions with SIMD bulk sampling, plus the foundational
4//! `FloatExt` / `SimdFloatExt` trait machinery and float impls.
5
6#![allow(non_snake_case)]
7#![allow(clippy::type_complexity)]
8#![allow(clippy::too_many_arguments)]
9
10use rand::Rng;
11pub use stochastic_rs_core::simd_rng;
12use wide::f32x8;
13use wide::f64x8;
14
15#[macro_use]
16mod macros;
17
18pub mod float_impls;
19pub mod special;
20pub mod traits;
21
22#[cfg(feature = "python")]
23pub use crate::traits::CallableDist;
24pub use crate::traits::DistributionExt;
25pub use crate::traits::DistributionSampler;
26pub use crate::traits::FloatExt;
27pub use crate::traits::Fn1D;
28pub use crate::traits::Fn2D;
29pub use crate::traits::SimdFloatExt;
30
31pub mod alpha_stable;
32pub mod beta;
33pub mod binomial;
34pub mod cauchy;
35pub mod chi_square;
36pub mod complex;
37pub mod exp;
38pub mod gamma;
39pub mod geometric;
40pub mod hypergeometric;
41pub mod inverse_gauss;
42pub mod lognormal;
43pub mod non_central_chi_squared;
44pub mod normal;
45pub mod normal_inverse_gauss;
46pub mod pareto;
47pub mod poisson;
48pub mod studentt;
49pub mod uniform;
50pub mod weibull;
51
52macro_rules! impl_distribution_sampler_float {
53  ($($dist:ty),+ $(,)?) => {
54    $(
55      impl<T: SimdFloatExt> DistributionSampler<T> for $dist {
56        #[inline]
57        fn fill_slice<R: Rng + ?Sized>(&self, rng: &mut R, out: &mut [T]) {
58          self.fill_slice(rng, out);
59        }
60      }
61    )+
62  };
63}
64
65macro_rules! impl_distribution_sampler_int {
66  ($($dist:ty),+ $(,)?) => {
67    $(
68      impl<T: num_traits::PrimInt> DistributionSampler<T> for $dist {
69        #[inline]
70        fn fill_slice<R: Rng + ?Sized>(&self, rng: &mut R, out: &mut [T]) {
71          self.fill_slice(rng, out);
72        }
73      }
74    )+
75  };
76}
77
78macro_rules! impl_distribution_sampler_float_const_n {
79  ($($dist:ty),+ $(,)?) => {
80    $(
81      impl<T: SimdFloatExt, const N: usize> DistributionSampler<T> for $dist {
82        #[inline]
83        fn fill_slice<R: Rng + ?Sized>(&self, rng: &mut R, out: &mut [T]) {
84          self.fill_slice(rng, out);
85        }
86      }
87    )+
88  };
89}
90
91impl_distribution_sampler_float!(
92  alpha_stable::SimdAlphaStable<T>,
93  beta::SimdBeta<T>,
94  cauchy::SimdCauchy<T>,
95  chi_square::SimdChiSquared<T>,
96  exp::SimdExp<T>,
97  gamma::SimdGamma<T>,
98  inverse_gauss::SimdInverseGauss<T>,
99  lognormal::SimdLogNormal<T>,
100  normal_inverse_gauss::SimdNormalInverseGauss<T>,
101  pareto::SimdPareto<T>,
102  studentt::SimdStudentT<T>,
103  uniform::SimdUniform<T>,
104  weibull::SimdWeibull<T>,
105);
106
107impl_distribution_sampler_int!(
108  binomial::SimdBinomial<T>,
109  geometric::SimdGeometric<T>,
110  hypergeometric::SimdHypergeometric<T>,
111  poisson::SimdPoisson<T>,
112);
113
114impl_distribution_sampler_float_const_n!(normal::SimdNormal<T, N>, exp::SimdExpZig<T, N>,);
115
116#[cfg(test)]
117mod distribution_sampler_tests {
118  use super::DistributionSampler;
119  use super::normal::SimdNormal;
120  use super::poisson::SimdPoisson;
121
122  #[test]
123  fn sample_n_returns_requested_length() {
124    let dist = SimdNormal::<f64>::new(0.0, 1.0);
125    let out = dist.sample_n(1024);
126    assert_eq!(out.len(), 1024);
127  }
128
129  #[test]
130  fn sample_matrix_float_has_expected_shape() {
131    let dist = SimdNormal::<f32>::new(0.0, 1.0);
132    let out = dist.sample_matrix(32, 64);
133    assert_eq!(out.shape(), &[32, 64]);
134  }
135
136  #[test]
137  fn sample_matrix_int_has_expected_shape() {
138    let dist = SimdPoisson::<i64>::new(1.5);
139    let out = dist.sample_matrix(16, 8);
140    assert_eq!(out.shape(), &[16, 8]);
141  }
142}
143
144fn fill_f32_zero_one<R: Rng + ?Sized>(rng: &mut R, out: &mut [f32]) {
145  for x in out.iter_mut() {
146    *x = rng.random();
147  }
148}
149
150fn fill_f64_zero_one<R: Rng + ?Sized>(rng: &mut R, out: &mut [f64]) {
151  for x in out.iter_mut() {
152    *x = rng.random();
153  }
154}
155
156impl SimdFloatExt for f32 {
157  type Simd = f32x8;
158
159  fn splat(val: f32) -> f32x8 {
160    f32x8::splat(val)
161  }
162
163  fn simd_from_array(arr: [f32; 8]) -> f32x8 {
164    f32x8::from(arr)
165  }
166
167  fn simd_to_array(v: f32x8) -> [f32; 8] {
168    v.to_array()
169  }
170
171  fn simd_ln(v: f32x8) -> f32x8 {
172    v.ln()
173  }
174
175  fn simd_sqrt(v: f32x8) -> f32x8 {
176    v.sqrt()
177  }
178
179  fn simd_cos(v: f32x8) -> f32x8 {
180    v.cos()
181  }
182
183  fn simd_sin(v: f32x8) -> f32x8 {
184    v.sin()
185  }
186
187  fn simd_exp(v: f32x8) -> f32x8 {
188    v.exp()
189  }
190
191  fn simd_tan(v: f32x8) -> f32x8 {
192    v.tan()
193  }
194
195  fn simd_max(a: f32x8, b: f32x8) -> f32x8 {
196    a.max(b)
197  }
198
199  fn simd_powf(v: f32x8, exp: f32) -> f32x8 {
200    v.powf(exp)
201  }
202
203  fn simd_floor(v: f32x8) -> f32x8 {
204    v.floor()
205  }
206
207  fn fill_uniform<R: Rng + ?Sized>(rng: &mut R, out: &mut [f32]) {
208    fill_f32_zero_one(rng, out)
209  }
210
211  fn fill_uniform_simd(rng: &mut crate::simd_rng::SimdRng, out: &mut [f32]) {
212    let mut chunks = out.chunks_exact_mut(8);
213    for chunk in &mut chunks {
214      chunk.copy_from_slice(&rng.next_f32_array());
215    }
216    let rem = chunks.into_remainder();
217    if !rem.is_empty() {
218      let arr = rng.next_f32_array();
219      rem.copy_from_slice(&arr[..rem.len()]);
220    }
221  }
222
223  fn sample_uniform<R: Rng + ?Sized>(rng: &mut R) -> f32 {
224    rng.random()
225  }
226
227  #[inline(always)]
228  fn sample_uniform_simd(rng: &mut crate::simd_rng::SimdRng) -> f32 {
229    rng.next_f32()
230  }
231
232  fn simd_from_i32x8(v: wide::i32x8) -> f32x8 {
233    v.round_float()
234  }
235
236  const PREFERS_F32_WN: bool = true;
237
238  #[inline(always)]
239  fn from_f64_fast(v: f64) -> f32 {
240    v as f32
241  }
242
243  #[inline(always)]
244  fn from_f32_fast(v: f32) -> f32 {
245    v
246  }
247
248  fn pi() -> f32 {
249    std::f32::consts::PI
250  }
251
252  fn two_pi() -> f32 {
253    2.0 * std::f32::consts::PI
254  }
255
256  fn min_positive_val() -> f32 {
257    f32::MIN_POSITIVE
258  }
259}
260
261impl SimdFloatExt for f64 {
262  type Simd = f64x8;
263
264  fn splat(val: f64) -> f64x8 {
265    f64x8::splat(val)
266  }
267
268  fn simd_from_array(arr: [f64; 8]) -> f64x8 {
269    f64x8::from(arr)
270  }
271
272  fn simd_to_array(v: f64x8) -> [f64; 8] {
273    v.to_array()
274  }
275
276  fn simd_ln(v: f64x8) -> f64x8 {
277    v.ln()
278  }
279
280  fn simd_sqrt(v: f64x8) -> f64x8 {
281    v.sqrt()
282  }
283
284  fn simd_cos(v: f64x8) -> f64x8 {
285    v.cos()
286  }
287
288  fn simd_sin(v: f64x8) -> f64x8 {
289    v.sin()
290  }
291
292  fn simd_exp(v: f64x8) -> f64x8 {
293    v.exp()
294  }
295
296  fn simd_tan(v: f64x8) -> f64x8 {
297    v.tan()
298  }
299
300  fn simd_max(a: f64x8, b: f64x8) -> f64x8 {
301    a.max(b)
302  }
303
304  fn simd_powf(v: f64x8, exp: f64) -> f64x8 {
305    v.powf(exp)
306  }
307
308  fn simd_floor(v: f64x8) -> f64x8 {
309    v.floor()
310  }
311
312  fn fill_uniform<R: Rng + ?Sized>(rng: &mut R, out: &mut [f64]) {
313    fill_f64_zero_one(rng, out)
314  }
315
316  fn fill_uniform_simd(rng: &mut crate::simd_rng::SimdRng, out: &mut [f64]) {
317    let mut chunks = out.chunks_exact_mut(8);
318    for chunk in &mut chunks {
319      chunk.copy_from_slice(&rng.next_f64_array());
320    }
321    let rem = chunks.into_remainder();
322    if !rem.is_empty() {
323      let arr = rng.next_f64_array();
324      rem.copy_from_slice(&arr[..rem.len()]);
325    }
326  }
327
328  fn sample_uniform<R: Rng + ?Sized>(rng: &mut R) -> f64 {
329    rng.random()
330  }
331
332  #[inline(always)]
333  fn sample_uniform_simd(rng: &mut crate::simd_rng::SimdRng) -> f64 {
334    rng.next_f64()
335  }
336
337  fn simd_from_i32x8(v: wide::i32x8) -> f64x8 {
338    f64x8::from_i32x8(v)
339  }
340
341  const PREFERS_F32_WN: bool = false;
342
343  #[inline(always)]
344  fn from_f64_fast(v: f64) -> f64 {
345    v
346  }
347
348  fn pi() -> f64 {
349    std::f64::consts::PI
350  }
351
352  fn two_pi() -> f64 {
353    2.0 * std::f64::consts::PI
354  }
355
356  fn min_positive_val() -> f64 {
357    f64::MIN_POSITIVE
358  }
359}
360
361#[cfg(test)]
362mod tests {
363  use plotly::Layout;
364  use plotly::Plot;
365  use plotly::Scatter;
366  use plotly::common::Line;
367  use plotly::common::LineShape;
368  use plotly::common::Mode;
369  use plotly::layout::GridPattern;
370  use plotly::layout::LayoutGrid;
371  use rand::rng;
372  use rand_distr::Distribution;
373
374  use crate::beta::SimdBeta;
375  use crate::binomial::SimdBinomial;
376  use crate::cauchy::SimdCauchy;
377  use crate::chi_square::SimdChiSquared;
378  use crate::exp::SimdExp;
379  use crate::exp::SimdExpZig;
380  use crate::gamma::SimdGamma;
381  use crate::geometric::SimdGeometric;
382  use crate::hypergeometric::SimdHypergeometric;
383  use crate::inverse_gauss::SimdInverseGauss;
384  use crate::lognormal::SimdLogNormal;
385  use crate::normal::SimdNormal;
386  use crate::normal_inverse_gauss::SimdNormalInverseGauss;
387  use crate::pareto::SimdPareto;
388  use crate::poisson::SimdPoisson;
389  use crate::studentt::SimdStudentT;
390  use crate::uniform::SimdUniform;
391  use crate::weibull::SimdWeibull;
392
393  /// A small helper to create a PDF-like histogram for continuous data in [min_x, max_x].
394  fn make_histogram(
395    samples: &[f32],
396    bins_count: usize,
397    min_x: f32,
398    max_x: f32,
399  ) -> (Vec<f32>, Vec<f32>) {
400    let bin_width = (max_x - min_x) / bins_count as f32;
401    let mut bins = vec![0.0; bins_count + 1];
402    for &val in samples {
403      if val >= min_x && val < max_x {
404        let idx = ((val - min_x) / bin_width) as usize;
405        bins[idx] += 1.0;
406      }
407    }
408    let total = samples.len() as f32;
409    // convert to PDF-like
410    for b in bins.iter_mut() {
411      *b /= total * bin_width;
412    }
413    let xs: Vec<f32> = (0..bins_count)
414      .map(|i| min_x + (i as f32 + 0.5) * bin_width)
415      .collect();
416    (xs, bins)
417  }
418
419  /// A small helper for discrete data: we just do a 0..=max_range “histogram”
420  fn make_discrete_pmf(samples: &[u32], max_range: u32) -> (Vec<f32>, Vec<f32>) {
421    let mut bins = vec![0.0; (max_range + 1) as usize];
422    for &val in samples {
423      if val <= max_range {
424        bins[val as usize] += 1.0;
425      }
426    }
427    let n = samples.len() as f32;
428    for b in bins.iter_mut() {
429      *b /= n;
430    }
431    let xs: Vec<f32> = (0..=max_range).map(|i| i as f32).collect();
432    (xs, bins)
433  }
434
435  /// We’ll place each distribution in a 4x4 grid => up to 16 subplots.
436  /// Let’s define a small function to map (row,col) -> "xN", "yN".
437  /// row, col are in [1..4].
438  fn subplot_axes(row: usize, col: usize) -> (String, String) {
439    // Subplot index = (row-1)*4 + col
440    let index = (row - 1) * 4 + col;
441    let xaxis = format!("x{}", index);
442    let yaxis = format!("y{}", index);
443    (xaxis, yaxis)
444  }
445
446  #[test]
447  #[ignore = "interactive: opens a browser via plot.show(); run with --ignored when generating combined plots"]
448  fn combined_all_distributions() {
449    // Create a 5x4 grid for 17 distributions
450    let mut plot = Plot::new();
451    plot.set_layout(
452      Layout::new().grid(
453        LayoutGrid::new()
454          .rows(5)
455          .columns(4)
456          .pattern(GridPattern::Independent),
457      ),
458    );
459
460    // We'll store a large number of samples for each distribution:
461    // typically 50k or 100k.
462    let sample_size = 50_000;
463
464    // 1) Normal => subplot (row=1, col=1)
465    {
466      let (xa, ya) = subplot_axes(1, 1);
467      let mut rng = rand::rng();
468      let dist: SimdNormal<f32> = SimdNormal::new(0.0, 1.0);
469      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
470      let (xs, bins) = make_histogram(&samples, 100, -4.0, 4.0);
471      let trace = Scatter::new(xs.clone(), bins)
472        .name("Normal(0,1) - SIMD")
473        .mode(Mode::Lines)
474        .line(Line::new().shape(LineShape::Linear))
475        .x_axis(&xa)
476        .y_axis(&ya);
477      plot.add_trace(trace);
478
479      // rand_distr comparison
480      let mut rng = rand::rng();
481      let rd = rand_distr::Normal::<f32>::new(0.0, 1.0).unwrap();
482      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
483      let (_, bins_rd) = make_histogram(&samples_rd, 100, -4.0, 4.0);
484      let trace_rd = Scatter::new(xs, bins_rd)
485        .name("Normal(0,1) - rand_distr")
486        .mode(Mode::Lines)
487        .line(
488          Line::new()
489            .shape(LineShape::Linear)
490            .dash(plotly::common::DashType::Dash),
491        )
492        .x_axis(&xa)
493        .y_axis(&ya);
494      plot.add_trace(trace_rd);
495    }
496
497    // 2) Cauchy => subplot (1,2)
498    {
499      let (xa, ya) = subplot_axes(1, 2);
500      let mut rng = rand::rng();
501      let dist = SimdCauchy::new(0.0, 1.0);
502      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
503      let (xs, bins) = make_histogram(&samples, 100, -10.0, 10.0);
504      let trace = Scatter::new(xs.clone(), bins)
505        .name("Cauchy(0,1) - SIMD")
506        .mode(Mode::Lines)
507        .line(Line::new().shape(LineShape::Linear))
508        .x_axis(&xa)
509        .y_axis(&ya);
510      plot.add_trace(trace);
511
512      // rand_distr comparison
513      let mut rng = rand::rng();
514      let rd = rand_distr::Cauchy::<f32>::new(0.0, 1.0).unwrap();
515      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
516      let (_, bins_rd) = make_histogram(&samples_rd, 100, -10.0, 10.0);
517      let trace_rd = Scatter::new(xs, bins_rd)
518        .name("Cauchy(0,1) - rand_distr")
519        .mode(Mode::Lines)
520        .line(
521          Line::new()
522            .shape(LineShape::Linear)
523            .dash(plotly::common::DashType::Dash),
524        )
525        .x_axis(&xa)
526        .y_axis(&ya);
527      plot.add_trace(trace_rd);
528    }
529
530    // 3) LogNormal => (1,3)
531    {
532      let (xa, ya) = subplot_axes(1, 3);
533      let mut rng = rand::rng();
534      let dist = SimdLogNormal::new(0.0, 1.0);
535      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
536      let (xs, bins) = make_histogram(&samples, 100, 0.0, 8.0);
537      let trace = Scatter::new(xs.clone(), bins)
538        .name("LogNormal(0,1) - SIMD")
539        .mode(Mode::Lines)
540        .line(Line::new().shape(LineShape::Linear))
541        .x_axis(&xa)
542        .y_axis(&ya);
543      plot.add_trace(trace);
544
545      // rand_distr comparison
546      let mut rng = rand::rng();
547      let rd = rand_distr::LogNormal::<f32>::new(0.0, 1.0).unwrap();
548      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
549      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 8.0);
550      let trace_rd = Scatter::new(xs, bins_rd)
551        .name("LogNormal(0,1) - rand_distr")
552        .mode(Mode::Lines)
553        .line(
554          Line::new()
555            .shape(LineShape::Linear)
556            .dash(plotly::common::DashType::Dash),
557        )
558        .x_axis(&xa)
559        .y_axis(&ya);
560      plot.add_trace(trace_rd);
561    }
562
563    // 4) Pareto => (1,4)
564    {
565      let (xa, ya) = subplot_axes(1, 4);
566      let mut rng = rand::rng();
567      let dist = SimdPareto::new(1.0, 1.5);
568      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
569      let (xs, bins) = make_histogram(&samples, 100, 0.0, 10.0);
570      let trace = Scatter::new(xs.clone(), bins)
571        .name("Pareto(1,1.5) - SIMD")
572        .mode(Mode::Lines)
573        .line(Line::new().shape(LineShape::Linear))
574        .x_axis(&xa)
575        .y_axis(&ya);
576      plot.add_trace(trace);
577
578      // rand_distr comparison
579      let mut rng = rand::rng();
580      let rd = rand_distr::Pareto::<f32>::new(1.0, 1.5).unwrap();
581      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
582      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 10.0);
583      let trace_rd = Scatter::new(xs, bins_rd)
584        .name("Pareto(1,1.5) - rand_distr")
585        .mode(Mode::Lines)
586        .line(
587          Line::new()
588            .shape(LineShape::Linear)
589            .dash(plotly::common::DashType::Dash),
590        )
591        .x_axis(&xa)
592        .y_axis(&ya);
593      plot.add_trace(trace_rd);
594    }
595
596    // 5) Weibull => (2,1)
597    {
598      let (xa, ya) = subplot_axes(2, 1);
599      let mut rng = rand::rng();
600      let dist = SimdWeibull::new(1.0, 1.5);
601      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
602      let (xs, bins) = make_histogram(&samples, 100, 0.0, 3.0);
603      let trace = Scatter::new(xs.clone(), bins)
604        .name("Weibull(1,1.5) - SIMD")
605        .mode(Mode::Lines)
606        .line(Line::new().shape(LineShape::Linear))
607        .x_axis(&xa)
608        .y_axis(&ya);
609      plot.add_trace(trace);
610
611      // rand_distr comparison
612      let mut rng = rand::rng();
613      let rd = rand_distr::Weibull::<f32>::new(1.0, 1.5).unwrap();
614      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
615      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 3.0);
616      let trace_rd = Scatter::new(xs, bins_rd)
617        .name("Weibull(1,1.5) - rand_distr")
618        .mode(Mode::Lines)
619        .line(
620          Line::new()
621            .shape(LineShape::Linear)
622            .dash(plotly::common::DashType::Dash),
623        )
624        .x_axis(&xa)
625        .y_axis(&ya);
626      plot.add_trace(trace_rd);
627    }
628
629    // 6) Gamma => (2,2)
630    {
631      let (xa, ya) = subplot_axes(2, 2);
632      let mut rng = rand::rng();
633      let dist = SimdGamma::new(2.0, 2.0);
634      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
635      let (xs, bins) = make_histogram(&samples, 120, 0.0, 20.0);
636      let trace = Scatter::new(xs.clone(), bins)
637        .name("Gamma(2,2) - SIMD")
638        .mode(Mode::Lines)
639        .line(Line::new().shape(LineShape::Linear))
640        .x_axis(&xa)
641        .y_axis(&ya);
642      plot.add_trace(trace);
643
644      // rand_distr comparison
645      let mut rng = rand::rng();
646      let rd = rand_distr::Gamma::<f32>::new(2.0, 2.0).unwrap();
647      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
648      let (_, bins_rd) = make_histogram(&samples_rd, 120, 0.0, 20.0);
649      let trace_rd = Scatter::new(xs, bins_rd)
650        .name("Gamma(2,2) - rand_distr")
651        .mode(Mode::Lines)
652        .line(
653          Line::new()
654            .shape(LineShape::Linear)
655            .dash(plotly::common::DashType::Dash),
656        )
657        .x_axis(&xa)
658        .y_axis(&ya);
659      plot.add_trace(trace_rd);
660    }
661
662    // 7) Beta => (2,3)
663    {
664      let (xa, ya) = subplot_axes(2, 3);
665      let mut rng = rand::rng();
666      let dist = SimdBeta::new(2.0, 2.0);
667      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
668      let (xs, bins) = make_histogram(&samples, 100, 0.0, 1.0);
669      let trace = Scatter::new(xs.clone(), bins)
670        .name("Beta(2,2) - SIMD")
671        .mode(Mode::Lines)
672        .line(Line::new().shape(LineShape::Linear))
673        .x_axis(&xa)
674        .y_axis(&ya);
675      plot.add_trace(trace);
676
677      // rand_distr comparison
678      let mut rng = rand::rng();
679      let rd = rand_distr::Beta::<f32>::new(2.0, 2.0).unwrap();
680      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
681      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 1.0);
682      let trace_rd = Scatter::new(xs, bins_rd)
683        .name("Beta(2,2) - rand_distr")
684        .mode(Mode::Lines)
685        .line(
686          Line::new()
687            .shape(LineShape::Linear)
688            .dash(plotly::common::DashType::Dash),
689        )
690        .x_axis(&xa)
691        .y_axis(&ya);
692      plot.add_trace(trace_rd);
693    }
694
695    // 8) Inverse Gaussian => (2,4)
696    {
697      let (xa, ya) = subplot_axes(2, 4);
698      let mut rng = rng();
699      let dist = SimdInverseGauss::new(1.0, 2.0);
700      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
701      let (xs, bins) = make_histogram(&samples, 100, 0.0, 3.0);
702      let trace = Scatter::new(xs.clone(), bins)
703        .name("InverseGauss(1,2) - SIMD")
704        .mode(Mode::Lines)
705        .line(Line::new().shape(LineShape::Linear))
706        .x_axis(&xa)
707        .y_axis(&ya);
708      plot.add_trace(trace);
709
710      // rand_distr comparison (InverseGaussian in rand_distr)
711      let mut rng = rand::rng();
712      let rd = rand_distr::InverseGaussian::<f32>::new(1.0, 2.0).unwrap();
713      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
714      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 3.0);
715      let trace_rd = Scatter::new(xs, bins_rd)
716        .name("InverseGauss(1,2) - rand_distr")
717        .mode(Mode::Lines)
718        .line(
719          Line::new()
720            .shape(LineShape::Linear)
721            .dash(plotly::common::DashType::Dash),
722        )
723        .x_axis(&xa)
724        .y_axis(&ya);
725      plot.add_trace(trace_rd);
726    }
727
728    // 9) Normal-Inverse Gauss => (3,1)
729    {
730      let (xa, ya) = subplot_axes(3, 1);
731      let mut rng = rand::rng();
732      let dist = SimdNormalInverseGauss::new(2.0, 0.0, 1.0, 0.0);
733      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
734      let (xs, bins) = make_histogram(&samples, 100, -3.0, 3.0);
735      let trace = Scatter::new(xs.clone(), bins)
736        .name("Nig(2,0) - SIMD")
737        .mode(Mode::Lines)
738        .line(Line::new().shape(LineShape::Linear))
739        .x_axis(&xa)
740        .y_axis(&ya);
741      plot.add_trace(trace);
742
743      // rand_distr has NormalInverseGaussian
744      let mut rng = rand::rng();
745      let rd = rand_distr::NormalInverseGaussian::<f32>::new(2.0, 0.0).unwrap();
746      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
747      let (_, bins_rd) = make_histogram(&samples_rd, 100, -3.0, 3.0);
748      let trace_rd = Scatter::new(xs, bins_rd)
749        .name("Nig(2,0) - rand_distr")
750        .mode(Mode::Lines)
751        .line(
752          Line::new()
753            .shape(LineShape::Linear)
754            .dash(plotly::common::DashType::Dash),
755        )
756        .x_axis(&xa)
757        .y_axis(&ya);
758      plot.add_trace(trace_rd);
759    }
760
761    // 10) StudentT => (3,2)
762    {
763      let (xa, ya) = subplot_axes(3, 2);
764      let mut rng = rng();
765      let dist = SimdStudentT::new(5.0);
766      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
767      let (xs, bins) = make_histogram(&samples, 120, -5.0, 5.0);
768      let trace = Scatter::new(xs.clone(), bins)
769        .name("StudentT(5) - SIMD")
770        .mode(Mode::Lines)
771        .line(Line::new().shape(LineShape::Linear))
772        .x_axis(&xa)
773        .y_axis(&ya);
774      plot.add_trace(trace);
775
776      // rand_distr comparison
777      let mut rng = rand::rng();
778      let rd = rand_distr::StudentT::<f32>::new(5.0).unwrap();
779      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
780      let (_, bins_rd) = make_histogram(&samples_rd, 120, -5.0, 5.0);
781      let trace_rd = Scatter::new(xs, bins_rd)
782        .name("StudentT(5) - rand_distr")
783        .mode(Mode::Lines)
784        .line(
785          Line::new()
786            .shape(LineShape::Linear)
787            .dash(plotly::common::DashType::Dash),
788        )
789        .x_axis(&xa)
790        .y_axis(&ya);
791      plot.add_trace(trace_rd);
792    }
793
794    // 11) Binomial => (3,3) (discrete)
795    {
796      let (xa, ya) = subplot_axes(3, 3);
797      let mut rng = rand::rng();
798      let dist = SimdBinomial::new(10, 0.3);
799      let samples: Vec<u32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
800      let (xs, bins) = make_discrete_pmf(&samples, 10);
801
802      let trace = Scatter::new(xs.clone(), bins)
803        .name("Binomial(10,0.3) - SIMD")
804        .mode(Mode::Lines)
805        .line(Line::new().shape(LineShape::Linear))
806        .x_axis(&xa)
807        .y_axis(&ya);
808      plot.add_trace(trace);
809
810      // rand_distr comparison
811      let mut rng = rand::rng();
812      let rd = rand_distr::Binomial::new(10, 0.3).unwrap();
813      let samples_rd: Vec<u32> = (0..sample_size)
814        .map(|_| rd.sample(&mut rng) as u32)
815        .collect();
816      let (_, bins_rd) = make_discrete_pmf(&samples_rd, 10);
817      let trace_rd = Scatter::new(xs, bins_rd)
818        .name("Binomial(10,0.3) - rand_distr")
819        .mode(Mode::Lines)
820        .line(
821          Line::new()
822            .shape(LineShape::Linear)
823            .dash(plotly::common::DashType::Dash),
824        )
825        .x_axis(&xa)
826        .y_axis(&ya);
827      plot.add_trace(trace_rd);
828    }
829
830    // 12) Geometric => (3,4)
831    {
832      let (xa, ya) = subplot_axes(3, 4);
833      let mut rng = rng();
834      let dist = SimdGeometric::new(0.25);
835      let samples: Vec<u32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
836      let (xs, bins) = make_discrete_pmf(&samples, 20);
837      let trace = Scatter::new(xs.clone(), bins)
838        .name("Geometric(0.25) - SIMD")
839        .mode(Mode::Lines)
840        .line(Line::new().shape(LineShape::Linear))
841        .x_axis(&xa)
842        .y_axis(&ya);
843      plot.add_trace(trace);
844
845      // rand_distr comparison
846      let mut rng = rand::rng();
847      let rd = rand_distr::Geometric::new(0.25).unwrap();
848      let samples_rd: Vec<u32> = (0..sample_size)
849        .map(|_| rd.sample(&mut rng) as u32)
850        .collect();
851      let (_, bins_rd) = make_discrete_pmf(&samples_rd, 20);
852      let trace_rd = Scatter::new(xs, bins_rd)
853        .name("Geometric(0.25) - rand_distr")
854        .mode(Mode::Lines)
855        .line(
856          Line::new()
857            .shape(LineShape::Linear)
858            .dash(plotly::common::DashType::Dash),
859        )
860        .x_axis(&xa)
861        .y_axis(&ya);
862      plot.add_trace(trace_rd);
863    }
864
865    // 13) HyperGeometric => (4,1)
866    {
867      let (xa, ya) = subplot_axes(4, 1);
868      let mut rng = rand::rng();
869      // N=20, K=5, n=6
870      let dist = SimdHypergeometric::new(20, 5, 6);
871      let samples: Vec<u32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
872      let (xs, bins) = make_discrete_pmf(&samples, 6);
873
874      let trace = Scatter::new(xs.clone(), bins)
875        .name("HyperGeo(20,5,6) - SIMD")
876        .mode(Mode::Lines)
877        .line(Line::new().shape(LineShape::Linear))
878        .x_axis(&xa)
879        .y_axis(&ya);
880      plot.add_trace(trace);
881
882      // rand_distr comparison
883      let mut rng = rand::rng();
884      let rd = rand_distr::Hypergeometric::new(20, 5, 6).unwrap();
885      let samples_rd: Vec<u32> = (0..sample_size)
886        .map(|_| rd.sample(&mut rng) as u32)
887        .collect();
888      let (_, bins_rd) = make_discrete_pmf(&samples_rd, 6);
889      let trace_rd = Scatter::new(xs, bins_rd)
890        .name("HyperGeo(20,5,6) - rand_distr")
891        .mode(Mode::Lines)
892        .line(
893          Line::new()
894            .shape(LineShape::Linear)
895            .dash(plotly::common::DashType::Dash),
896        )
897        .x_axis(&xa)
898        .y_axis(&ya);
899      plot.add_trace(trace_rd);
900    }
901
902    // 14) Poisson => (4,2)
903    {
904      let (xa, ya) = subplot_axes(4, 2);
905      let mut rng = rand::rng();
906      let dist = SimdPoisson::new(4.0);
907      let samples: Vec<u32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
908      let (xs, bins) = make_discrete_pmf(&samples, 15);
909
910      let trace = Scatter::new(xs.clone(), bins)
911        .name("Poisson(4) - SIMD")
912        .mode(Mode::Lines)
913        .line(Line::new().shape(LineShape::Linear))
914        .x_axis(&xa)
915        .y_axis(&ya);
916      plot.add_trace(trace);
917
918      // rand_distr comparison
919      let mut rng = rand::rng();
920      let rd = rand_distr::Poisson::<f64>::new(4.0).unwrap();
921      let samples_rd: Vec<u32> = (0..sample_size)
922        .map(|_| rd.sample(&mut rng) as u32)
923        .collect();
924      let (_, bins_rd) = make_discrete_pmf(&samples_rd, 15);
925      let trace_rd = Scatter::new(xs, bins_rd)
926        .name("Poisson(4) - rand_distr")
927        .mode(Mode::Lines)
928        .line(
929          Line::new()
930            .shape(LineShape::Linear)
931            .dash(plotly::common::DashType::Dash),
932        )
933        .x_axis(&xa)
934        .y_axis(&ya);
935      plot.add_trace(trace_rd);
936    }
937
938    // 15) Uniform (0,1) => (4,3)
939    {
940      let (xa, ya) = subplot_axes(4, 3);
941      let mut rng = rand::rng();
942      let dist = SimdUniform::new(0.0, 1.0);
943      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
944      let (xs, bins) = make_histogram(&samples, 100, 0.0, 1.0);
945      let trace = Scatter::new(xs.clone(), bins)
946        .name("Uniform(0,1) - SIMD")
947        .mode(Mode::Lines)
948        .line(Line::new().shape(LineShape::Linear))
949        .x_axis(&xa)
950        .y_axis(&ya);
951      plot.add_trace(trace);
952
953      // rand_distr comparison
954      let mut rng = rand::rng();
955      let rd = rand_distr::Uniform::<f32>::new(0.0, 1.0).unwrap();
956      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
957      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 1.0);
958      let trace_rd = Scatter::new(xs, bins_rd)
959        .name("Uniform(0,1) - rand_distr")
960        .mode(Mode::Lines)
961        .line(
962          Line::new()
963            .shape(LineShape::Linear)
964            .dash(plotly::common::DashType::Dash),
965        )
966        .x_axis(&xa)
967        .y_axis(&ya);
968      plot.add_trace(trace_rd);
969    }
970
971    // 16) Exponential => (4,4)
972    {
973      let (xa, ya) = subplot_axes(4, 4);
974      let mut rng = rand::rng();
975      let dist = SimdExp::new(1.5);
976      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
977      let (xs, bins) = make_histogram(&samples, 100, 0.0, 4.0);
978      let trace = Scatter::new(xs.clone(), bins)
979        .name("Exp(1.5) - SIMD")
980        .mode(Mode::Lines)
981        .line(Line::new().shape(LineShape::Linear))
982        .x_axis(&xa)
983        .y_axis(&ya);
984      plot.add_trace(trace);
985
986      // rand_distr comparison
987      let mut rng = rand::rng();
988      let rd = rand_distr::Exp::<f32>::new(1.5).unwrap();
989      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
990      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 4.0);
991      let trace_rd = Scatter::new(xs, bins_rd)
992        .name("Exp(1.5) - rand_distr")
993        .mode(Mode::Lines)
994        .line(
995          Line::new()
996            .shape(LineShape::Linear)
997            .dash(plotly::common::DashType::Dash),
998        )
999        .x_axis(&xa)
1000        .y_axis(&ya);
1001      plot.add_trace(trace_rd);
1002    }
1003
1004    // 17) Chi-Squared => (5,1)
1005    {
1006      let (xa, ya) = subplot_axes(5, 1);
1007      let mut rng = rand::rng();
1008      let dist = SimdChiSquared::new(5.0);
1009      let samples: Vec<f32> = (0..sample_size).map(|_| dist.sample(&mut rng)).collect();
1010      let (xs, bins) = make_histogram(&samples, 100, 0.0, 20.0);
1011      let trace = Scatter::new(xs.clone(), bins)
1012        .name("ChiSquared(5) - SIMD")
1013        .mode(Mode::Lines)
1014        .line(Line::new().shape(LineShape::Linear))
1015        .x_axis(&xa)
1016        .y_axis(&ya);
1017      plot.add_trace(trace);
1018
1019      // rand_distr comparison
1020      let mut rng = rand::rng();
1021      let rd = rand_distr::ChiSquared::<f32>::new(5.0).unwrap();
1022      let samples_rd: Vec<f32> = (0..sample_size).map(|_| rd.sample(&mut rng)).collect();
1023      let (_, bins_rd) = make_histogram(&samples_rd, 100, 0.0, 20.0);
1024      let trace_rd = Scatter::new(xs, bins_rd)
1025        .name("ChiSquared(5) - rand_distr")
1026        .mode(Mode::Lines)
1027        .line(
1028          Line::new()
1029            .shape(LineShape::Linear)
1030            .dash(plotly::common::DashType::Dash),
1031        )
1032        .x_axis(&xa)
1033        .y_axis(&ya);
1034      plot.add_trace(trace_rd);
1035    }
1036
1037    plot.show();
1038  }
1039
1040  use std::time::Instant;
1041
1042  #[test]
1043  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1044  fn bench_normal_simd_vs_rand() {
1045    let n = 10_000_000usize;
1046    let warmup = 1_000_000usize;
1047
1048    {
1049      let mut rng = rand::rng();
1050      let d: SimdNormal<f32> = SimdNormal::new(0.0, 1.0);
1051      let rd = rand_distr::Normal::<f32>::new(0.0, 1.0).unwrap();
1052      let mut s = 0.0f32;
1053      for _ in 0..warmup {
1054        s += d.sample(&mut rng);
1055        s += rd.sample(&mut rng);
1056      }
1057      std::hint::black_box(s);
1058    }
1059
1060    let mut rng = rand::rng();
1061    let simd: SimdNormal<f32> = SimdNormal::new(0.0, 1.0);
1062    let mut s_sum = 0.0f32;
1063    let t0 = Instant::now();
1064    for _ in 0..n {
1065      s_sum += simd.sample(&mut rng);
1066    }
1067    let dt_s = t0.elapsed();
1068
1069    let mut rng = rand::rng();
1070    let rd = rand_distr::Normal::<f32>::new(0.0, 1.0).unwrap();
1071    let mut r_sum = 0.0f32;
1072    let t1 = Instant::now();
1073    for _ in 0..n {
1074      r_sum += rd.sample(&mut rng);
1075    }
1076    let dt_r = t1.elapsed();
1077
1078    println!(
1079      "Normal single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1080      dt_s, s_sum, dt_r, r_sum
1081    );
1082    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1083  }
1084
1085  #[test]
1086  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1087  fn bench_lognormal_simd_vs_rand() {
1088    let n = 10_000_000usize;
1089    let warmup = 1_000_000usize;
1090
1091    {
1092      let mut rng = rand::rng();
1093      let d = SimdLogNormal::new(0.2f32, 0.8);
1094      let rd = rand_distr::LogNormal::<f32>::new(0.2, 0.8).unwrap();
1095      let mut s = 0.0f32;
1096      for _ in 0..warmup {
1097        s += d.sample(&mut rng);
1098        s += rd.sample(&mut rng);
1099      }
1100      std::hint::black_box(s);
1101    }
1102
1103    let mut rng = rand::rng();
1104    let simd = SimdLogNormal::new(0.2, 0.8);
1105    let mut s_sum = 0.0f32;
1106    let t0 = Instant::now();
1107    for _ in 0..n {
1108      s_sum += simd.sample(&mut rng);
1109    }
1110    let dt_s = t0.elapsed();
1111
1112    let mut rng = rand::rng();
1113    let rd = rand_distr::LogNormal::<f32>::new(0.2, 0.8).unwrap();
1114    let mut r_sum = 0.0f32;
1115    let t1 = Instant::now();
1116    for _ in 0..n {
1117      r_sum += rd.sample(&mut rng);
1118    }
1119    let dt_r = t1.elapsed();
1120
1121    println!(
1122      "LogNormal single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1123      dt_s, s_sum, dt_r, r_sum
1124    );
1125    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1126  }
1127
1128  #[test]
1129  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1130  fn bench_exp_simd_vs_rand() {
1131    let n = 10_000_000usize;
1132    let lambda = 1.5f32;
1133    let warmup = 1_000_000usize;
1134
1135    {
1136      let mut rng = rand::rng();
1137      let d = SimdExp::new(lambda);
1138      let rd = rand_distr::Exp::<f32>::new(lambda).unwrap();
1139      let mut s = 0.0f32;
1140      for _ in 0..warmup {
1141        s += d.sample(&mut rng);
1142        s += rd.sample(&mut rng);
1143      }
1144      std::hint::black_box(s);
1145    }
1146
1147    let mut rng = rand::rng();
1148    let simd = SimdExp::new(lambda);
1149    let mut s_sum = 0.0f32;
1150    let t0 = Instant::now();
1151    for _ in 0..n {
1152      s_sum += simd.sample(&mut rng);
1153    }
1154    let dt_s = t0.elapsed();
1155
1156    let mut rng = rand::rng();
1157    let rd = rand_distr::Exp::<f32>::new(lambda).unwrap();
1158    let mut r_sum = 0.0f32;
1159    let t1 = Instant::now();
1160    for _ in 0..n {
1161      r_sum += rd.sample(&mut rng);
1162    }
1163    let dt_r = t1.elapsed();
1164
1165    println!(
1166      "Exp single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1167      dt_s, s_sum, dt_r, r_sum
1168    );
1169    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1170  }
1171
1172  #[test]
1173  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1174  fn bench_exp_zig_simd_vs_rand() {
1175    let n = 10_000_000usize;
1176    let lambda = 1.5f32;
1177    let warmup = 1_000_000usize;
1178
1179    {
1180      let mut rng = rand::rng();
1181      let d: SimdExpZig<f32> = SimdExpZig::new(lambda);
1182      let d2 = SimdExp::new(lambda);
1183      let rd = rand_distr::Exp::<f32>::new(lambda).unwrap();
1184      let mut s = 0.0f32;
1185      for _ in 0..warmup {
1186        s += d.sample(&mut rng);
1187        s += d2.sample(&mut rng);
1188        s += rd.sample(&mut rng);
1189      }
1190      std::hint::black_box(s);
1191    }
1192
1193    let mut rng = rand::rng();
1194    let zig: SimdExpZig<f32> = SimdExpZig::new(lambda);
1195    let mut z_sum = 0.0f32;
1196    let t0 = Instant::now();
1197    for _ in 0..n {
1198      z_sum += zig.sample(&mut rng);
1199    }
1200    let dt_z = t0.elapsed();
1201
1202    let mut rng = rand::rng();
1203    let old = SimdExp::new(lambda);
1204    let mut o_sum = 0.0f32;
1205    let t1 = Instant::now();
1206    for _ in 0..n {
1207      o_sum += old.sample(&mut rng);
1208    }
1209    let dt_o = t1.elapsed();
1210
1211    let mut rng = rand::rng();
1212    let rd = rand_distr::Exp::<f32>::new(lambda).unwrap();
1213    let mut r_sum = 0.0f32;
1214    let t2 = Instant::now();
1215    for _ in 0..n {
1216      r_sum += rd.sample(&mut rng);
1217    }
1218    let dt_r = t2.elapsed();
1219
1220    println!(
1221      "Exp Ziggurat: {:?}, sum={:.3} | Exp ICDF: {:?}, sum={:.3} | rand_distr: {:?}, sum={:.3}",
1222      dt_z, z_sum, dt_o, o_sum, dt_r, r_sum
1223    );
1224    assert!(!z_sum.is_nan() && !o_sum.is_nan() && !r_sum.is_nan());
1225  }
1226
1227  #[test]
1228  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1229  fn bench_cauchy_simd_vs_rand() {
1230    let n = 10_000_000usize;
1231    let warmup = 1_000_000usize;
1232
1233    {
1234      let mut rng = rand::rng();
1235      let d = SimdCauchy::new(0.0f32, 1.0);
1236      let rd = rand_distr::Cauchy::<f32>::new(0.0, 1.0).unwrap();
1237      let mut s = 0.0f32;
1238      for _ in 0..warmup {
1239        s += d.sample(&mut rng);
1240        s += rd.sample(&mut rng);
1241      }
1242      std::hint::black_box(s);
1243    }
1244
1245    let mut rng = rand::rng();
1246    let simd = SimdCauchy::new(0.0, 1.0);
1247    let mut s_sum = 0.0f32;
1248    let t0 = Instant::now();
1249    for _ in 0..n {
1250      s_sum += simd.sample(&mut rng);
1251    }
1252    let dt_s = t0.elapsed();
1253
1254    let mut rng = rand::rng();
1255    let rd = rand_distr::Cauchy::<f32>::new(0.0, 1.0).unwrap();
1256    let mut r_sum = 0.0f32;
1257    let t1 = Instant::now();
1258    for _ in 0..n {
1259      r_sum += rd.sample(&mut rng);
1260    }
1261    let dt_r = t1.elapsed();
1262
1263    println!(
1264      "Cauchy single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1265      dt_s, s_sum, dt_r, r_sum
1266    );
1267    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1268  }
1269
1270  #[test]
1271  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1272  fn bench_gamma_simd_vs_rand() {
1273    let n = 10_000_000usize;
1274    let warmup = 1_000_000usize;
1275
1276    {
1277      let mut rng = rand::rng();
1278      let d = SimdGamma::new(2.0f32, 2.0);
1279      let rd = rand_distr::Gamma::<f32>::new(2.0, 2.0).unwrap();
1280      let mut s = 0.0f32;
1281      for _ in 0..warmup {
1282        s += d.sample(&mut rng);
1283        s += rd.sample(&mut rng);
1284      }
1285      std::hint::black_box(s);
1286    }
1287
1288    let mut rng = rand::rng();
1289    let simd = SimdGamma::new(2.0, 2.0);
1290    let mut s_sum = 0.0f32;
1291    let t0 = Instant::now();
1292    for _ in 0..n {
1293      s_sum += simd.sample(&mut rng);
1294    }
1295    let dt_s = t0.elapsed();
1296
1297    let mut rng = rand::rng();
1298    let rd = rand_distr::Gamma::<f32>::new(2.0, 2.0).unwrap();
1299    let mut r_sum = 0.0f32;
1300    let t1 = Instant::now();
1301    for _ in 0..n {
1302      r_sum += rd.sample(&mut rng);
1303    }
1304    let dt_r = t1.elapsed();
1305
1306    println!(
1307      "Gamma single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1308      dt_s, s_sum, dt_r, r_sum
1309    );
1310    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1311  }
1312
1313  #[test]
1314  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1315  fn bench_weibull_simd_vs_rand() {
1316    let n = 10_000_000usize;
1317    let warmup = 1_000_000usize;
1318
1319    {
1320      let mut rng = rand::rng();
1321      let d = SimdWeibull::new(1.0f32, 1.5);
1322      let rd = rand_distr::Weibull::<f32>::new(1.0, 1.5).unwrap();
1323      let mut s = 0.0f32;
1324      for _ in 0..warmup {
1325        s += d.sample(&mut rng);
1326        s += rd.sample(&mut rng);
1327      }
1328      std::hint::black_box(s);
1329    }
1330
1331    let mut rng = rand::rng();
1332    let simd = SimdWeibull::new(1.0, 1.5);
1333    let mut s_sum = 0.0f32;
1334    let t0 = Instant::now();
1335    for _ in 0..n {
1336      s_sum += simd.sample(&mut rng);
1337    }
1338    let dt_s = t0.elapsed();
1339
1340    let mut rng = rand::rng();
1341    let rd = rand_distr::Weibull::<f32>::new(1.0, 1.5).unwrap();
1342    let mut r_sum = 0.0f32;
1343    let t1 = Instant::now();
1344    for _ in 0..n {
1345      r_sum += rd.sample(&mut rng);
1346    }
1347    let dt_r = t1.elapsed();
1348
1349    println!(
1350      "Weibull single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1351      dt_s, s_sum, dt_r, r_sum
1352    );
1353    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1354  }
1355
1356  #[test]
1357  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1358  fn bench_beta_simd_vs_rand() {
1359    let n = 10_000_000usize;
1360    let warmup = 1_000_000usize;
1361
1362    {
1363      let mut rng = rand::rng();
1364      let d = SimdBeta::new(2.0f32, 2.0);
1365      let rd = rand_distr::Beta::<f32>::new(2.0, 2.0).unwrap();
1366      let mut s = 0.0f32;
1367      for _ in 0..warmup {
1368        s += d.sample(&mut rng);
1369        s += rd.sample(&mut rng);
1370      }
1371      std::hint::black_box(s);
1372    }
1373
1374    let mut rng = rand::rng();
1375    let simd = SimdBeta::new(2.0, 2.0);
1376    let mut s_sum = 0.0f32;
1377    let t0 = Instant::now();
1378    for _ in 0..n {
1379      s_sum += simd.sample(&mut rng);
1380    }
1381    let dt_s = t0.elapsed();
1382
1383    let mut rng = rand::rng();
1384    let rd = rand_distr::Beta::<f32>::new(2.0, 2.0).unwrap();
1385    let mut r_sum = 0.0f32;
1386    let t1 = Instant::now();
1387    for _ in 0..n {
1388      r_sum += rd.sample(&mut rng);
1389    }
1390    let dt_r = t1.elapsed();
1391
1392    println!(
1393      "Beta single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1394      dt_s, s_sum, dt_r, r_sum
1395    );
1396    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1397  }
1398
1399  #[test]
1400  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1401  fn bench_chisq_simd_vs_rand() {
1402    let n = 10_000_000usize;
1403    let warmup = 1_000_000usize;
1404
1405    {
1406      let mut rng = rand::rng();
1407      let d = SimdChiSquared::new(5.0f32);
1408      let rd = rand_distr::ChiSquared::<f32>::new(5.0).unwrap();
1409      let mut s = 0.0f32;
1410      for _ in 0..warmup {
1411        s += d.sample(&mut rng);
1412        s += rd.sample(&mut rng);
1413      }
1414      std::hint::black_box(s);
1415    }
1416
1417    let mut rng = rand::rng();
1418    let simd = SimdChiSquared::new(5.0);
1419    let mut s_sum = 0.0f32;
1420    let t0 = Instant::now();
1421    for _ in 0..n {
1422      s_sum += simd.sample(&mut rng);
1423    }
1424    let dt_s = t0.elapsed();
1425
1426    let mut rng = rand::rng();
1427    let rd = rand_distr::ChiSquared::<f32>::new(5.0).unwrap();
1428    let mut r_sum = 0.0f32;
1429    let t1 = Instant::now();
1430    for _ in 0..n {
1431      r_sum += rd.sample(&mut rng);
1432    }
1433    let dt_r = t1.elapsed();
1434
1435    println!(
1436      "ChiSq single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1437      dt_s, s_sum, dt_r, r_sum
1438    );
1439    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1440  }
1441
1442  #[test]
1443  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1444  fn bench_studentt_simd_vs_rand() {
1445    let n = 10_000_000usize;
1446    let warmup = 1_000_000usize;
1447
1448    {
1449      let mut rng = rand::rng();
1450      let d = SimdStudentT::new(5.0f32);
1451      let rd = rand_distr::StudentT::<f32>::new(5.0).unwrap();
1452      let mut s = 0.0f32;
1453      for _ in 0..warmup {
1454        s += d.sample(&mut rng);
1455        s += rd.sample(&mut rng);
1456      }
1457      std::hint::black_box(s);
1458    }
1459
1460    let mut rng = rand::rng();
1461    let simd = SimdStudentT::new(5.0);
1462    let mut s_sum = 0.0f32;
1463    let t0 = Instant::now();
1464    for _ in 0..n {
1465      s_sum += simd.sample(&mut rng);
1466    }
1467    let dt_s = t0.elapsed();
1468
1469    let mut rng = rand::rng();
1470    let rd = rand_distr::StudentT::<f32>::new(5.0).unwrap();
1471    let mut r_sum = 0.0f32;
1472    let t1 = Instant::now();
1473    for _ in 0..n {
1474      r_sum += rd.sample(&mut rng);
1475    }
1476    let dt_r = t1.elapsed();
1477
1478    println!(
1479      "StudentT single: simd {:?}, sum={:.3} | rand_distr {:?}, sum={:.3}",
1480      dt_s, s_sum, dt_r, r_sum
1481    );
1482    assert!(!s_sum.is_nan() && !r_sum.is_nan());
1483  }
1484
1485  #[test]
1486  #[ignore = "perf benchmark (5-10M sample loop): run with --ignored or via cargo bench"]
1487  fn bench_poisson_simd_vs_rand() {
1488    let n = 5_000_000usize;
1489    let warmup = 500_000usize;
1490
1491    {
1492      let mut rng = rand::rng();
1493      let d = SimdPoisson::<u32>::new(4.0);
1494      let rd = rand_distr::Poisson::<f64>::new(4.0).unwrap();
1495      let mut s: u64 = 0;
1496      for _ in 0..warmup {
1497        s += d.sample(&mut rng) as u64;
1498        s += rd.sample(&mut rng) as u64;
1499      }
1500      std::hint::black_box(s);
1501    }
1502
1503    let mut rng = rand::rng();
1504    let simd = SimdPoisson::<u32>::new(4.0);
1505    let mut s_sum: u64 = 0;
1506    let t0 = Instant::now();
1507    for _ in 0..n {
1508      s_sum += simd.sample(&mut rng) as u64;
1509    }
1510    let dt_s = t0.elapsed();
1511
1512    let mut rng = rand::rng();
1513    let rd = rand_distr::Poisson::<f64>::new(4.0).unwrap();
1514    let mut r_sum: u64 = 0;
1515    let t1 = Instant::now();
1516    for _ in 0..n {
1517      r_sum += rd.sample(&mut rng) as u64;
1518    }
1519    let dt_r = t1.elapsed();
1520
1521    println!(
1522      "Poisson single: simd {:?}, sum={} | rand_distr {:?}, sum={}",
1523      dt_s, s_sum, dt_r, r_sum
1524    );
1525    assert!(s_sum > 0 && r_sum > 0);
1526  }
1527
1528  // Helpers for timing benchmarks
1529  struct Row {
1530    name: &'static str,
1531    simd_ms: f64,
1532    rand_ms: f64,
1533  }
1534  fn time_f32<F1, F2>(
1535    rows: &mut Vec<Row>,
1536    n: usize,
1537    name: &'static str,
1538    mut simd_fn: F1,
1539    mut rand_fn: F2,
1540  ) where
1541    F1: FnMut() -> f32,
1542    F2: FnMut() -> f32,
1543  {
1544    use std::hint::black_box;
1545    let warmup = n / 5;
1546    let mut w = 0.0f32;
1547    for _ in 0..warmup {
1548      w += simd_fn();
1549      w += rand_fn();
1550    }
1551    black_box(w);
1552
1553    let t0 = Instant::now();
1554    let mut s_sum = 0.0f32;
1555    for _ in 0..n {
1556      s_sum += simd_fn();
1557    }
1558    let dt_simd = t0.elapsed().as_secs_f64() * 1000.0;
1559
1560    let t1 = Instant::now();
1561    let mut r_sum = 0.0f32;
1562    for _ in 0..n {
1563      r_sum += rand_fn();
1564    }
1565    let dt_rand = t1.elapsed().as_secs_f64() * 1000.0;
1566
1567    black_box(s_sum);
1568    black_box(r_sum);
1569    rows.push(Row {
1570      name,
1571      simd_ms: dt_simd,
1572      rand_ms: dt_rand,
1573    });
1574  }
1575  fn time_u32<F1, F2>(
1576    rows: &mut Vec<Row>,
1577    n: usize,
1578    name: &'static str,
1579    mut simd_fn: F1,
1580    mut rand_fn: F2,
1581  ) where
1582    F1: FnMut() -> u32,
1583    F2: FnMut() -> u32,
1584  {
1585    use std::hint::black_box;
1586    let warmup = n / 5;
1587    let mut w: u64 = 0;
1588    for _ in 0..warmup {
1589      w += simd_fn() as u64;
1590      w += rand_fn() as u64;
1591    }
1592    black_box(w);
1593
1594    let t0 = Instant::now();
1595    let mut s_sum: u64 = 0;
1596    for _ in 0..n {
1597      s_sum += simd_fn() as u64;
1598    }
1599    let dt_simd = t0.elapsed().as_secs_f64() * 1000.0;
1600
1601    let t1 = Instant::now();
1602    let mut r_sum: u64 = 0;
1603    for _ in 0..n {
1604      r_sum += rand_fn() as u64;
1605    }
1606    let dt_rand = t1.elapsed().as_secs_f64() * 1000.0;
1607
1608    black_box(s_sum);
1609    black_box(r_sum);
1610    rows.push(Row {
1611      name,
1612      simd_ms: dt_simd,
1613      rand_ms: dt_rand,
1614    });
1615  }
1616
1617  #[test]
1618  #[ignore = "perf benchmark summary (~119s): run with --ignored or via cargo bench"]
1619  fn bench_summary_table() {
1620    let n_f = 5_000_000usize; // samples for continuous/f32
1621    let n_i = 5_000_000usize; // samples for discrete/u32
1622
1623    let mut rows: Vec<Row> = Vec::new();
1624
1625    // Normal
1626    {
1627      let mut rng = rand::rng();
1628      let simd: SimdNormal<f32> = SimdNormal::new(0.0, 1.0);
1629      let mut rng2 = rand::rng();
1630      let rd = rand_distr::Normal::<f32>::new(0.0, 1.0).unwrap();
1631      time_f32(
1632        &mut rows,
1633        n_f,
1634        "Normal",
1635        || simd.sample(&mut rng),
1636        || rd.sample(&mut rng2),
1637      );
1638    }
1639
1640    // LogNormal
1641    {
1642      let mut rng = rand::rng();
1643      let simd = SimdLogNormal::new(0.2, 0.8);
1644      let mut rng2 = rand::rng();
1645      let rd = rand_distr::LogNormal::<f32>::new(0.2, 0.8).unwrap();
1646      time_f32(
1647        &mut rows,
1648        n_f,
1649        "LogNormal",
1650        || simd.sample(&mut rng),
1651        || rd.sample(&mut rng2),
1652      );
1653    }
1654
1655    // Exp
1656    {
1657      let mut rng = rand::rng();
1658      let simd = SimdExp::new(1.5);
1659      let mut rng2 = rand::rng();
1660      let rd = rand_distr::Exp::<f32>::new(1.5).unwrap();
1661      time_f32(
1662        &mut rows,
1663        n_f,
1664        "Exp",
1665        || simd.sample(&mut rng),
1666        || rd.sample(&mut rng2),
1667      );
1668    }
1669
1670    // Cauchy
1671    {
1672      let mut rng = rand::rng();
1673      let simd = SimdCauchy::new(0.0, 1.0);
1674      let mut rng2 = rand::rng();
1675      let rd = rand_distr::Cauchy::<f32>::new(0.0, 1.0).unwrap();
1676      time_f32(
1677        &mut rows,
1678        n_f,
1679        "Cauchy",
1680        || simd.sample(&mut rng),
1681        || rd.sample(&mut rng2),
1682      );
1683    }
1684
1685    // Gamma
1686    {
1687      let mut rng = rand::rng();
1688      let simd = SimdGamma::new(2.0, 2.0);
1689      let mut rng2 = rand::rng();
1690      let rd = rand_distr::Gamma::<f32>::new(2.0, 2.0).unwrap();
1691      time_f32(
1692        &mut rows,
1693        n_f,
1694        "Gamma",
1695        || simd.sample(&mut rng),
1696        || rd.sample(&mut rng2),
1697      );
1698    }
1699
1700    // Weibull
1701    {
1702      let mut rng = rand::rng();
1703      let simd = SimdWeibull::new(1.0, 1.5);
1704      let mut rng2 = rand::rng();
1705      let rd = rand_distr::Weibull::<f32>::new(1.0, 1.5).unwrap();
1706      time_f32(
1707        &mut rows,
1708        n_f,
1709        "Weibull",
1710        || simd.sample(&mut rng),
1711        || rd.sample(&mut rng2),
1712      );
1713    }
1714
1715    // Beta
1716    {
1717      let mut rng = rand::rng();
1718      let simd = SimdBeta::new(2.0, 2.0);
1719      let mut rng2 = rand::rng();
1720      let rd = rand_distr::Beta::<f32>::new(2.0, 2.0).unwrap();
1721      time_f32(
1722        &mut rows,
1723        n_f,
1724        "Beta",
1725        || simd.sample(&mut rng),
1726        || rd.sample(&mut rng2),
1727      );
1728    }
1729
1730    // Chi-Squared
1731    {
1732      let mut rng = rand::rng();
1733      let simd = SimdChiSquared::new(5.0);
1734      let mut rng2 = rand::rng();
1735      let rd = rand_distr::ChiSquared::<f32>::new(5.0).unwrap();
1736      time_f32(
1737        &mut rows,
1738        n_f,
1739        "ChiSquared",
1740        || simd.sample(&mut rng),
1741        || rd.sample(&mut rng2),
1742      );
1743    }
1744
1745    // StudentT
1746    {
1747      let mut rng = rand::rng();
1748      let simd = SimdStudentT::new(5.0);
1749      let mut rng2 = rand::rng();
1750      let rd = rand_distr::StudentT::<f32>::new(5.0).unwrap();
1751      time_f32(
1752        &mut rows,
1753        n_f,
1754        "StudentT",
1755        || simd.sample(&mut rng),
1756        || rd.sample(&mut rng2),
1757      );
1758    }
1759
1760    // Poisson (discrete)
1761    {
1762      let mut rng = rand::rng();
1763      let simd = SimdPoisson::new(4.0);
1764      let mut rng2 = rand::rng();
1765      let rd = rand_distr::Poisson::<f64>::new(4.0).unwrap();
1766      time_u32(
1767        &mut rows,
1768        n_i,
1769        "Poisson",
1770        || simd.sample(&mut rng),
1771        || rd.sample(&mut rng2) as u32,
1772      );
1773    }
1774
1775    // Optionally Pareto if available in rand_distr
1776    #[allow(unused)]
1777    {
1778      // If rand_distr had Pareto<f32>, uncomment below lines.
1779      let _ = SimdPareto::new(1.0, 1.5);
1780    }
1781
1782    // Print table
1783    println!(
1784      "{:<14} {:>12} {:>14}",
1785      "Distribution", "simd (ms)", "rand_distr (ms)"
1786    );
1787    println!("{:-<14} {:-<12} {:-<14}", "", "", "");
1788    for r in &rows {
1789      println!("{:<14} {:>12.2} {:>14.2}", r.name, r.simd_ms, r.rand_ms);
1790    }
1791
1792    // Normal fill_slice benchmark at various sizes
1793    println!();
1794    println!(
1795      "{:<24} {:>12} {:>14} {:>8}",
1796      "Normal fill_slice", "simd (ms)", "rand_distr (ms)", "speedup"
1797    );
1798    println!("{:-<24} {:-<12} {:-<14} {:-<8}", "", "", "", "");
1799    let total = 5_000_000usize;
1800    for &size in &[8, 16, 64, 256, 1024, 10_000, 100_000] {
1801      let iters = total / size;
1802      let simd = SimdNormal::<f32>::new(0.0, 1.0);
1803      let rd = rand_distr::Normal::<f32>::new(0.0, 1.0).unwrap();
1804      let mut buf = vec![0.0f32; size];
1805
1806      let mut rng = rand::rng();
1807      let t0 = Instant::now();
1808      for _ in 0..iters {
1809        simd.fill_slice(&mut rng, &mut buf);
1810        std::hint::black_box(&buf);
1811      }
1812      let dt_simd = t0.elapsed().as_secs_f64() * 1000.0;
1813
1814      let mut rng2 = rand::rng();
1815      let t1 = Instant::now();
1816      for _ in 0..iters {
1817        for x in buf.iter_mut() {
1818          *x = rd.sample(&mut rng2);
1819        }
1820        std::hint::black_box(&buf);
1821      }
1822      let dt_rand = t1.elapsed().as_secs_f64() * 1000.0;
1823
1824      let speedup = dt_rand / dt_simd;
1825      println!(
1826        "  n={:<20} {:>10.2} {:>14.2} {:>7.2}x",
1827        size, dt_simd, dt_rand, speedup
1828      );
1829    }
1830  }
1831}