Skip to main content

des_sim/modeling/sampler/instance/
empirical.rs

1//! The `empirical` module provides the `EmpiricalSampler`, a `DurationSampler`
2//! that draws samples from a discrete empirical distribution.
3//!
4//! This sampler allows users to define a set of durations, each with an associated
5//! weight, and then samples from this distribution using inverse transform sampling.
6//! It supports both custom weighted distributions and uniform selection.
7
8use crate::modeling::sampler::{DurationSampler, PendingDuration};
9use crate::primitive::time::{Duration, SimTime};
10use rand::{Rng, RngExt};
11
12/// A sampler that draws from a discrete empirical distribution.
13///
14/// Samples are generated using inverse transform sampling, where values are selected
15/// based on their relative weights.
16#[derive(Debug, Clone)]
17pub struct EmpiricalSampler {
18    cdf: Vec<u64>,         // Cumulative distribution function (boundary values)
19    values: Vec<Duration>, // Corresponding values
20    total_weight: u64,     // Sum of all weights
21}
22
23impl DurationSampler for EmpiricalSampler {
24    fn sample(&mut self, rng: &mut dyn Rng, _current_tick: SimTime) -> PendingDuration {
25        // Generate a random number in the range [0, total_weight)
26        let r = rng.random_range(0..self.total_weight);
27
28        // Find the index of the interval to which r belongs using binary search.
29        // `partition_point` returns the index of the first element that satisfies `!predicate`.
30        // Since we want the last index where `x <= r`, we search for `x <= r` and subtract 1.
31        let idx = self.cdf.partition_point(|&x| x <= r) - 1;
32
33        self.values[idx].into()
34    }
35}
36
37impl EmpiricalSampler {
38    /// Constructs an `EmpiricalSampler` from a collection of `(Duration, Weight)` pairs.
39    pub fn new(histogram: impl IntoIterator<Item = (Duration, u64)>) -> Self {
40        // Converted in advance so that it can be sampled using Inverse Transform Sampling
41        // from the empirical distribution
42        let mut cdf = Vec::new();
43        let mut values = Vec::new();
44        let mut current_sum = 0;
45
46        // The first boundary is 0
47        cdf.push(0);
48        for (duration, weight) in histogram.into_iter() {
49            current_sum += weight;
50            cdf.push(current_sum);
51            values.push(duration);
52        }
53
54        assert!(
55            !values.is_empty(),
56            "EmpiricalSampler must have at least one sampler"
57        );
58        assert!(current_sum > 0, "Total weight must be greater than 0");
59
60        Self {
61            cdf,
62            values,
63            total_weight: current_sum,
64        }
65    }
66
67    /// Constructs an `EmpiricalSampler` with uniform weight (1) for all provided durations.
68    pub fn new_as_uniform(histogram: impl IntoIterator<Item = Duration>) -> Self {
69        EmpiricalSampler::new(histogram.into_iter().map(|d| (d, 1)))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::modeling::sampler::instance::ExponentialSampler;
77    use crate::primitive::time::Duration;
78    use rand::SeedableRng;
79    use rand::rngs::SmallRng;
80    use std::collections::HashMap;
81
82    #[test]
83    fn test_empirical_sampler_weighted() {
84        let mut rng = SmallRng::seed_from_u64(2);
85        let d1 = Duration::ticks(10);
86        let d2 = Duration::ticks(20);
87        let d3 = Duration::ticks(30);
88
89        // Weights: 1, 2, 1. Total: 4.
90        let mut sampler = EmpiricalSampler::new(vec![(d1, 1), (d2, 2), (d3, 1)]);
91
92        let mut results: HashMap<String, usize> = HashMap::new();
93        for _ in 0..1000 {
94            let sample = sampler.sample(&mut rng, SimTime::from_ticks(0));
95            let entry = results
96                .entry(format!("{:<.2}", sample.raw_value()))
97                .or_insert(0);
98            *entry += 1;
99        }
100
101        // Check if the distribution is roughly as expected
102        // With 1000 samples, and weights 1:2:1, we expect roughly 250:500:250
103        let count_10 = *results.get(&format!("{:<.2}", 10.0)).unwrap_or(&0);
104        let count_20 = *results.get(&format!("{:<.2}", 20.0)).unwrap_or(&0);
105        let count_30 = *results.get(&format!("{:<.2}", 30.0)).unwrap_or(&0);
106
107        // Allow for some deviation due to randomness
108        assert!(count_10 > 200 && count_10 < 300);
109        assert!(count_20 > 450 && count_20 < 550);
110        assert!(count_30 > 200 && count_30 < 300);
111    }
112
113    #[test]
114    fn test_empirical_sampler_uniform() {
115        let mut rng = SmallRng::seed_from_u64(2);
116        let d1 = Duration::ticks(10);
117        let d2 = Duration::ticks(20);
118        let d3 = Duration::ticks(30);
119
120        let mut sampler = EmpiricalSampler::new_as_uniform(vec![d1, d2, d3]);
121
122        let mut results: HashMap<String, usize> = HashMap::new();
123        for _ in 0..1000 {
124            let sample = sampler.sample(&mut rng, SimTime::from_ticks(0));
125            let entry = results
126                .entry(format!("{:<.2}", sample.raw_value()))
127                .or_insert(0);
128            *entry += 1;
129        }
130
131        // With 1000 samples and 3 choices, we expect roughly 333 for each
132        let count_10 = *results.get(&format!("{:<.2}", 10.0)).unwrap_or(&0);
133        let count_20 = *results.get(&format!("{:<.2}", 20.0)).unwrap_or(&0);
134        let count_30 = *results.get(&format!("{:<.2}", 30.0)).unwrap_or(&0);
135
136        assert!(count_10 > 280 && count_10 < 380);
137        assert!(count_20 > 280 && count_20 < 380);
138        assert!(count_30 > 280 && count_30 < 380);
139    }
140
141    #[test]
142    #[should_panic(expected = "EmpiricalSampler must have at least one sampler")]
143    fn test_empirical_sampler_empty_histogram() {
144        let _sampler = EmpiricalSampler::new(vec![]);
145    }
146
147    #[test]
148    #[should_panic(expected = "Total weight must be greater than 0")]
149    fn test_empirical_sampler_zero_total_weight() {
150        let d1 = Duration::ticks(10);
151        let _sampler = EmpiricalSampler::new(vec![(d1, 0)]);
152    }
153
154    #[test]
155    fn test_exponential_sampler() {
156        let mut rng = SmallRng::seed_from_u64(2);
157        let lambda = 0.5; // mean = 1/lambda = 2.0
158        let mut sampler = ExponentialSampler::new(lambda).unwrap();
159
160        let mut samples = Vec::new();
161        for _ in 0..10000 {
162            samples.push(sampler.sample(&mut rng, SimTime::from_ticks(0)).raw_value());
163        }
164
165        let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
166        let expected_mean = 1.0 / lambda;
167
168        // Check if the sampled mean is close to the expected mean
169        assert!(
170            (mean - expected_mean).abs() < 0.1,
171            "Mean: {}, Expected: {}",
172            mean,
173            expected_mean
174        );
175
176        // Check if all samples are non-negative
177        assert!(samples.iter().all(|&x| x >= 0.0));
178    }
179}