use crate::modeling::sampler::{DurationSampler, PendingDuration};
use crate::primitive::time::SimTime;
use rand::{Rng, RngExt};
pub struct ChoiceSampler {
cdf: Vec<u64>, values: Vec<Box<dyn DurationSampler>>, total_weight: u64, }
impl DurationSampler for ChoiceSampler {
fn sample(&mut self, rng: &mut dyn Rng, current_tick: SimTime) -> PendingDuration {
let r = rng.random_range(0..self.total_weight);
let idx = self.cdf.partition_point(|&x| x <= r) - 1;
self.values[idx].sample(rng, current_tick)
}
}
impl ChoiceSampler {
pub fn new(histogram: impl IntoIterator<Item = (Box<dyn DurationSampler>, u64)>) -> Self {
let mut cdf = Vec::new();
let mut values = Vec::new();
let mut current_sum = 0;
cdf.push(0);
for (duration, weight) in histogram.into_iter() {
current_sum += weight;
cdf.push(current_sum);
values.push(duration);
}
assert!(
!values.is_empty(),
"ChoiceSampler must have at least one sampler with positive weight"
);
assert!(current_sum > 0, "Total weight must be greater than 0");
Self {
cdf,
values,
total_weight: current_sum,
}
}
pub fn new_as_uniform(histogram: impl IntoIterator<Item = Box<dyn DurationSampler>>) -> Self {
ChoiceSampler::new(histogram.into_iter().map(|s| (s, 1)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modeling::sampler::CombinatorExt;
use crate::modeling::sampler::instance::ConstantSampler;
use rand::SeedableRng;
use rand::rngs::SmallRng;
use std::collections::HashMap;
#[test]
fn test_choice_sampler_weighted() {
let mut rng = SmallRng::seed_from_u64(2);
let s1 = ConstantSampler::new(10.0);
let s2 = ConstantSampler::new(20.0);
let s3 = ConstantSampler::new(30.0);
let mut sampler =
ChoiceSampler::new(vec![(s1.boxed(), 1), (s2.boxed(), 2), (s3.boxed(), 1)]);
let mut results: HashMap<String, usize> = HashMap::new();
for _ in 0..1000 {
let sample = sampler.sample(&mut rng, SimTime::from_ticks(0));
let entry = results
.entry(format!("{:<.2}", sample.raw_value()))
.or_insert(0);
*entry += 1;
}
let count_10 = *results.get(&format!("{:<.2}", 10.0)).unwrap_or(&0);
let count_20 = *results.get(&format!("{:<.2}", 20.0)).unwrap_or(&0);
let count_30 = *results.get(&format!("{:<.2}", 30.0)).unwrap_or(&0);
assert!(count_10 > 200 && count_10 < 300);
assert!(count_20 > 450 && count_20 < 550);
assert!(count_30 > 200 && count_30 < 300);
}
#[test]
fn test_choice_sampler_uniform() {
let mut rng = SmallRng::seed_from_u64(2);
let s1 = ConstantSampler::new(10.0);
let s2 = ConstantSampler::new(20.0);
let s3 = ConstantSampler::new(30.0);
let mut sampler = ChoiceSampler::new_as_uniform(vec![s1.boxed(), s2.boxed(), s3.boxed()]);
let mut results: HashMap<String, usize> = HashMap::new();
for _ in 0..1000 {
let sample = sampler.sample(&mut rng, SimTime::from_ticks(0));
let entry = results
.entry(format!("{:<.2}", sample.raw_value()))
.or_insert(0);
*entry += 1_usize;
}
let count_10 = *results.get(&format!("{:<.2}", 10.0)).unwrap_or(&0);
let count_20 = *results.get(&format!("{:<.2}", 20.0)).unwrap_or(&0);
let count_30 = *results.get(&format!("{:<.2}", 30.0)).unwrap_or(&0);
assert!(count_10 > 280 && count_10 < 380);
assert!(count_20 > 280 && count_20 < 380);
assert!(count_30 > 280 && count_30 < 380);
}
#[test]
#[should_panic(expected = "ChoiceSampler must have at least one sampler")]
fn test_choice_sampler_empty_histogram() {
let _sampler = ChoiceSampler::new(vec![]);
}
#[test]
#[should_panic(expected = "Total weight must be greater than 0")]
fn test_choice_sampler_zero_total_weight() {
let s1 = ConstantSampler::new(10.0);
let _sampler = ChoiceSampler::new(vec![(s1.boxed(), 0)]);
}
}