stochastic-rs

Distributions

26 of 36 SIMD distribution structs with full Python + closed-form parity. Bulk samplers; ziggurat, rejection, inversion.

The stochastic-rs-distributions crate ships 36 Simd* distribution structs (grep -rc "^pub struct Simd" stochastic-rs-distributions/src --include='*.rs'), plus ComplexDistribution (37 total). This page's table covers the 26 that carry the full package — every one of them has:

  • A Simd<Name><T> struct that implements rand_distr::Distribution<T>
  • A bulk filler fill_slice(dst) that draws from the distribution's own internal RNG (no separate rng-explicit / _fast pair)
  • A DistributionExt impl with closed-form pdf / cdf / characteristic function / moments
  • A py_distribution!- or py_distribution_int!-generated #[pyclass] for Python use
  • A KS-test against the analytic CDF

Not shown here — 10 more real, public Simd* types that don't carry the full package (26 + 10 = 36), plus ComplexDistribution outside that count entirely:

  • Ged, Skellam, and the four Truncated{Normal,Exp,Beta,Gamma} wrappers (6) implement DistributionExt but have no Python binding.
  • SimdExpZig is the ziggurat-table Exp(1) primitive that SimdExp and other samplers build on internally — the same distribution as the Exponential row below, exposed as a lower-level type, not a distinct continuous distribution of its own.
  • SimdNonCentralChiSquared is structurally different from every type in this table: its noncentrality parameter is supplied per draw (sample_ncp(ncp)), not fixed at construction, so it implements neither rand_distr::Distribution nor DistributionExt nor fill_slice — see its own module docs. It backs the exact CIR transition density (stochastic-rs-stats/src/cir.rs) via the free function non_central_chi_squared::sample(df, lambda, seed).
  • Dirichlet and Wishart are multivariate (a Vec<T> / Array2<f64> per draw), so the scalar rand_distr::Distribution<T> shape does not apply; each exposes its own sample_fast/log_pdf instead.

