#![allow(
clippy::indexing_slicing,
reason = "alias-table construction uses closed-form indices 0..count matching ur-rs"
)]
use alloc::vec::Vec;
use super::Xoshiro256;
#[derive(Debug)]
pub(crate) struct Sampler {
aliases: Vec<u32>,
probs: Vec<f64>,
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
reason = "alias method uses f64 probabilities; table sizes match ur-rs exactly"
)]
impl Sampler {
pub(crate) fn new(mut weights: Vec<f64>) -> Self {
assert!(
!weights.iter().any(|&p| p < 0.0),
"negative probability encountered"
);
let summed = weights.iter().sum::<f64>();
assert!(summed > 0.0, "probabilities don't sum to a positive value");
let count = weights.len();
for w in &mut weights {
*w *= count as f64 / summed;
}
let (mut s, mut l): (Vec<usize>, Vec<usize>) = (1..=count)
.map(|j| count - j)
.partition(|&j| weights[j] < 1.0);
let mut probs: Vec<f64> = alloc::vec![0.0; count];
let mut aliases: Vec<u32> = alloc::vec![0; count];
while !s.is_empty() && !l.is_empty() {
let a = s.remove(s.len() - 1);
let g = l.remove(l.len() - 1);
probs[a] = weights[a];
aliases[a] = g as u32;
weights[g] += weights[a] - 1.0;
if weights[g] < 1.0 {
s.push(g);
} else {
l.push(g);
}
}
while !l.is_empty() {
let g = l.remove(l.len() - 1);
probs[g] = 1.0;
}
while !s.is_empty() {
let a = s.remove(s.len() - 1);
probs[a] = 1.0;
}
Self { aliases, probs }
}
pub(crate) fn next(&self, xoshiro: &mut Xoshiro256) -> u32 {
let n = self.probs.len();
let i = xoshiro.next_index(n);
let r2 = xoshiro.next_double();
if r2 < self.probs[i] {
i as u32
} else {
self.aliases[i]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sampler() {
let weights = vec![1.0, 2.0, 4.0, 8.0];
let mut xoshiro = Xoshiro256::from("Wolf");
let sampler = Sampler::new(weights);
let expected = [
3, 3, 3, 3, 3, 3, 3, 0, 2, 3, 3, 3, 3, 1, 2, 2, 1, 3, 3, 2, 3, 3, 1, 1, 2, 1, 1, 3, 1,
3,
];
for e in expected {
assert_eq!(sampler.next(&mut xoshiro), e);
}
}
}