use std::convert::Infallible;
use getrandom04::SysRng;
use rand010::rand_core::UnwrapErr;
use rand010::{RngExt as _, SeedableRng, TryCryptoRng, TryRng};
use rand_chacha010::ChaCha20Rng;
use rand_distr06::{Distribution as _, Exp, Normal};
pub use rand010::CryptoRng;
pub(crate) enum RngSource {
Os(UnwrapErr<SysRng>),
Custom(Box<dyn CryptoRng + Send>),
}
impl Default for RngSource {
fn default() -> Self {
RngSource::Os(UnwrapErr(SysRng))
}
}
impl RngSource {
pub(crate) fn seeded(seed: [u8; 32]) -> Self {
RngSource::Custom(Box::new(ChaCha20Rng::from_seed(seed)))
}
pub(crate) fn custom<R: CryptoRng + Send + 'static>(rng: R) -> Self {
RngSource::Custom(Box::new(rng))
}
}
impl std::fmt::Debug for RngSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RngSource::Os(_) => f.write_str("RngSource::Os"),
RngSource::Custom(_) => f.write_str("RngSource::Custom"),
}
}
}
impl TryRng for RngSource {
type Error = Infallible;
fn try_next_u32(&mut self) -> Result<u32, Infallible> {
match self {
RngSource::Os(rng) => rng.try_next_u32(),
RngSource::Custom(rng) => rng.try_next_u32(),
}
}
fn try_next_u64(&mut self) -> Result<u64, Infallible> {
match self {
RngSource::Os(rng) => rng.try_next_u64(),
RngSource::Custom(rng) => rng.try_next_u64(),
}
}
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Infallible> {
match self {
RngSource::Os(rng) => rng.try_fill_bytes(dest),
RngSource::Custom(rng) => rng.try_fill_bytes(dest),
}
}
}
impl TryCryptoRng for RngSource {}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Sampling {
Uniform,
Poisson {
mean: f64,
},
Normal {
mean: f64,
std_dev: f64,
},
}
const MAX_REJECTION_RETRIES: usize = 1000;
pub(crate) fn sample_bounded(rng: &mut RngSource, sampling: &Sampling, min: f64, max: f64) -> f64 {
assert!(
min <= max && min.is_finite(),
"invalid sampling bounds: min = {min}, max = {max}"
);
if min == max {
return min;
}
match sampling {
Sampling::Uniform => {
assert!(max.is_finite(), "uniform sampling requires a finite max");
rng.random_range(min..=max)
}
Sampling::Poisson { mean } => {
assert!(*mean > 0.0, "poisson sampling requires mean > 0");
let exp = Exp::new(1.0 / mean).expect("mean checked positive above");
if max.is_infinite() {
return min + exp.sample(rng);
}
reject_into_bounds(
rng,
min,
max,
|rng| exp.sample(rng),
|rng| rng.random_range(min..=max),
)
}
Sampling::Normal { mean, std_dev } => {
assert!(*std_dev > 0.0, "normal sampling requires std_dev > 0");
let normal = Normal::new(*mean, *std_dev).expect("std_dev checked positive above");
reject_into_bounds(
rng,
min,
max,
|rng| normal.sample(rng),
|rng| {
if max.is_finite() {
rng.random_range(min..=max)
} else {
let half =
Normal::new(0.0, *std_dev).expect("std_dev checked positive above");
min + half.sample(rng).abs()
}
},
)
}
}
}
fn reject_into_bounds(
rng: &mut RngSource,
min: f64,
max: f64,
mut draw: impl FnMut(&mut RngSource) -> f64,
fallback: impl FnOnce(&mut RngSource) -> f64,
) -> f64 {
for _ in 0..MAX_REJECTION_RETRIES {
let sample = draw(rng);
if sample >= min && sample <= max {
return sample;
}
}
tracing::debug!(
"rejection sampling exhausted {MAX_REJECTION_RETRIES} retries for bounds \
[{min}, {max}]; using the termination fallback"
);
fallback(rng)
}
pub(crate) fn validate_sampling(sampling: &Sampling) {
match sampling {
Sampling::Uniform => {}
Sampling::Poisson { mean } => {
assert!(
mean.is_finite() && *mean > 0.0,
"poisson sampling requires a finite mean > 0, got {mean}"
);
}
Sampling::Normal { mean, std_dev } => {
assert!(mean.is_finite(), "normal sampling requires a finite mean");
assert!(
std_dev.is_finite() && *std_dev > 0.0,
"normal sampling requires a finite std_dev > 0, got {std_dev}"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seeded() -> RngSource {
RngSource::seeded([7u8; 32])
}
#[test]
fn uniform_stays_in_bounds() {
let mut rng = seeded();
for _ in 0..10_000 {
let s = sample_bounded(&mut rng, &Sampling::Uniform, 5.0, 10.0);
assert!((5.0..=10.0).contains(&s));
}
}
#[test]
fn poisson_rejection_stays_in_bounds() {
let mut rng = seeded();
for _ in 0..10_000 {
let s = sample_bounded(&mut rng, &Sampling::Poisson { mean: 3.0 }, 0.0, 4.0);
assert!((0.0..=4.0).contains(&s));
}
}
#[test]
fn normal_rejection_stays_in_bounds() {
let mut rng = seeded();
let sampling = Sampling::Normal {
mean: 5.0,
std_dev: 3.0,
};
for _ in 0..10_000 {
let s = sample_bounded(&mut rng, &sampling, 2.0, 8.0);
assert!((2.0..=8.0).contains(&s));
}
}
#[test]
fn pathological_config_terminates_via_fallback() {
let mut rng = seeded();
let sampling = Sampling::Normal {
mean: 1_000.0,
std_dev: 1.0,
};
let s = sample_bounded(&mut rng, &sampling, 0.0, 1.0);
assert!((0.0..=1.0).contains(&s));
}
#[test]
fn min_only_exponential_is_memoryless_shift() {
let mut rng = seeded();
let sampling = Sampling::Poisson { mean: 1.0 };
let n = 20_000;
let mut sum = 0.0;
let mut at_min = 0usize;
for _ in 0..n {
let s = sample_bounded(&mut rng, &sampling, 60.0, f64::INFINITY);
assert!(s >= 60.0);
sum += s;
if s == 60.0 {
at_min += 1;
}
}
let mean = sum / n as f64;
assert!(
(60.9..61.1).contains(&mean),
"min-truncated exponential mean should be min + mean, got {mean}"
);
assert!(at_min == 0, "continuous sampler put point mass at min");
}
#[test]
fn min_only_normal_fallback_is_not_constant() {
let mut rng = seeded();
let sampling = Sampling::Normal {
mean: -1_000.0,
std_dev: 1.0,
};
let draws: Vec<f64> = (0..50)
.map(|_| sample_bounded(&mut rng, &sampling, 5.0, f64::INFINITY))
.collect();
assert!(draws.iter().all(|&s| s >= 5.0));
assert!(
draws.iter().any(|&s| s != draws[0]),
"unbounded-max fallback returned a constant — a boundary spike"
);
}
#[test]
fn degenerate_bounds_return_min() {
let mut rng = seeded();
assert_eq!(
sample_bounded(&mut rng, &Sampling::Poisson { mean: 1.0 }, 2.0, 2.0),
2.0
);
}
#[test]
fn seeded_sampling_is_deterministic() {
let mut a = RngSource::seeded([1u8; 32]);
let mut b = RngSource::seeded([1u8; 32]);
for _ in 0..100 {
assert_eq!(
sample_bounded(&mut a, &Sampling::Poisson { mean: 5.0 }, 0.0, 20.0),
sample_bounded(&mut b, &Sampling::Poisson { mean: 5.0 }, 0.0, 20.0),
);
}
}
}