#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/core/distribution.md"))]
use rand::{Rng, RngCore};
use rand_distr::{
Beta as RDBeta, Binomial as RDBinomial, Cauchy as RDCauchy, ChiSquared as RDChiSquared,
Distribution as RandDistr, Exp as RDExp, Gamma as RDGamma, LogNormal as RDLogNormal,
Normal as RDNormal, Poisson as RDPoisson, StudentT as RDStudentT, Weibull as RDWeibull,
};
pub type LogF64 = f64;
pub trait Distribution<T>: Send + Sync {
fn sample(&self, rng: &mut dyn RngCore) -> T;
fn log_prob(&self, x: &T) -> LogF64;
fn clone_box(&self) -> Box<dyn Distribution<T>>;
}
#[derive(Clone, Copy, Debug)]
pub struct Normal {
mu: f64,
sigma: f64,
}
impl Normal {
pub fn new(mu: f64, sigma: f64) -> crate::error::FugueResult<Self> {
if !mu.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Normal",
"Mean (mu) must be finite",
crate::error::ErrorCode::InvalidMean,
)
.with_context("mu", format!("{}", mu)));
}
if sigma <= 0.0 || !sigma.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Normal",
"Standard deviation (sigma) must be positive and finite",
crate::error::ErrorCode::InvalidVariance,
)
.with_context("sigma", format!("{}", sigma))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Normal { mu, sigma })
}
pub fn standard() -> Self {
Normal {
mu: 0.0,
sigma: 1.0,
}
}
pub fn mu(&self) -> f64 {
self.mu
}
pub fn sigma(&self) -> f64 {
self.sigma
}
}
impl Distribution<f64> for Normal {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.sigma <= 0.0 {
return f64::NAN;
}
RDNormal::new(self.mu, self.sigma).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.sigma <= 0.0 || !self.sigma.is_finite() || !self.mu.is_finite() || !x.is_finite() {
return f64::NEG_INFINITY;
}
let z = (x - self.mu) / self.sigma;
const LN_2PI: f64 = 1.837_877_066_409_345_6; -0.5 * z * z - self.sigma.ln() - 0.5 * LN_2PI
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Uniform {
low: f64,
high: f64,
}
impl Uniform {
pub fn new(low: f64, high: f64) -> crate::error::FugueResult<Self> {
if !low.is_finite() || !high.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Uniform",
"Bounds must be finite",
crate::error::ErrorCode::InvalidRange,
)
.with_context("low", format!("{}", low))
.with_context("high", format!("{}", high)));
}
if low >= high {
return Err(crate::error::FugueError::invalid_parameters(
"Uniform",
"Lower bound must be less than upper bound",
crate::error::ErrorCode::InvalidRange,
)
.with_context("low", format!("{}", low))
.with_context("high", format!("{}", high)));
}
Ok(Uniform { low, high })
}
pub fn unit() -> Self {
Uniform {
low: 0.0,
high: 1.0,
}
}
pub fn low(&self) -> f64 {
self.low
}
pub fn high(&self) -> f64 {
self.high
}
}
impl Distribution<f64> for Uniform {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.low >= self.high || !self.low.is_finite() || !self.high.is_finite() {
return f64::NAN;
}
Rng::gen_range(rng, self.low..self.high)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.low >= self.high
|| !self.low.is_finite()
|| !self.high.is_finite()
|| !x.is_finite()
{
return f64::NEG_INFINITY;
}
if *x < self.low || *x >= self.high {
f64::NEG_INFINITY
} else {
let width = self.high - self.low;
if width <= 0.0 {
f64::NEG_INFINITY
} else {
-width.ln()
}
}
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct LogNormal {
mu: f64,
sigma: f64,
}
impl LogNormal {
pub fn new(mu: f64, sigma: f64) -> crate::error::FugueResult<Self> {
if !mu.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"LogNormal",
"Mean (mu) must be finite",
crate::error::ErrorCode::InvalidMean,
)
.with_context("mu", format!("{}", mu)));
}
if sigma <= 0.0 || !sigma.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"LogNormal",
"Standard deviation (sigma) must be positive and finite",
crate::error::ErrorCode::InvalidVariance,
)
.with_context("sigma", format!("{}", sigma))
.with_context("expected", "> 0.0 and finite"));
}
Ok(LogNormal { mu, sigma })
}
pub fn mu(&self) -> f64 {
self.mu
}
pub fn sigma(&self) -> f64 {
self.sigma
}
}
impl Distribution<f64> for LogNormal {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.sigma <= 0.0 {
return f64::NAN;
}
RDLogNormal::new(self.mu, self.sigma).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.sigma <= 0.0 || !self.sigma.is_finite() || !self.mu.is_finite() {
return f64::NEG_INFINITY;
}
if *x <= 0.0 || !x.is_finite() {
return f64::NEG_INFINITY;
}
let lx = x.ln();
let z = (lx - self.mu) / self.sigma;
const LN_2PI: f64 = 1.837_877_066_409_345_6; -0.5 * z * z - lx - self.sigma.ln() - 0.5 * LN_2PI
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Exponential {
rate: f64,
}
impl Exponential {
pub fn new(rate: f64) -> crate::error::FugueResult<Self> {
if rate <= 0.0 || !rate.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Exponential",
"Rate parameter must be positive and finite",
crate::error::ErrorCode::InvalidRate,
)
.with_context("rate", format!("{}", rate))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Exponential { rate })
}
pub fn rate(&self) -> f64 {
self.rate
}
}
impl Distribution<f64> for Exponential {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.rate <= 0.0 {
return f64::NAN;
}
RDExp::new(self.rate).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.rate <= 0.0 || !self.rate.is_finite() || !x.is_finite() {
return f64::NEG_INFINITY;
}
if *x < 0.0 {
f64::NEG_INFINITY
} else {
self.rate.ln() - self.rate * x
}
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Bernoulli {
p: f64,
}
impl Bernoulli {
pub fn new(p: f64) -> crate::error::FugueResult<Self> {
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(crate::error::FugueError::invalid_parameters(
"Bernoulli",
"Probability must be in [0, 1]",
crate::error::ErrorCode::InvalidProbability,
)
.with_context("p", format!("{}", p))
.with_context("expected", "[0.0, 1.0]"));
}
Ok(Bernoulli { p })
}
pub fn fair() -> Self {
Bernoulli { p: 0.5 }
}
pub fn p(&self) -> f64 {
self.p
}
}
impl Distribution<bool> for Bernoulli {
fn sample(&self, rng: &mut dyn RngCore) -> bool {
if self.p < 0.0 || self.p > 1.0 || !self.p.is_finite() {
return false; }
use rand::Rng;
rng.gen::<f64>() < self.p
}
fn log_prob(&self, x: &bool) -> LogF64 {
if self.p < 0.0 || self.p > 1.0 || !self.p.is_finite() {
return f64::NEG_INFINITY;
}
if *x {
if self.p <= 0.0 {
f64::NEG_INFINITY
} else {
self.p.ln()
}
} else {
if self.p >= 1.0 {
f64::NEG_INFINITY
} else {
(1.0 - self.p).ln()
}
}
}
fn clone_box(&self) -> Box<dyn Distribution<bool>> {
Box::new(*self)
}
}
#[derive(Clone, Debug)]
pub struct Categorical {
probs: Vec<f64>,
cumulative: Vec<f64>,
}
impl Categorical {
fn compute_cumulative(probs: &[f64]) -> Vec<f64> {
let mut cumulative = Vec::with_capacity(probs.len());
let mut acc = 0.0;
for &p in probs {
acc += p;
cumulative.push(acc);
}
cumulative
}
fn validate_probs(probs: &[f64]) -> crate::error::FugueResult<()> {
if probs.is_empty() {
return Err(crate::error::FugueError::invalid_parameters(
"Categorical",
"Probability vector cannot be empty",
crate::error::ErrorCode::InvalidProbability,
)
.with_context("length", "0"));
}
let sum: f64 = probs.iter().sum();
if (sum - 1.0).abs() > 1e-6 {
return Err(crate::error::FugueError::invalid_parameters(
"Categorical",
"Probabilities must sum to 1.0",
crate::error::ErrorCode::InvalidProbability,
)
.with_context("sum", format!("{:.6}", sum))
.with_context("expected", "1.0")
.with_context("tolerance", "1e-6"));
}
for (i, &p) in probs.iter().enumerate() {
if !p.is_finite() || p < 0.0 {
return Err(crate::error::FugueError::invalid_parameters(
"Categorical",
"All probabilities must be non-negative and finite",
crate::error::ErrorCode::InvalidProbability,
)
.with_context("index", format!("{}", i))
.with_context("value", format!("{}", p))
.with_context("expected", ">= 0.0 and finite"));
}
}
Ok(())
}
pub fn new(probs: Vec<f64>) -> crate::error::FugueResult<Self> {
Self::validate_probs(&probs)?;
let cumulative = Self::compute_cumulative(&probs);
Ok(Categorical { probs, cumulative })
}
pub fn uniform(k: usize) -> crate::error::FugueResult<Self> {
if k == 0 {
return Err(crate::error::FugueError::invalid_parameters(
"Categorical",
"Number of categories must be positive",
crate::error::ErrorCode::InvalidCount,
)
.with_context("k", "0"));
}
let prob = 1.0 / k as f64;
let probs = vec![prob; k];
let cumulative = Self::compute_cumulative(&probs);
Ok(Categorical { probs, cumulative })
}
pub fn revalidate(&self) -> crate::error::FugueResult<()> {
Self::validate_probs(&self.probs)
}
pub fn probs(&self) -> &[f64] {
&self.probs
}
pub fn len(&self) -> usize {
self.probs.len()
}
pub fn is_empty(&self) -> bool {
self.probs.is_empty()
}
}
impl Distribution<usize> for Categorical {
fn sample(&self, rng: &mut dyn RngCore) -> usize {
if self.cumulative.is_empty() {
return 0;
}
use rand::Rng;
let u: f64 = rng.gen();
let idx = self.cumulative.partition_point(|&c| c < u);
idx.min(self.probs.len() - 1)
}
fn log_prob(&self, x: &usize) -> LogF64 {
match self.probs.get(*x) {
Some(&p) if p > 0.0 => p.ln(),
_ => f64::NEG_INFINITY,
}
}
fn clone_box(&self) -> Box<dyn Distribution<usize>> {
Box::new(self.clone())
}
}
#[derive(Clone, Copy, Debug)]
pub struct Beta {
alpha: f64,
beta: f64,
}
impl Beta {
pub fn new(alpha: f64, beta: f64) -> crate::error::FugueResult<Self> {
if alpha <= 0.0 || !alpha.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Beta",
"Alpha parameter must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("alpha", format!("{}", alpha))
.with_context("expected", "> 0.0 and finite"));
}
if beta <= 0.0 || !beta.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Beta",
"Beta parameter must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("beta", format!("{}", beta))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Beta { alpha, beta })
}
pub fn uniform_prior() -> Self {
Beta {
alpha: 1.0,
beta: 1.0,
}
}
pub fn alpha(&self) -> f64 {
self.alpha
}
pub fn beta(&self) -> f64 {
self.beta
}
}
impl Distribution<f64> for Beta {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.alpha <= 0.0 || self.beta <= 0.0 {
return f64::NAN;
}
RDBeta::new(self.alpha, self.beta).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.alpha <= 0.0
|| self.beta <= 0.0
|| !self.alpha.is_finite()
|| !self.beta.is_finite()
|| !x.is_finite()
{
return f64::NEG_INFINITY;
}
let x = *x;
if !(0.0..=1.0).contains(&x) {
return f64::NEG_INFINITY;
}
let log_beta_fn = libm::lgamma(self.alpha) + libm::lgamma(self.beta)
- libm::lgamma(self.alpha + self.beta);
if x == 0.0 {
return if self.alpha > 1.0 {
f64::NEG_INFINITY
} else if self.alpha < 1.0 {
f64::INFINITY
} else {
-log_beta_fn
};
}
if x == 1.0 {
return if self.beta > 1.0 {
f64::NEG_INFINITY
} else if self.beta < 1.0 {
f64::INFINITY
} else {
-log_beta_fn
};
}
let ln_x = x.ln();
let ln_1_minus_x = (1.0 - x).ln();
(self.alpha - 1.0) * ln_x + (self.beta - 1.0) * ln_1_minus_x - log_beta_fn
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Gamma {
shape: f64,
rate: f64,
}
impl Gamma {
pub fn new(shape: f64, rate: f64) -> crate::error::FugueResult<Self> {
if shape <= 0.0 || !shape.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Gamma",
"Shape parameter must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("shape", format!("{}", shape))
.with_context("expected", "> 0.0 and finite"));
}
if rate <= 0.0 || !rate.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Gamma",
"Rate parameter must be positive and finite",
crate::error::ErrorCode::InvalidRate,
)
.with_context("rate", format!("{}", rate))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Gamma { shape, rate })
}
pub fn shape(&self) -> f64 {
self.shape
}
pub fn rate(&self) -> f64 {
self.rate
}
}
impl Distribution<f64> for Gamma {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.shape <= 0.0 || self.rate <= 0.0 {
return f64::NAN;
}
RDGamma::new(self.shape, 1.0 / self.rate)
.unwrap()
.sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.shape <= 0.0
|| self.rate <= 0.0
|| !self.shape.is_finite()
|| !self.rate.is_finite()
|| !x.is_finite()
{
return f64::NEG_INFINITY;
}
if *x <= 0.0 {
return f64::NEG_INFINITY;
}
let log_rate = self.rate.ln();
let log_x = x.ln();
let log_gamma_shape = libm::lgamma(self.shape);
self.shape * log_rate + (self.shape - 1.0) * log_x - self.rate * x - log_gamma_shape
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Binomial {
n: u64,
p: f64,
}
impl Binomial {
pub fn new(n: u64, p: f64) -> crate::error::FugueResult<Self> {
if !p.is_finite() || !(0.0..=1.0).contains(&p) {
return Err(crate::error::FugueError::invalid_parameters(
"Binomial",
"Probability must be in [0, 1]",
crate::error::ErrorCode::InvalidProbability,
)
.with_context("p", format!("{}", p))
.with_context("expected", "[0.0, 1.0]"));
}
Ok(Binomial { n, p })
}
pub fn n(&self) -> u64 {
self.n
}
pub fn p(&self) -> f64 {
self.p
}
}
impl Distribution<u64> for Binomial {
fn sample(&self, rng: &mut dyn RngCore) -> u64 {
RDBinomial::new(self.n, self.p).unwrap().sample(rng)
}
fn log_prob(&self, x: &u64) -> LogF64 {
if !self.p.is_finite() || !(0.0..=1.0).contains(&self.p) {
return f64::NEG_INFINITY;
}
let k = *x;
if k > self.n {
return f64::NEG_INFINITY;
}
if self.p == 0.0 {
return if k == 0 { 0.0 } else { f64::NEG_INFINITY };
}
if self.p == 1.0 {
return if k == self.n { 0.0 } else { f64::NEG_INFINITY };
}
let log_binom_coeff = libm::lgamma(self.n as f64 + 1.0)
- libm::lgamma(k as f64 + 1.0)
- libm::lgamma((self.n - k) as f64 + 1.0);
log_binom_coeff + (k as f64) * self.p.ln() + ((self.n - k) as f64) * (1.0 - self.p).ln()
}
fn clone_box(&self) -> Box<dyn Distribution<u64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Poisson {
lambda: f64,
}
impl Poisson {
pub fn new(lambda: f64) -> crate::error::FugueResult<Self> {
if lambda <= 0.0 || !lambda.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Poisson",
"Rate parameter lambda must be positive and finite",
crate::error::ErrorCode::InvalidRate,
)
.with_context("lambda", format!("{}", lambda))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Poisson { lambda })
}
pub fn lambda(&self) -> f64 {
self.lambda
}
}
impl Distribution<u64> for Poisson {
fn sample(&self, rng: &mut dyn RngCore) -> u64 {
if self.lambda <= 0.0 || !self.lambda.is_finite() {
return 0;
}
RDPoisson::new(self.lambda).unwrap().sample(rng) as u64
}
fn log_prob(&self, x: &u64) -> LogF64 {
if self.lambda <= 0.0 || !self.lambda.is_finite() {
return f64::NEG_INFINITY;
}
let k = *x;
if self.lambda > 700.0 && k == 0 {
return -self.lambda; }
let k_f64 = k as f64;
let log_lambda = self.lambda.ln();
let log_factorial = libm::lgamma(k_f64 + 1.0);
k_f64 * log_lambda - self.lambda - log_factorial
}
fn clone_box(&self) -> Box<dyn Distribution<u64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct StudentT {
df: f64,
loc: f64,
scale: f64,
}
impl StudentT {
pub fn new(df: f64, loc: f64, scale: f64) -> crate::error::FugueResult<Self> {
if df <= 0.0 || !df.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"StudentT",
"Degrees of freedom must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("df", format!("{}", df))
.with_context("expected", "> 0.0 and finite"));
}
if !loc.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"StudentT",
"Location (loc) must be finite",
crate::error::ErrorCode::InvalidMean,
)
.with_context("loc", format!("{}", loc)));
}
if scale <= 0.0 || !scale.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"StudentT",
"Scale must be positive and finite",
crate::error::ErrorCode::InvalidVariance,
)
.with_context("scale", format!("{}", scale))
.with_context("expected", "> 0.0 and finite"));
}
Ok(StudentT { df, loc, scale })
}
pub fn df(&self) -> f64 {
self.df
}
pub fn loc(&self) -> f64 {
self.loc
}
pub fn scale(&self) -> f64 {
self.scale
}
}
impl Distribution<f64> for StudentT {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.df <= 0.0 || self.scale <= 0.0 {
return f64::NAN;
}
let t = RDStudentT::new(self.df).unwrap().sample(rng);
self.loc + self.scale * t
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.df <= 0.0
|| self.scale <= 0.0
|| !self.df.is_finite()
|| !self.scale.is_finite()
|| !self.loc.is_finite()
|| !x.is_finite()
{
return f64::NEG_INFINITY;
}
const LN_PI: f64 = 1.144_729_885_849_400_2; let z = (x - self.loc) / self.scale;
libm::lgamma((self.df + 1.0) / 2.0)
- libm::lgamma(self.df / 2.0)
- 0.5 * (self.df.ln() + LN_PI)
- self.scale.ln()
- 0.5 * (self.df + 1.0) * (z * z / self.df).ln_1p()
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Cauchy {
loc: f64,
scale: f64,
}
impl Cauchy {
pub fn new(loc: f64, scale: f64) -> crate::error::FugueResult<Self> {
if !loc.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Cauchy",
"Location (loc) must be finite",
crate::error::ErrorCode::InvalidMean,
)
.with_context("loc", format!("{}", loc)));
}
if scale <= 0.0 || !scale.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Cauchy",
"Scale must be positive and finite",
crate::error::ErrorCode::InvalidVariance,
)
.with_context("scale", format!("{}", scale))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Cauchy { loc, scale })
}
pub fn loc(&self) -> f64 {
self.loc
}
pub fn scale(&self) -> f64 {
self.scale
}
}
impl Distribution<f64> for Cauchy {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.scale <= 0.0 {
return f64::NAN;
}
RDCauchy::new(self.loc, self.scale).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.scale <= 0.0 || !self.scale.is_finite() || !self.loc.is_finite() || !x.is_finite() {
return f64::NEG_INFINITY;
}
const LN_PI: f64 = 1.144_729_885_849_400_2; let z = (x - self.loc) / self.scale;
-LN_PI - self.scale.ln() - (z * z).ln_1p()
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Laplace {
loc: f64,
scale: f64,
}
impl Laplace {
pub fn new(loc: f64, scale: f64) -> crate::error::FugueResult<Self> {
if !loc.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Laplace",
"Location (loc) must be finite",
crate::error::ErrorCode::InvalidMean,
)
.with_context("loc", format!("{}", loc)));
}
if scale <= 0.0 || !scale.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Laplace",
"Scale must be positive and finite",
crate::error::ErrorCode::InvalidVariance,
)
.with_context("scale", format!("{}", scale))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Laplace { loc, scale })
}
pub fn loc(&self) -> f64 {
self.loc
}
pub fn scale(&self) -> f64 {
self.scale
}
}
impl Distribution<f64> for Laplace {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.scale <= 0.0 {
return f64::NAN;
}
let u: f64 = rng.gen::<f64>() - 0.5;
self.loc - self.scale * u.signum() * (1.0 - 2.0 * u.abs()).ln()
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.scale <= 0.0 || !self.scale.is_finite() || !self.loc.is_finite() || !x.is_finite() {
return f64::NEG_INFINITY;
}
-(2.0 * self.scale).ln() - (x - self.loc).abs() / self.scale
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Weibull {
shape: f64,
scale: f64,
}
impl Weibull {
pub fn new(shape: f64, scale: f64) -> crate::error::FugueResult<Self> {
if shape <= 0.0 || !shape.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Weibull",
"Shape parameter must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("shape", format!("{}", shape))
.with_context("expected", "> 0.0 and finite"));
}
if scale <= 0.0 || !scale.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"Weibull",
"Scale parameter must be positive and finite",
crate::error::ErrorCode::InvalidVariance,
)
.with_context("scale", format!("{}", scale))
.with_context("expected", "> 0.0 and finite"));
}
Ok(Weibull { shape, scale })
}
pub fn shape(&self) -> f64 {
self.shape
}
pub fn scale(&self) -> f64 {
self.scale
}
}
impl Distribution<f64> for Weibull {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.shape <= 0.0 || self.scale <= 0.0 {
return f64::NAN;
}
RDWeibull::new(self.scale, self.shape).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.shape <= 0.0
|| self.scale <= 0.0
|| !self.shape.is_finite()
|| !self.scale.is_finite()
|| !x.is_finite()
{
return f64::NEG_INFINITY;
}
let x = *x;
if x < 0.0 {
return f64::NEG_INFINITY;
}
if x == 0.0 {
return if self.shape > 1.0 {
f64::NEG_INFINITY
} else if self.shape < 1.0 {
f64::INFINITY
} else {
-self.scale.ln()
};
}
self.shape.ln() - self.shape * self.scale.ln() + (self.shape - 1.0) * x.ln()
- (x / self.scale).powf(self.shape)
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ChiSquared {
k: f64,
}
impl ChiSquared {
pub fn new(k: f64) -> crate::error::FugueResult<Self> {
if k <= 0.0 || !k.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"ChiSquared",
"Degrees of freedom must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("k", format!("{}", k))
.with_context("expected", "> 0.0 and finite"));
}
Ok(ChiSquared { k })
}
pub fn k(&self) -> f64 {
self.k
}
}
impl Distribution<f64> for ChiSquared {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.k <= 0.0 {
return f64::NAN;
}
RDChiSquared::new(self.k).unwrap().sample(rng)
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.k <= 0.0 || !self.k.is_finite() || !x.is_finite() {
return f64::NEG_INFINITY;
}
if *x <= 0.0 {
return f64::NEG_INFINITY;
}
let half_k = self.k / 2.0;
-half_k * std::f64::consts::LN_2 - libm::lgamma(half_k) + (half_k - 1.0) * x.ln() - x / 2.0
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct InverseGamma {
shape: f64,
rate: f64,
}
impl InverseGamma {
pub fn new(shape: f64, rate: f64) -> crate::error::FugueResult<Self> {
if shape <= 0.0 || !shape.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"InverseGamma",
"Shape parameter must be positive and finite",
crate::error::ErrorCode::InvalidShape,
)
.with_context("shape", format!("{}", shape))
.with_context("expected", "> 0.0 and finite"));
}
if rate <= 0.0 || !rate.is_finite() {
return Err(crate::error::FugueError::invalid_parameters(
"InverseGamma",
"Rate parameter must be positive and finite",
crate::error::ErrorCode::InvalidRate,
)
.with_context("rate", format!("{}", rate))
.with_context("expected", "> 0.0 and finite"));
}
Ok(InverseGamma { shape, rate })
}
pub fn shape(&self) -> f64 {
self.shape
}
pub fn rate(&self) -> f64 {
self.rate
}
}
impl Distribution<f64> for InverseGamma {
fn sample(&self, rng: &mut dyn RngCore) -> f64 {
if self.shape <= 0.0 || self.rate <= 0.0 {
return f64::NAN;
}
let y = RDGamma::new(self.shape, 1.0 / self.rate)
.unwrap()
.sample(rng);
1.0 / y
}
fn log_prob(&self, x: &f64) -> LogF64 {
if self.shape <= 0.0
|| self.rate <= 0.0
|| !self.shape.is_finite()
|| !self.rate.is_finite()
|| !x.is_finite()
{
return f64::NEG_INFINITY;
}
if *x <= 0.0 {
return f64::NEG_INFINITY;
}
self.shape * self.rate.ln()
- libm::lgamma(self.shape)
- (self.shape + 1.0) * x.ln()
- self.rate / x
}
fn clone_box(&self) -> Box<dyn Distribution<f64>> {
Box::new(*self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct DiscreteUniform {
low: i64,
high: i64,
}
impl DiscreteUniform {
pub fn new(low: i64, high: i64) -> crate::error::FugueResult<Self> {
if high < low {
return Err(crate::error::FugueError::invalid_parameters(
"DiscreteUniform",
"Upper bound must be >= lower bound",
crate::error::ErrorCode::InvalidRange,
)
.with_context("low", format!("{}", low))
.with_context("high", format!("{}", high)));
}
Ok(DiscreteUniform { low, high })
}
pub fn low(&self) -> i64 {
self.low
}
pub fn high(&self) -> i64 {
self.high
}
pub fn len(&self) -> u64 {
u64::try_from(self.count()).unwrap_or(u64::MAX)
}
fn count(&self) -> u128 {
(self.high as i128 - self.low as i128 + 1) as u128
}
fn is_full_i64_range(&self) -> bool {
self.low == i64::MIN && self.high == i64::MAX
}
pub fn is_empty(&self) -> bool {
false
}
}
impl Distribution<i64> for DiscreteUniform {
fn sample(&self, rng: &mut dyn RngCore) -> i64 {
if self.high < self.low {
return self.low;
}
if self.is_full_i64_range() {
return Rng::gen::<i64>(rng);
}
let n = self.count() as u64;
let offset = Rng::gen_range(rng, 0..n) as i128;
(self.low as i128 + offset) as i64
}
fn log_prob(&self, x: &i64) -> LogF64 {
if self.high < self.low {
return f64::NEG_INFINITY;
}
if *x < self.low || *x > self.high {
return f64::NEG_INFINITY;
}
if self.is_full_i64_range() {
-(64.0 * std::f64::consts::LN_2)
} else {
-(self.count() as f64).ln()
}
}
fn clone_box(&self) -> Box<dyn Distribution<i64>> {
Box::new(*self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
#[test]
fn normal_constructor_and_log_prob() {
assert!(Normal::new(0.0, 1.0).is_ok());
assert!(Normal::new(f64::NAN, 1.0).is_err());
assert!(Normal::new(0.0, 0.0).is_err());
let n = Normal::new(0.0, 1.0).unwrap();
assert!(n.log_prob(&0.0).is_finite());
assert_eq!(n.log_prob(&f64::INFINITY), f64::NEG_INFINITY);
}
#[test]
fn uniform_support_and_log_prob() {
assert!(Uniform::new(0.0, 1.0).is_ok());
assert!(Uniform::new(1.0, 0.0).is_err());
let u = Uniform::new(-2.0, 2.0).unwrap();
let lp0 = u.log_prob(&0.0);
assert!(lp0.is_finite());
assert_eq!(u.log_prob(&2.0), f64::NEG_INFINITY);
assert_eq!(u.log_prob(&-2.1), f64::NEG_INFINITY);
}
#[test]
fn lognormal_validation() {
assert!(LogNormal::new(0.0, 1.0).is_ok());
assert!(LogNormal::new(0.0, 0.0).is_err());
let ln = LogNormal::new(0.0, 1.0).unwrap();
assert_eq!(ln.log_prob(&0.0), f64::NEG_INFINITY);
assert!(ln.log_prob(&1.0).is_finite());
}
#[test]
fn exponential_validation() {
assert!(Exponential::new(1.0).is_ok());
assert!(Exponential::new(0.0).is_err());
let e = Exponential::new(2.0).unwrap();
assert_eq!(e.log_prob(&-1.0), f64::NEG_INFINITY);
assert!((e.log_prob(&0.0) - (2.0f64).ln()).abs() < 1e-12);
}
#[test]
fn bernoulli_validation() {
assert!(Bernoulli::new(0.5).is_ok());
assert!(Bernoulli::new(-0.1).is_err());
let b = Bernoulli::new(0.25).unwrap();
assert!((b.log_prob(&true) - (0.25f64).ln()).abs() < 1e-12);
assert!((b.log_prob(&false) - (0.75f64).ln()).abs() < 1e-12);
}
#[test]
fn categorical_validation_and_log_prob() {
assert!(Categorical::new(vec![0.5, 0.5]).is_ok());
assert!(Categorical::new(vec![]).is_err());
assert!(Categorical::new(vec![0.6, 0.5]).is_err());
let c = Categorical::new(vec![0.2, 0.8]).unwrap();
assert!((c.log_prob(&1) - (0.8f64).ln()).abs() < 1e-12);
assert_eq!(c.log_prob(&2), f64::NEG_INFINITY);
}
#[test]
fn beta_validation_and_support() {
assert!(Beta::new(2.0, 3.0).is_ok());
assert!(Beta::new(0.0, 1.0).is_err());
let b = Beta::new(2.0, 5.0).unwrap();
assert_eq!(b.log_prob(&0.0), f64::NEG_INFINITY);
assert_eq!(b.log_prob(&1.0), f64::NEG_INFINITY);
assert!(b.log_prob(&0.5).is_finite());
}
#[test]
fn gamma_validation_and_support() {
assert!(Gamma::new(1.5, 2.0).is_ok());
assert!(Gamma::new(0.0, 2.0).is_err());
assert!(Gamma::new(1.0, 0.0).is_err());
let g = Gamma::new(2.0, 1.0).unwrap();
assert_eq!(g.log_prob(&-1.0), f64::NEG_INFINITY);
assert!(g.log_prob(&1.0).is_finite());
}
#[test]
fn binomial_validation_and_log_prob() {
assert!(Binomial::new(10, 0.5).is_ok());
assert!(Binomial::new(10, 1.5).is_err());
let bi = Binomial::new(5, 0.3).unwrap();
assert_eq!(bi.log_prob(&6), f64::NEG_INFINITY); assert!(bi.log_prob(&3).is_finite());
}
#[test]
fn poisson_validation_and_log_prob() {
assert!(Poisson::new(1.0).is_ok());
assert!(Poisson::new(0.0).is_err());
let p = Poisson::new(3.0).unwrap();
assert!(p.log_prob(&0).is_finite());
assert!(p.log_prob(&5).is_finite());
}
#[test]
fn sampling_basic_sanity() {
let mut rng = StdRng::seed_from_u64(42);
let n = Normal::new(0.0, 1.0).unwrap();
let x = n.sample(&mut rng);
assert!(x.is_finite());
let u = Uniform::new(-1.0, 2.0).unwrap();
let y = u.sample(&mut rng);
assert!((-1.0..2.0).contains(&y));
let b = Bernoulli::new(0.7).unwrap();
let _z = b.sample(&mut rng);
}
#[test]
fn categorical_uniform_constructor() {
let cu = Categorical::uniform(4).unwrap();
assert_eq!(cu.len(), 4);
for &p in cu.probs() {
assert!((p - 0.25).abs() < 1e-12);
}
}
fn close(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn fg06_interior_point_known_answers() {
close(
Normal::new(0.0, 1.0).unwrap().log_prob(&0.0),
-0.9189385332046727,
);
close(
Normal::new(1.0, 2.0).unwrap().log_prob(&2.5),
-1.893335713764618,
);
close(
Uniform::new(-2.0, 2.0).unwrap().log_prob(&1.5),
-1.3862943611198906,
);
close(
LogNormal::new(0.0, 1.0).unwrap().log_prob(&2.0),
-1.8523122207237186,
);
close(
Exponential::new(2.0).unwrap().log_prob(&1.0),
-1.3068528194400546,
);
close(
Beta::new(2.0, 3.0).unwrap().log_prob(&0.5),
0.4054651081081637,
);
close(
Gamma::new(3.0, 2.0).unwrap().log_prob(&1.5),
-0.8027754226637804,
);
close(
Binomial::new(20, 0.3).unwrap().log_prob(&7),
-1.8062926549204255,
);
close(Poisson::new(3.0).unwrap().log_prob(&2), -1.4959226032237254);
close(
Categorical::new(vec![0.2, 0.3, 0.5]).unwrap().log_prob(&2),
-std::f64::consts::LN_2, );
}
#[test]
fn fg07_fg08_fg30_removed_overflow_guards_return_finite() {
close(
Gamma::new(2.0, 1.0).unwrap().log_prob(&800.0),
-793.315388272332,
); close(
Normal::new(0.0, 0.001).unwrap().log_prob(&0.05),
-1244.0111832542225,
); close(
LogNormal::new(0.0, 0.001).unwrap().log_prob(&1.05),
-1184.3000332584572,
); close(
Exponential::new(2.0).unwrap().log_prob(&400.0),
-799.3068528194401,
); }
#[test]
fn fg27_beta_boundaries() {
close(
Beta::new(0.5, 0.5).unwrap().log_prob(&1e-100),
113.98452476385289,
);
close(
Beta::new(1.0, 5.0).unwrap().log_prob(&0.0),
1.6094379124341003,
); close(
Beta::new(3.0, 1.0).unwrap().log_prob(&1.0),
1.0986122886681098,
); assert_eq!(
Beta::new(2.0, 5.0).unwrap().log_prob(&0.0),
f64::NEG_INFINITY
);
assert_eq!(Beta::new(0.5, 3.0).unwrap().log_prob(&0.0), f64::INFINITY);
}
#[test]
fn fg28_binomial_degenerate_p_not_nan() {
let b0 = Binomial::new(5, 0.0).unwrap();
assert!(!b0.log_prob(&0).is_nan());
close(b0.log_prob(&0), 0.0);
assert_eq!(b0.log_prob(&1), f64::NEG_INFINITY);
let b1 = Binomial::new(5, 1.0).unwrap();
assert!(!b1.log_prob(&5).is_nan());
close(b1.log_prob(&5), 0.0);
assert_eq!(b1.log_prob(&3), f64::NEG_INFINITY);
}
#[test]
fn fg29_infallible_constructors() {
assert_eq!(
(Normal::standard().mu(), Normal::standard().sigma()),
(0.0, 1.0)
);
assert_eq!((Uniform::unit().low(), Uniform::unit().high()), (0.0, 1.0));
assert_eq!(
(Beta::uniform_prior().alpha(), Beta::uniform_prior().beta()),
(1.0, 1.0)
);
assert_eq!(Bernoulli::fair().p(), 0.5);
}
#[test]
fn fg53_categorical_cached_cdf_and_revalidate() {
let c = Categorical::new(vec![0.1, 0.2, 0.3, 0.4]).unwrap();
close(c.log_prob(&3), (0.4f64).ln());
assert_eq!(c.log_prob(&4), f64::NEG_INFINITY);
assert!(c.revalidate().is_ok());
let mut rng = StdRng::seed_from_u64(7);
let mut counts = [0usize; 4];
let n = 40_000usize;
for _ in 0..n {
counts[c.sample(&mut rng)] += 1;
}
for (k, &p) in [0.1, 0.2, 0.3, 0.4].iter().enumerate() {
assert!((counts[k] as f64 / n as f64 - p).abs() < 1.1e-2);
}
}
#[test]
fn fg31_new_distributions_interior_point_log_prob() {
close(
StudentT::new(3.0, 1.0, 2.0).unwrap().log_prob(&2.5),
-2.0377365440367736,
);
close(
StudentT::new(5.0, 0.0, 1.0).unwrap().log_prob(&0.0),
-0.9686195890547249,
);
close(
StudentT::new(10.0, 2.0, 0.5).unwrap().log_prob(&1.0),
-2.1013474730076767,
);
close(
Cauchy::new(0.0, 1.0).unwrap().log_prob(&1.5),
-2.3233848821910463,
);
close(
Cauchy::new(2.0, 3.0).unwrap().log_prob(&5.0),
-2.9364893550774553,
);
close(
Laplace::new(0.0, 1.0).unwrap().log_prob(&1.5),
-2.1931471805599454,
);
close(
Laplace::new(1.0, 2.0).unwrap().log_prob(&-0.5),
-2.136294361119891,
);
close(
Weibull::new(1.5, 2.0).unwrap().log_prob(&1.0),
-0.9878090533250272,
);
close(
Weibull::new(2.0, 1.5).unwrap().log_prob(&2.0),
-1.2024136328742159,
);
close(
ChiSquared::new(4.0).unwrap().log_prob(&3.0),
-1.7876820724517808,
);
close(
ChiSquared::new(1.0).unwrap().log_prob(&0.5),
-0.8223649429247004,
);
close(
ChiSquared::new(2.5).unwrap().log_prob(&2.0),
-1.5948753441381327,
);
close(
InverseGamma::new(3.0, 2.0).unwrap().log_prob(&1.5),
-1.5688994046461,
);
close(
InverseGamma::new(2.0, 1.0).unwrap().log_prob(&0.5),
0.07944154167983575,
);
close(
DiscreteUniform::new(-2, 5).unwrap().log_prob(&0),
-2.0794415416798357,
);
}
#[test]
fn fg31_new_distributions_validation_and_support() {
assert!(StudentT::new(0.0, 0.0, 1.0).is_err()); assert!(StudentT::new(3.0, f64::NAN, 1.0).is_err());
assert!(StudentT::new(3.0, 0.0, 0.0).is_err()); assert!(Cauchy::new(0.0, -1.0).is_err());
assert!(Cauchy::new(f64::INFINITY, 1.0).is_err());
assert!(Laplace::new(0.0, 0.0).is_err());
assert!(Weibull::new(0.0, 1.0).is_err());
assert!(Weibull::new(1.0, 0.0).is_err());
assert!(ChiSquared::new(0.0).is_err());
assert!(ChiSquared::new(-1.0).is_err());
assert!(InverseGamma::new(0.0, 1.0).is_err());
assert!(InverseGamma::new(1.0, 0.0).is_err());
assert!(DiscreteUniform::new(5, 4).is_err());
assert_eq!(
Weibull::new(2.0, 1.0).unwrap().log_prob(&-0.5),
f64::NEG_INFINITY
);
assert_eq!(
Weibull::new(2.0, 1.0).unwrap().log_prob(&0.0),
f64::NEG_INFINITY
); close(
Weibull::new(1.0, 2.0).unwrap().log_prob(&0.0),
-(2.0f64).ln(),
); assert_eq!(
Weibull::new(0.5, 1.0).unwrap().log_prob(&0.0),
f64::INFINITY
); assert_eq!(
ChiSquared::new(3.0).unwrap().log_prob(&0.0),
f64::NEG_INFINITY
);
assert_eq!(
ChiSquared::new(3.0).unwrap().log_prob(&-1.0),
f64::NEG_INFINITY
);
assert_eq!(
InverseGamma::new(2.0, 1.0).unwrap().log_prob(&0.0),
f64::NEG_INFINITY
);
assert!(StudentT::new(2.0, 0.0, 1.0)
.unwrap()
.log_prob(&-100.0)
.is_finite());
assert!(Cauchy::new(0.0, 1.0).unwrap().log_prob(&1e6).is_finite());
assert!(Laplace::new(0.0, 1.0).unwrap().log_prob(&-42.0).is_finite());
let du = DiscreteUniform::new(1, 6).unwrap();
assert_eq!(du.log_prob(&0), f64::NEG_INFINITY);
assert_eq!(du.log_prob(&7), f64::NEG_INFINITY);
assert!(du.log_prob(&1).is_finite());
assert!(du.log_prob(&6).is_finite());
assert_eq!(du.len(), 6);
}
#[test]
fn fg31_new_distributions_moment_sanity() {
let mut rng = StdRng::seed_from_u64(31);
let n = 60_000usize;
fn mean_of(d: &impl Distribution<f64>, rng: &mut StdRng, n: usize) -> f64 {
(0..n).map(|_| d.sample(rng)).sum::<f64>() / n as f64
}
let t = StudentT::new(6.0, 1.0, 2.0).unwrap();
assert!((mean_of(&t, &mut rng, n) - 1.0).abs() < 0.1);
let lap = Laplace::new(0.0, 2.0).unwrap();
let lap_samples: Vec<f64> = (0..n).map(|_| lap.sample(&mut rng)).collect();
let lap_mean = lap_samples.iter().sum::<f64>() / n as f64;
let lap_var = lap_samples
.iter()
.map(|x| (x - lap_mean).powi(2))
.sum::<f64>()
/ n as f64;
assert!(lap_mean.abs() < 0.1);
assert!((lap_var - 8.0).abs() < 0.6);
let w = Weibull::new(2.0, 1.5).unwrap();
assert!((mean_of(&w, &mut rng, n) - 1.3293403881791368).abs() < 0.05);
let c = ChiSquared::new(4.0).unwrap();
let c_samples: Vec<f64> = (0..n).map(|_| c.sample(&mut rng)).collect();
let c_mean = c_samples.iter().sum::<f64>() / n as f64;
let c_var = c_samples.iter().map(|x| (x - c_mean).powi(2)).sum::<f64>() / n as f64;
assert!((c_mean - 4.0).abs() < 0.1);
assert!((c_var - 8.0).abs() < 0.6);
let ig = InverseGamma::new(4.0, 3.0).unwrap();
assert!((mean_of(&ig, &mut rng, n) - 1.0).abs() < 0.05);
let cau = Cauchy::new(2.0, 1.0).unwrap();
let mut cau_samples: Vec<f64> = (0..n).map(|_| cau.sample(&mut rng)).collect();
cau_samples.sort_by(|a, b| a.partial_cmp(b).unwrap());
let median = cau_samples[n / 2];
assert!((median - 2.0).abs() < 0.1);
let du = DiscreteUniform::new(1, 6).unwrap();
let du_samples: Vec<i64> = (0..n).map(|_| du.sample(&mut rng)).collect();
assert!(du_samples.iter().all(|&k| (1..=6).contains(&k)));
let du_mean = du_samples.iter().map(|&k| k as f64).sum::<f64>() / n as f64;
assert!((du_mean - 3.5).abs() < 0.05);
}
#[test]
fn fg55_validate_rejects_invalid_parameters() {
use crate::error::{ErrorCode, Validate};
assert_eq!(
LogNormal {
mu: 0.0,
sigma: 0.0
}
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidVariance
);
assert_eq!(
Binomial { n: 10, p: 1.5 }.validate().unwrap_err().code(),
ErrorCode::InvalidProbability
);
assert_eq!(
Poisson { lambda: -1.0 }.validate().unwrap_err().code(),
ErrorCode::InvalidRate
);
assert_eq!(
StudentT {
df: 0.0,
loc: 0.0,
scale: 1.0
}
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidShape
);
assert_eq!(
Cauchy {
loc: 0.0,
scale: -1.0
}
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidVariance
);
assert_eq!(
Laplace {
loc: f64::NAN,
scale: 1.0
}
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidMean
);
assert_eq!(
Weibull {
shape: -2.0,
scale: 1.0
}
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidShape
);
assert_eq!(
ChiSquared { k: 0.0 }.validate().unwrap_err().code(),
ErrorCode::InvalidShape
);
assert_eq!(
InverseGamma {
shape: 2.0,
rate: -1.0
}
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidRate
);
assert_eq!(
DiscreteUniform { low: 5, high: 1 }
.validate()
.unwrap_err()
.code(),
ErrorCode::InvalidRange
);
}
#[test]
fn discrete_uniform_full_i64_range_samples_and_scores() {
let du = DiscreteUniform::new(i64::MIN, i64::MAX).unwrap();
assert_eq!(du.len(), u64::MAX);
assert!(!du.is_empty());
let mut rng = StdRng::seed_from_u64(0xF017_2026);
let mut saw_negative = false;
let mut saw_positive = false;
for _ in 0..10_000 {
let x = du.sample(&mut rng);
assert!(du.log_prob(&x).is_finite());
saw_negative |= x < 0;
saw_positive |= x > 0;
}
assert!(
saw_negative && saw_positive,
"full-range sampler is not uniform"
);
let expected = -(64.0 * std::f64::consts::LN_2);
for &x in &[i64::MIN, -1_000_000_i64, -1, 0, 1, 1_000_000_i64, i64::MAX] {
let lp = du.log_prob(&x);
assert!(
(lp - expected).abs() < 1e-12,
"full-range log_prob({x}) = {lp}, expected {expected}"
);
}
}
#[test]
fn discrete_uniform_near_full_ranges_are_exact() {
let mut rng = StdRng::seed_from_u64(0xBEEF_2026);
for du in [
DiscreteUniform::new(i64::MIN, i64::MAX - 1).unwrap(),
DiscreteUniform::new(i64::MIN + 1, i64::MAX).unwrap(),
] {
assert_eq!(du.len(), u64::MAX);
let expected = -((u64::MAX as f64).ln());
for _ in 0..2_000 {
let x = du.sample(&mut rng);
assert!(du.log_prob(&x).is_finite());
}
close(du.log_prob(&0), expected);
let excluded = if du.high() == i64::MAX - 1 {
i64::MAX
} else {
i64::MIN
};
assert_eq!(du.log_prob(&excluded), f64::NEG_INFINITY);
}
}
}