That accounts for all 36. ComplexDistribution is a 37th, separate type (not Simd-prefixed) that samples Complex<T> directly and is the one type with no DistributionExt impl at all (see CLAUDE.md's note on the trait). The API docs (cargo doc -p stochastic-rs-distributions) are the exhaustive reference for all of them.

The list

Continuous

DistributionStrategy
NormalZiggurat
LognormalTransformation (exp(N))
CauchyInversion
Student-tComposition (Z/χν2/νZ / \sqrt{\chi^2_\nu/\nu})
ExponentialZiggurat
GammaMarsaglia-Tsang squeeze
BetaGamma-ratio composition
Chi-squaredComposition (Gamma)
Inverse GaussianMichael-Schucany
ParetoInversion
Generalized ParetoInversion
Generalized Extreme ValueInversion
WeibullZiggurat (Exp) + power transform
UniformInversion (trivial)
Alpha-stableChambers-Mallows-Stuck
Normal Inverse GaussianInverse-Gaussian mixture
Variance GammaGamma-time normal mixture
Johnson SUTransformation (sinh of N)
Skewed Student-t (Hansen)Two-piece scaled Student-t
Generalized Inverse GaussianHörmann-Leydold rejection / ratio-of-uniforms
Generalized HyperbolicGIG-clock normal mixture
Tempered Stable (positive)Devroye double rejection (cumulants and transforms; no closed-form density)

Discrete

DistributionStrategy
BinomialBTRS rejection / geometric waiting-time
GeometricInversion
PoissonInverse transform (precomputed CDF table)
HypergeometricInverse transform (precomputed CDF table)

Choosing a distribution

The tables above group by sampling strategy; this groups by modeling role instead. The full version — with the honest "these are actually the same distribution" call-outs and every citation — lives in the crate's own doc comment (stochastic-rs-distributions/src/lib.rs). Short version:

NeedReach for
Symmetric, light-tailed, unboundedNormal (default), or Ged to tune peakedness — Ged is never power-law heavy-tailed, for any shape parameter
Heavy-tailed, unbounded (financial returns)StudentT (ν=1 is Cauchy, exactly) or AlphaStable (generalizes Normal at α=2 and Cauchy at α=1, β=0, but has no closed-form pdf/cdf at general α); NormalInverseGauss or VarianceGamma for a skewed heavy tail with finite variance and closed-form moments (neither has a closed-form cdf); SkewT for Hansen's standardised skewed t with closed-form cdf and quantile — the GARCH innovation density; JohnsonSu when you want skew with light tails and every moment closed-form; GeneralizedHyperbolic is the parent family of NormalInverseGauss (λ = −1/2) and VarianceGamma over a Gig clock
Extreme-value tailsGev for block maxima, Gpd for excesses over a threshold — a different job from bulk return modeling; both carry closed-form moments with the usual ξ\xi existence thresholds, and stats::evt fits them
Positive, duration/volatility-shapedExponential (constant hazard), Weibull (rising or falling hazard), Gamma (waiting time; backs Beta/ChiSquared/Ged/Dirichlet internally), LogNormal (multiplicative growth), InverseGauss (first-passage time)
Genuine power-law heavy tail, positive supportPareto — the only positive-support type here with real threshold-dependent moment existence
Bounded [0, 1] or [a, b]Beta, Uniform, or a Truncated* wrapper — the four Truncated* types aren't a family to pick among, just "which base distribution, restricted to an interval"
Discrete counts, with replacementPoisson, Binomial
Discrete counts, without replacement (finite population)Hypergeometric — that assumption, not a tuning knob, separates it from Binomial
Signed count dataSkellam — the only discrete type here that can go negative
Covariance matrices, simplex proportionsWishart, Dirichlet — both skip DistributionExt entirely for their own inherent density methods

DistributionExt coverage is uneven: Ged, Skellam and the four Truncated* wrappers implement only pdf/cdf, not the moments; Dirichlet, Wishart, SimdNonCentralChiSquared and ComplexDistribution implement none of it. Confirm the override exists (cargo doc -p stochastic-rs-distributions) before assuming .mean() / .variance() are there.

Examples

Normal — bulk sampling and closed-form moments

tests/doctest_distributions_normal.rs
// docs: distributions#normal-bulk-sampling-and-closed-form-moments
//! Backs the Normal example on the distributions catalog page.

use rand_distr::Distribution;
use stochastic_rs::distributions::normal::SimdNormal;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::simd_rng::SeedExt;
use stochastic_rs::traits::DistributionExt;

#[test]
fn normal_bulk_sample_and_closed_form() {
  let seed = Deterministic::new(42);
  let d = SimdNormal::<f64>::new(/* mean */ 0.0, /* std */ 1.0, &seed);

  // Single sample, drawn from the project's own RNG (not `rand::thread_rng`).
  let mut rng = seed.rng();
  let _x: f64 = d.sample(&mut rng);

  // Bulk fill (uses internal RNG)
  let mut buf = vec![0.0_f64; 10_000];
  d.fill_slice(&mut buf);

  // Closed-form analytics
  assert!((d.mean() - 0.0).abs() < 1e-12);
  assert!((d.variance() - 1.0).abs() < 1e-12);
  let pdf = d.pdf(0.0); // 1/sqrt(2*pi) ~= 0.3989
  let cdf = d.cdf(1.96); // ~= 0.975
  assert!((pdf - 0.398_942_280_4).abs() < 1e-6);
  assert!((cdf - 0.975).abs() < 1e-3);
}
import stochastic_rs as srs
import numpy as np

d = srs.PyNormal(mean=0.0, std_dev=1.0, seed=42)
samples = d.sample(10_000)                   # numpy.ndarray
matrix  = d.sample_par(100, 10_000)          # shape (100, 10_000)

# Closed-form analytics
print(d.mean(), d.variance())                # 0.0 1.0
print(d.pdf(0.0), d.cdf(1.96))               # 0.3989, 0.975

Beta — bounded support distribution

tests/doctest_distributions_beta.rs
// docs: distributions#beta-bounded-support-distribution
//! Backs the Beta example on the distributions catalog page.

use stochastic_rs::distributions::beta::SimdBeta;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::traits::DistributionExt;

#[test]
fn beta_bounded_support_and_moments() {
  let d = SimdBeta::<f64>::new(
    /* alpha */ 2.0,
    /* beta */ 5.0,
    &Deterministic::new(42),
  );
  let mut buf = vec![0.0; 1_000];
  d.fill_slice(&mut buf);
  assert!(buf.iter().all(|&x| (0.0..=1.0).contains(&x)));

  // E[X] = alpha / (alpha + beta) = 2/7
  // Var  = alpha*beta / ((alpha+beta)^2 * (alpha+beta+1)) = 10/(49*8)
  assert!((d.mean() - 2.0 / 7.0).abs() < 1e-12);
  assert!((d.variance() - 10.0 / (49.0 * 8.0)).abs() < 1e-12);
}
import stochastic_rs as srs

d = srs.PyBeta(alpha=2.0, beta=5.0, seed=42)
samples = d.sample(1000)
print(samples.min(), samples.max())          # in [0, 1]
print(d.mean(), d.variance())                # 0.2857, 0.0255

Poisson — discrete count distribution

tests/doctest_distributions_poisson.rs
// docs: distributions#poisson-discrete-count-distribution
//! Backs the Poisson example on the distributions catalog page.

use stochastic_rs::distributions::poisson::SimdPoisson;
use stochastic_rs::simd_rng::Deterministic;

#[test]
fn poisson_bulk_sample_mean() {
  let d = SimdPoisson::<u32>::new(/* lambda */ 4.0, &Deterministic::new(42));
  let mut buf = vec![0_u32; 10_000];
  d.fill_slice(&mut buf);

  let mean = buf.iter().sum::<u32>() as f64 / 10_000.0;
  assert!(
    (mean - 4.0).abs() < 0.1,
    "sample mean = {mean:.3} (expect ~4.0)"
  );
}
import stochastic_rs as srs

d = srs.PyPoissonD(lambda_=4.0, seed=42)
counts = d.sample(10_000)                    # int dtype
print(counts.mean(), counts.var())           # both ≈ 4.0

Common patterns

Construction

Every distribution follows the same new(args, &seed) constructor pattern — pass Unseeded for an auto-seeded RNG or Deterministic::new(u64) for a reproducible stream. See the seeding concept page for the full design (including SeedExt::reseed, the generic R: SimdRngExt backing RNG, and the experimental dual-stream-rng feature). Note the seed is taken by reference and comes last. SimdNormal stands in for any distribution below; substitute the parameters of the one you want:

use stochastic_rs::distributions::normal::SimdNormal;
use stochastic_rs::simd_rng::Deterministic;
use stochastic_rs::simd_rng::Unseeded;

// auto-seeded
let d = SimdNormal::<f64>::new(/* mean */ 0.0, /* std_dev */ 1.0, &Unseeded);

// reproducible
let d = SimdNormal::<f64>::new(0.0, 1.0, &Deterministic::new(42));

// share one seed source across sub-components
let shared = Deterministic::new(42);
let d = SimdNormal::<f64>::new(0.0, 1.0, &shared);

Sampling

use rand_distr::Distribution;
use stochastic_rs::simd_rng::SeedExt;

// Single sample, via the project's own RNG (see the seeding concept page).
// `SeedExt` is what brings `.rng()` into scope.
let mut rng = Deterministic::new(0).rng();
let x: f64 = d.sample(&mut rng);

// Bulk fill (uses the distribution's own internal RNG, applies SIMD)
let mut buf = vec![0.0_f64; 10_000];
d.fill_slice(&mut buf);

Closed-form analytics

DistributionExt is f64-only (not generic over T) and every method defaults to a type_name-annotated unimplemented!(), overridden per distribution where a closed form exists:

use stochastic_rs::traits::DistributionExt;

let pdf = d.pdf(0.5);
let cdf = d.cdf(0.5);
let cf = d.characteristic_function(2.0); // Complex64
let m1 = d.mean();
let m2 = d.variance();

Adding a new distribution

See the adding-distribution SKILL — file-by-file recipe including the py_distribution! macro at the bottom.

Edit on GitHub

Last updated on

On this page