sim/input_modeling/
random_variable.rs

1//! Random variables underpin both stochastic and deterministic model
2//! behaviors, in that deterministic operation is simply a random variable
3//! with a single value of probability 1.  Common distributions, with their
4//! common parameterizations, are wrapped in enums `Continuous`, `Boolean`,
5//! `Discrete`, and `Index`.
6
7use rand::distributions::Distribution;
8use serde::{Deserialize, Serialize};
9// Continuous distributions
10use rand_distr::{Beta, Exp, Gamma, LogNormal, Normal, Triangular, Uniform, Weibull};
11// Discrete distributions
12use rand_distr::{Bernoulli, Geometric, Poisson, WeightedIndex};
13
14use super::dynamic_rng::DynRng;
15use crate::utils::errors::SimulationError;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum Continuous {
20    Beta { alpha: f64, beta: f64 },
21    Exp { lambda: f64 },
22    Gamma { shape: f64, scale: f64 },
23    LogNormal { mu: f64, sigma: f64 },
24    Normal { mean: f64, std_dev: f64 },
25    Triangular { min: f64, max: f64, mode: f64 },
26    Uniform { min: f64, max: f64 },
27    Weibull { shape: f64, scale: f64 },
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub enum Boolean {
33    Bernoulli { p: f64 },
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(rename_all = "camelCase")]
38pub enum Discrete {
39    Geometric {
40        p: f64,
41    },
42    Poisson {
43        lambda: f64,
44    },
45    /// Range is inclusive of min, exclusive of max: [min, max)
46    Uniform {
47        min: u64,
48        max: u64,
49    },
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub enum Index {
55    /// Range is inclusive of min, exclusive of max: [min, max)
56    Uniform {
57        min: usize,
58        max: usize,
59    },
60    WeightedIndex {
61        weights: Vec<u64>,
62    },
63}
64
65impl Continuous {
66    /// The generation of random variates drives stochastic behaviors during
67    /// simulation execution.  This function requires the random number
68    /// generator of the simulation, and produces a f64 random variate.
69    pub fn random_variate(&mut self, uniform_rng: DynRng) -> Result<f64, SimulationError> {
70        let mut rng = (*uniform_rng).borrow_mut();
71        match self {
72            Continuous::Beta { alpha, beta } => Ok(Beta::new(*alpha, *beta)?.sample(&mut *rng)),
73            Continuous::Exp { lambda } => Ok(Exp::new(*lambda)?.sample(&mut *rng)),
74            Continuous::Gamma { shape, scale } => Ok(Gamma::new(*shape, *scale)?.sample(&mut *rng)),
75            Continuous::LogNormal { mu, sigma } => {
76                Ok(LogNormal::new(*mu, *sigma)?.sample(&mut *rng))
77            }
78            Continuous::Normal { mean, std_dev } => {
79                Ok(Normal::new(*mean, *std_dev)?.sample(&mut *rng))
80            }
81            Continuous::Triangular { min, max, mode } => {
82                Ok(Triangular::new(*min, *max, *mode)?.sample(&mut *rng))
83            }
84            Continuous::Uniform { min, max } => Ok(Uniform::new(*min, *max).sample(&mut *rng)),
85            Continuous::Weibull { shape, scale } => {
86                Ok(Weibull::new(*shape, *scale)?.sample(&mut *rng))
87            }
88        }
89    }
90}
91
92impl Boolean {
93    /// The generation of random variates drives stochastic behaviors during
94    /// simulation execution.  This function requires the random number
95    /// generator of the simulation, and produces a boolean random variate.
96    pub fn random_variate(&mut self, uniform_rng: DynRng) -> Result<bool, SimulationError> {
97        let mut rng = (*uniform_rng).borrow_mut();
98        match self {
99            Boolean::Bernoulli { p } => Ok(Bernoulli::new(*p)?.sample(&mut *rng)),
100        }
101    }
102}
103
104impl Discrete {
105    /// The generation of random variates drives stochastic behaviors during
106    /// simulation execution.  This function requires the random number
107    /// generator of the simulation, and produces a u64 random variate.
108    pub fn random_variate(&mut self, uniform_rng: DynRng) -> Result<u64, SimulationError> {
109        let mut rng = (*uniform_rng).borrow_mut();
110        match self {
111            Discrete::Geometric { p } => Ok(Geometric::new(*p)?.sample(&mut *rng)),
112            Discrete::Poisson { lambda } => Ok(Poisson::new(*lambda)?.sample(&mut *rng) as u64),
113            Discrete::Uniform { min, max } => Ok(Uniform::new(*min, *max).sample(&mut *rng)),
114        }
115    }
116}
117
118impl Index {
119    /// The generation of random variates drives stochastic behaviors during
120    /// simulation execution.  This function requires the random number
121    /// generator of the simulation, and produces a usize random variate.
122    pub fn random_variate(&mut self, uniform_rng: DynRng) -> Result<usize, SimulationError> {
123        let mut rng = (*uniform_rng).borrow_mut();
124        match self {
125            Index::Uniform { min, max } => Ok(Uniform::new(*min, *max).sample(&mut *rng)),
126            Index::WeightedIndex { weights } => {
127                Ok(WeightedIndex::new(weights.clone())?.sample(&mut *rng))
128            }
129        }
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use crate::input_modeling::dynamic_rng::default_rng;
136
137    use super::*;
138
139    enum RandomVariable {
140        Continuous(Continuous),
141        Discrete(Discrete),
142    }
143
144    enum ChiSquareTest {
145        Continuous {
146            variable: Continuous,
147            bin_mapping_fn: fn(f64) -> usize,
148        },
149        Boolean {
150            variable: Boolean,
151            bin_mapping_fn: fn(bool) -> usize,
152        },
153        Discrete {
154            variable: Discrete,
155            bin_mapping_fn: fn(u64) -> usize,
156        },
157        Index {
158            variable: Index,
159            bin_mapping_fn: fn(usize) -> usize,
160        },
161    }
162
163    fn empirical_mean(random_variable: &mut RandomVariable, sample_size: usize) -> f64 {
164        let uniform_rng = default_rng();
165        (0..sample_size)
166            .map(|_| match random_variable {
167                RandomVariable::Continuous(variable) => {
168                    variable.random_variate(uniform_rng.clone()).unwrap()
169                }
170                RandomVariable::Discrete(variable) => {
171                    variable.random_variate(uniform_rng.clone()).unwrap() as f64
172                }
173            })
174            .sum::<f64>()
175            / (sample_size as f64)
176    }
177
178    fn chi_square(test: &mut ChiSquareTest, expected_counts: &[usize]) -> f64 {
179        let mut class_counts = vec![0; expected_counts.len()];
180        let uniform_rng = default_rng();
181        let sample_size = expected_counts.iter().sum();
182        (0..sample_size).for_each(|_| {
183            let index = match test {
184                ChiSquareTest::Continuous {
185                    variable,
186                    bin_mapping_fn,
187                } => bin_mapping_fn(variable.random_variate(uniform_rng.clone()).unwrap()),
188                ChiSquareTest::Boolean {
189                    variable,
190                    bin_mapping_fn,
191                } => bin_mapping_fn(variable.random_variate(uniform_rng.clone()).unwrap()),
192                ChiSquareTest::Discrete {
193                    variable,
194                    bin_mapping_fn,
195                } => bin_mapping_fn(variable.random_variate(uniform_rng.clone()).unwrap()),
196                ChiSquareTest::Index {
197                    variable,
198                    bin_mapping_fn,
199                } => bin_mapping_fn(variable.random_variate(uniform_rng.clone()).unwrap()),
200            };
201            class_counts[index] += 1
202        });
203        class_counts.iter().zip(expected_counts.iter()).fold(
204            0.0,
205            |acc, (class_count, expected_count)| {
206                let f_class_count = *class_count as f64;
207                let f_expected_count = *expected_count as f64;
208                acc + (f_class_count - f_expected_count).powi(2) / f_expected_count
209            },
210        )
211    }
212
213    #[test]
214    fn beta_samples_match_expectation() {
215        let variable = Continuous::Beta {
216            alpha: 7.0,
217            beta: 11.0,
218        };
219        let mean = empirical_mean(&mut RandomVariable::Continuous(variable), 10000);
220        let expected = 7.0 / (7.0 + 11.0);
221        assert!((mean - expected).abs() / expected < 0.025);
222    }
223
224    #[test]
225    fn exponential_samples_match_expectation() {
226        let variable = Continuous::Exp { lambda: 7.0 };
227        let mean = empirical_mean(&mut RandomVariable::Continuous(variable), 10000);
228        let expected = 1.0 / 7.0;
229        assert!((mean - expected).abs() / expected < 0.025);
230    }
231
232    #[test]
233    fn gamma_samples_match_expectation() {
234        let variable = Continuous::Gamma {
235            shape: 7.0,
236            scale: 11.0,
237        };
238        let mean = empirical_mean(&mut RandomVariable::Continuous(variable), 10000);
239        let expected = 77.0;
240        assert!((mean - expected).abs() / expected < 0.025);
241    }
242
243    #[test]
244    fn lognormal_samples_match_expectation() {
245        let variable = Continuous::LogNormal {
246            mu: 11.0,
247            sigma: 1.0,
248        };
249        let mean = empirical_mean(&mut RandomVariable::Continuous(variable), 10000);
250        let expected = (11.0f64 + 1.0f64.powi(2) / 2.0f64).exp();
251        assert!((mean - expected).abs() / expected < 0.025);
252    }
253
254    #[test]
255    fn normal_samples_chi_square() {
256        fn bins_mapping(variate: f64) -> usize {
257            let mean = 11.0;
258            let std_dev = 3.0;
259            if variate < mean - 3.0 * std_dev {
260                0
261            } else if variate < mean - 2.0 * std_dev {
262                1
263            } else if variate < mean - std_dev {
264                2
265            } else if variate < mean {
266                3
267            } else if variate < mean + std_dev {
268                4
269            } else if variate < mean + 2.0 * std_dev {
270                5
271            } else if variate < mean + 3.0 * std_dev {
272                6
273            } else {
274                7
275            }
276        }
277        let variable = Continuous::Normal {
278            mean: 11.0,
279            std_dev: 3.0,
280        };
281        // 8 classes (a.k.a. bins)
282        // On each side: within 1 sigma, 1 sigma to 2 sigma, 2 sigma to 3 sigma, 3+ sigma
283        let expected_counts: [usize; 8] = [20, 210, 1360, 3410, 3410, 1360, 210, 20];
284        let chi_square_actual = chi_square(
285            &mut ChiSquareTest::Continuous {
286                variable: variable,
287                bin_mapping_fn: bins_mapping,
288            },
289            &expected_counts,
290        );
291        // At a significance level of 0.01, and with n-1=7 degrees of freedom, the chi square critical
292        // value for this scenario is 18.475
293        let chi_square_critical = 18.475;
294        assert![chi_square_actual < chi_square_critical];
295    }
296
297    #[test]
298    fn triangular_samples_chi_square() {
299        fn bins_mapping(variate: f64) -> usize {
300            ((variate - 5.0) / 5.0) as usize
301        }
302        let variable = Continuous::Triangular {
303            min: 5.0,
304            max: 25.0,
305            mode: 15.0,
306        };
307        // 4 classes/bins - each of width 5
308        let expected_counts: [usize; 4] = [125, 375, 375, 125];
309        let chi_square_actual = chi_square(
310            &mut ChiSquareTest::Continuous {
311                variable: variable,
312                bin_mapping_fn: bins_mapping,
313            },
314            &expected_counts,
315        );
316        // At a significance level of 0.01, and with n-1=3 degrees of freedom, the chi square critical
317        // value for this scenario is 134.642
318        let chi_square_critical = 11.345;
319        assert![chi_square_actual < chi_square_critical];
320    }
321
322    #[test]
323    fn continuous_uniform_samples_chi_square() {
324        fn bins_mapping(variate: f64) -> usize {
325            let min = 7.0;
326            let max = 11.0;
327            ((variate - min) * (max - 1.0)) as usize
328        }
329        let variable = Continuous::Uniform {
330            min: 7.0,
331            max: 11.0,
332        };
333        // Constant bin counts, due to uniformity of distribution
334        let expected_counts: [usize; 40] = [250; 40];
335        let chi_square_actual = chi_square(
336            &mut ChiSquareTest::Continuous {
337                variable: variable,
338                bin_mapping_fn: bins_mapping,
339            },
340            &expected_counts,
341        );
342        // At a significance level of 0.01, and with n-1=39 degrees of freedom, the chi square critical
343        // value for this scenario is 62.428
344        let chi_square_critical = 62.428;
345        assert![chi_square_actual < chi_square_critical];
346    }
347
348    #[test]
349    fn weibull_samples_match_expectation() {
350        let variable = Continuous::Weibull {
351            shape: 7.0,
352            scale: 0.5,
353        };
354        let mean = empirical_mean(&mut RandomVariable::Continuous(variable), 10000);
355        let expected = 14.0;
356        assert!((mean - expected).abs() / expected < 0.025);
357    }
358
359    #[test]
360    fn bernoulli_samples_chi_square() {
361        fn bins_mapping(variate: bool) -> usize {
362            variate as usize
363        }
364        let variable = Boolean::Bernoulli { p: 0.3 };
365        // Failures (false == 0) is 70% of trials and success (true == 1) is 30% of trials
366        let expected_counts: [usize; 2] = [7000, 3000];
367        let chi_square_actual = chi_square(
368            &mut ChiSquareTest::Boolean {
369                variable: variable,
370                bin_mapping_fn: bins_mapping,
371            },
372            &expected_counts,
373        );
374        // At a significance level of 0.01, and with n-1=1 degrees of freedom, the chi square critical
375        // value for this scenario is 6.635
376        let chi_square_critical = 6.635;
377        assert![chi_square_actual < chi_square_critical];
378    }
379
380    #[test]
381    fn geometric_samples_match_expectation() {
382        let variable = Discrete::Geometric { p: 0.2 };
383        let mean = empirical_mean(&mut RandomVariable::Discrete(variable), 10000);
384        let expected = (1.0 - 0.2) / 0.2;
385        assert!((mean - expected).abs() / expected < 0.025);
386    }
387
388    #[test]
389    fn poisson_samples_match_expectation() {
390        let variable = Discrete::Poisson { lambda: 7.0 };
391        let mean = empirical_mean(&mut RandomVariable::Discrete(variable), 10000);
392        let expected = 7.0;
393        assert!((mean - expected).abs() / expected < 0.025);
394    }
395
396    #[test]
397    fn discrete_uniform_samples_chi_square() {
398        fn bins_mapping(variate: u64) -> usize {
399            let min = 7;
400            (variate - min) as usize
401        }
402        let variable = Discrete::Uniform { min: 7, max: 11 };
403        // Constant bin counts, due to uniformity of distribution
404        let expected_counts: [usize; 4] = [2500; 4];
405        let chi_square_actual = chi_square(
406            &mut ChiSquareTest::Discrete {
407                variable: variable,
408                bin_mapping_fn: bins_mapping,
409            },
410            &expected_counts,
411        );
412        // At a significance level of 0.01, and with n-1=4 degrees of freedom, the chi square critical
413        // value for this scenario is 13.277
414        let chi_square_critical = 13.277;
415        assert![chi_square_actual < chi_square_critical];
416    }
417
418    #[test]
419    fn weighted_index_samples_chi_square() {
420        fn bins_mapping(variate: usize) -> usize {
421            variate
422        }
423        let variable = Index::WeightedIndex {
424            weights: vec![1, 2, 3, 4],
425        };
426        // The expected bin counts scale linearly with the weights
427        let expected_counts: [usize; 4] = [1000, 2000, 3000, 4000];
428        let chi_square_actual = chi_square(
429            &mut ChiSquareTest::Index {
430                variable: variable,
431                bin_mapping_fn: bins_mapping,
432            },
433            &expected_counts,
434        );
435        // At a significance level of 0.01, and with n-1=3 degrees of freedom, the chi square critical
436        // value for this scenario is 11.345
437        let chi_square_critical = 11.345;
438        assert![chi_square_actual < chi_square_critical];
439    }
440
441    #[test]
442    fn index_uniform_samples_chi_square() {
443        fn bins_mapping(variate: usize) -> usize {
444            let min = 7;
445            variate - min
446        }
447        let variable = Index::Uniform { min: 7, max: 11 };
448        // Constant bin counts, due to uniformity of distribution
449        let expected_counts: [usize; 4] = [2500; 4];
450        let chi_square_actual = chi_square(
451            &mut ChiSquareTest::Index {
452                variable: variable,
453                bin_mapping_fn: bins_mapping,
454            },
455            &expected_counts,
456        );
457        // At a significance level of 0.01, and with n-1=4 degrees of freedom, the chi square critical
458        // value for this scenario is 13.277
459        let chi_square_critical = 13.277;
460        assert![chi_square_actual < chi_square_critical];
461    }
462}