use crate::foundation::{AlgoError, Result};
use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng};
use rand_distr::{Distribution, LogNormal, Normal, Triangular};
use statrs::distribution::{ContinuousCDF, Normal as StatrsNormal};
pub fn seeded_rng(seed: u64) -> StdRng {
StdRng::seed_from_u64(seed)
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum Sampler {
Uniform { lo: f64, hi: f64 },
Normal { mean: f64, std_dev: f64 },
LogNormal { mean: f64, std_dev: f64 },
Triangular { min: f64, mode: f64, max: f64 },
TruncatedNormal {
mean: f64,
std_dev: f64,
lo: f64,
hi: f64,
},
}
impl Sampler {
pub fn new_uniform(lo: f64, hi: f64) -> Result<Sampler> {
if !(lo.is_finite() && hi.is_finite()) || lo >= hi {
return Err(AlgoError::InvalidArgument(
"sampling: uniform needs finite lo < hi".to_string(),
));
}
Ok(Sampler::Uniform { lo, hi })
}
pub fn new_normal(mean: f64, std_dev: f64) -> Result<Sampler> {
if !(mean.is_finite() && std_dev.is_finite()) || std_dev <= 0.0 {
return Err(AlgoError::InvalidArgument(
"sampling: normal needs finite mean and std_dev > 0".to_string(),
));
}
Ok(Sampler::Normal { mean, std_dev })
}
pub fn new_lognormal(mean: f64, std_dev: f64) -> Result<Sampler> {
if !(mean.is_finite() && std_dev.is_finite()) || std_dev <= 0.0 {
return Err(AlgoError::InvalidArgument(
"sampling: lognormal needs finite mean and std_dev > 0 (log-space)".to_string(),
));
}
Ok(Sampler::LogNormal { mean, std_dev })
}
pub fn new_triangular(min: f64, mode: f64, max: f64) -> Result<Sampler> {
if !(min.is_finite()
&& mode.is_finite()
&& max.is_finite()
&& min < max
&& (min..=max).contains(&mode))
{
return Err(AlgoError::InvalidArgument(
"sampling: triangular needs finite min < max with min <= mode <= max".to_string(),
));
}
Ok(Sampler::Triangular { min, mode, max })
}
pub fn new_truncated_normal(mean: f64, std_dev: f64, lo: f64, hi: f64) -> Result<Sampler> {
if !(mean.is_finite() && std_dev.is_finite() && lo.is_finite() && hi.is_finite())
|| std_dev <= 0.0
|| lo >= hi
{
return Err(AlgoError::InvalidArgument(
"sampling: truncated normal needs finite mean, std_dev > 0 and lo < hi".to_string(),
));
}
Ok(Sampler::TruncatedNormal {
mean,
std_dev,
lo,
hi,
})
}
pub fn clamped(self, lo: f64, hi: f64) -> Result<Clamped> {
Clamped::new(self, lo, hi)
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
match *self {
Sampler::Uniform { lo, hi } => rng.random_range(lo..hi),
Sampler::Normal { mean, std_dev } => {
Normal::new(mean, std_dev).expect("validated").sample(rng)
}
Sampler::LogNormal { mean, std_dev } => LogNormal::new(mean, std_dev)
.expect("validated")
.sample(rng),
Sampler::Triangular { min, mode, max } => {
Triangular::new(min, max, mode)
.expect("validated")
.sample(rng)
}
Sampler::TruncatedNormal {
mean,
std_dev,
lo,
hi,
} => sample_truncated_normal(mean, std_dev, lo, hi, rng),
}
}
pub fn sample_n<R: Rng>(&self, n: usize, rng: &mut R) -> Vec<f64> {
(0..n).map(|_| self.sample(rng)).collect()
}
}
fn sample_truncated_normal<R: Rng>(mean: f64, std_dev: f64, lo: f64, hi: f64, rng: &mut R) -> f64 {
let snorm = StatrsNormal::new(0.0, 1.0).expect("standard normal");
let a = snorm.cdf((lo - mean) / std_dev);
let b = snorm.cdf((hi - mean) / std_dev);
if b <= a {
return mean.clamp(lo, hi);
}
let u = rng.random_range(a..b);
(mean + std_dev * snorm.inverse_cdf(u)).clamp(lo, hi)
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Clamped {
inner: Sampler,
lo: f64,
hi: f64,
}
impl Clamped {
pub fn new(inner: Sampler, lo: f64, hi: f64) -> Result<Clamped> {
if !(lo.is_finite() && hi.is_finite()) || lo >= hi {
return Err(AlgoError::InvalidArgument(
"sampling: clamp needs finite lo < hi".to_string(),
));
}
Ok(Clamped { inner, lo, hi })
}
pub fn sample<R: Rng>(&self, rng: &mut R) -> f64 {
self.inner.sample(rng).clamp(self.lo, self.hi)
}
pub fn sample_n<R: Rng>(&self, n: usize, rng: &mut R) -> Vec<f64> {
(0..n).map(|_| self.sample(rng)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constructors_validate() {
assert!(Sampler::new_uniform(1.0, 0.0).is_err());
assert!(Sampler::new_uniform(0.0, 1.0).is_ok());
assert!(Sampler::new_normal(0.0, -1.0).is_err());
assert!(Sampler::new_normal(0.0, 1.0).is_ok());
assert!(Sampler::new_lognormal(0.0, 0.0).is_err());
assert!(Sampler::new_triangular(0.0, 5.0, 1.0).is_err()); assert!(Sampler::new_triangular(0.0, 0.5, 1.0).is_ok());
}
#[test]
fn seeded_rng_is_reproducible() {
let s = Sampler::new_normal(10.0, 2.0).unwrap();
let a = s.sample_n(100, &mut seeded_rng(42));
let b = s.sample_n(100, &mut seeded_rng(42));
assert_eq!(a, b, "same seed must reproduce the stream");
let c = s.sample_n(100, &mut seeded_rng(43));
assert_ne!(a, c, "a different seed should differ");
}
#[test]
fn uniform_stays_in_range() {
let s = Sampler::new_uniform(-3.0, 7.0).unwrap();
let mut rng = seeded_rng(1);
for v in s.sample_n(1000, &mut rng) {
assert!((-3.0..7.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn triangular_stays_within_support() {
let s = Sampler::new_triangular(2.0, 4.0, 10.0).unwrap();
let mut rng = seeded_rng(7);
for v in s.sample_n(1000, &mut rng) {
assert!((2.0..=10.0).contains(&v), "out of support: {v}");
}
}
#[test]
fn normal_mean_is_approximately_recovered() {
let s = Sampler::new_normal(5.0, 1.0).unwrap();
let mut rng = seeded_rng(2024);
let xs = s.sample_n(20_000, &mut rng);
let m: f64 = xs.iter().sum::<f64>() / xs.len() as f64;
assert!((m - 5.0).abs() < 0.05, "sample mean {m} far from 5.0");
}
#[test]
fn lognormal_is_positive() {
let s = Sampler::new_lognormal(0.0, 0.5).unwrap();
let mut rng = seeded_rng(9);
for v in s.sample_n(1000, &mut rng) {
assert!(v > 0.0, "lognormal must be positive: {v}");
}
}
#[test]
fn truncated_normal_validates() {
assert!(Sampler::new_truncated_normal(0.0, 0.0, -1.0, 1.0).is_err()); assert!(Sampler::new_truncated_normal(0.0, 1.0, 1.0, -1.0).is_err()); assert!(Sampler::new_truncated_normal(0.0, 1.0, f64::NAN, 1.0).is_err());
assert!(Sampler::new_truncated_normal(0.0, 1.0, -2.0, 2.0).is_ok());
}
#[test]
fn truncated_normal_stays_in_bounds() {
let s = Sampler::new_truncated_normal(0.0, 1.0, -0.5, 0.5).unwrap();
let mut rng = seeded_rng(11);
for v in s.sample_n(5000, &mut rng) {
assert!((-0.5..=0.5).contains(&v), "out of bounds: {v}");
}
}
#[test]
fn truncated_normal_symmetric_mean_is_centre() {
let s = Sampler::new_truncated_normal(0.0, 1.0, -1.5, 1.5).unwrap();
let mut rng = seeded_rng(2024);
let xs = s.sample_n(40_000, &mut rng);
let m: f64 = xs.iter().sum::<f64>() / xs.len() as f64;
assert!(m.abs() < 0.03, "truncated mean {m} not ~0");
}
#[test]
fn truncated_narrower_than_clamped() {
let mut rng = seeded_rng(5);
let clamped = Sampler::new_normal(0.0, 1.0)
.unwrap()
.clamped(-0.25, 0.25)
.unwrap();
let n_at_bound = clamped
.sample_n(2000, &mut rng)
.iter()
.filter(|v| (**v - 0.25).abs() < 1e-12 || (**v + 0.25).abs() < 1e-12)
.count();
assert!(n_at_bound > 100, "clamping should pile mass at bounds");
}
#[test]
fn clamped_validates_and_limits_any_sampler() {
assert!(Sampler::new_uniform(0.0, 10.0)
.unwrap()
.clamped(1.0, 1.0)
.is_err()); let s = Sampler::new_uniform(-100.0, 100.0)
.unwrap()
.clamped(-2.0, 3.0)
.unwrap();
let mut rng = seeded_rng(3);
for v in s.sample_n(2000, &mut rng) {
assert!((-2.0..=3.0).contains(&v), "clamp escaped: {v}");
}
}
}