use antecedent_core::CausalRng;
#[must_use]
pub fn standard_normal(rng: &mut CausalRng) -> f64 {
let (z, _) = standard_normal_pair(rng);
z
}
#[must_use]
pub fn standard_normal_pair(rng: &mut CausalRng) -> (f64, f64) {
let u1 = rng.next_f64().clamp(1e-12, 1.0);
let u2 = rng.next_f64();
let r = (-2.0 * u1.ln()).sqrt();
let theta = std::f64::consts::TAU * u2;
(r * theta.cos(), r * theta.sin())
}
pub fn fill_standard_normal(rng: &mut CausalRng, out: &mut [f64]) {
let mut i = 0;
while i < out.len() {
let (z0, z1) = standard_normal_pair(rng);
out[i] = z0;
i += 1;
if i < out.len() {
out[i] = z1;
i += 1;
}
}
}
#[must_use]
pub fn unbiased_index(rng: &mut CausalRng, n: usize) -> usize {
assert!(n > 0, "unbiased_index requires n > 0");
let n64 = u64::try_from(n).expect("usize fits u64");
let limit = u64::MAX - (u64::MAX % n64);
loop {
let v = rng.next_u64();
if v < limit {
return usize::try_from(v % n64).expect("remainder < n fits usize");
}
}
}
pub fn shuffle<T>(rng: &mut CausalRng, items: &mut [T]) {
for i in (1..items.len()).rev() {
let j = unbiased_index(rng, i + 1);
items.swap(i, j);
}
}
#[must_use]
pub fn sample_categorical(rng: &mut CausalRng, weights: &[f64]) -> usize {
if weights.is_empty() {
return 0;
}
let sum: f64 = weights.iter().copied().sum();
if sum.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
return 0;
}
let u = rng.next_f64() * sum;
let mut acc = 0.0;
for (i, &w) in weights.iter().enumerate() {
acc += w.max(0.0);
if u < acc {
return i;
}
}
weights.len() - 1
}
#[must_use]
pub fn categorical_from_u(u: f64, probs: &[f64]) -> usize {
if probs.is_empty() {
return 0;
}
let sum: f64 = probs.iter().copied().sum::<f64>().max(f64::EPSILON);
let target = u.clamp(0.0, 1.0) * sum;
let mut acc = 0.0;
for (i, &p) in probs.iter().enumerate() {
acc += p.max(0.0);
if target <= acc {
return i;
}
}
probs.len() - 1
}