Custom Distributions¶
Add statistical distributions for simulation variability.
Overview¶
Distributions control randomness in:
- Service times
- Order arrival
- Failure rates
- Battery consumption
Distribution Trait¶
pub trait Distribution: Send + Sync {
/// Sample a random value
fn sample(&self, rng: &mut impl Rng) -> f64;
/// Expected value (for planning)
fn mean(&self) -> f64;
/// Variance (for analysis)
fn variance(&self) -> f64;
/// Name for logging
fn name(&self) -> &str;
}
Built-in Distributions¶
Constant¶
Fixed value (no randomness):
pub struct Constant {
value: f64,
}
impl Distribution for Constant {
fn sample(&self, _rng: &mut impl Rng) -> f64 {
self.value
}
fn mean(&self) -> f64 {
self.value
}
fn variance(&self) -> f64 {
0.0
}
}
Uniform¶
Even probability across range:
pub struct Uniform {
min: f64,
max: f64,
}
impl Distribution for Uniform {
fn sample(&self, rng: &mut impl Rng) -> f64 {
rng.gen_range(self.min..=self.max)
}
fn mean(&self) -> f64 {
(self.min + self.max) / 2.0
}
fn variance(&self) -> f64 {
(self.max - self.min).powi(2) / 12.0
}
}
Exponential¶
Memoryless (for queuing theory):
pub struct Exponential {
rate: f64, // λ
}
impl Distribution for Exponential {
fn sample(&self, rng: &mut impl Rng) -> f64 {
let u: f64 = rng.gen();
-u.ln() / self.rate
}
fn mean(&self) -> f64 {
1.0 / self.rate
}
fn variance(&self) -> f64 {
1.0 / (self.rate * self.rate)
}
}
Example: Triangular Distribution¶
Step 1: Implement the Distribution¶
// crates/waremax-core/src/distributions/triangular.rs
use rand::Rng;
use crate::Distribution;
/// Triangular distribution with min, mode, max
pub struct Triangular {
min: f64,
mode: f64,
max: f64,
}
impl Triangular {
pub fn new(min: f64, mode: f64, max: f64) -> Self {
assert!(min <= mode && mode <= max);
Self { min, mode, max }
}
}
impl Distribution for Triangular {
fn sample(&self, rng: &mut impl Rng) -> f64 {
let u: f64 = rng.gen();
let fc = (self.mode - self.min) / (self.max - self.min);
if u < fc {
self.min + ((self.max - self.min) * (self.mode - self.min) * u).sqrt()
} else {
self.max - ((self.max - self.min) * (self.max - self.mode) * (1.0 - u)).sqrt()
}
}
fn mean(&self) -> f64 {
(self.min + self.mode + self.max) / 3.0
}
fn variance(&self) -> f64 {
let a = self.min;
let b = self.mode;
let c = self.max;
(a*a + b*b + c*c - a*b - a*c - b*c) / 18.0
}
fn name(&self) -> &str {
"triangular"
}
}
Step 2: Register the Distribution¶
// crates/waremax-config/src/distributions.rs
#[derive(Deserialize)]
#[serde(tag = "distribution")]
pub enum DistributionConfig {
#[serde(rename = "constant")]
Constant { value: f64 },
#[serde(rename = "uniform")]
Uniform { min: f64, max: f64 },
#[serde(rename = "triangular")]
Triangular { min: f64, mode: f64, max: f64 },
// ...
}
impl DistributionConfig {
pub fn build(&self) -> Box<dyn Distribution> {
match self {
Self::Constant { value } =>
Box::new(Constant::new(*value)),
Self::Uniform { min, max } =>
Box::new(Uniform::new(*min, *max)),
Self::Triangular { min, mode, max } =>
Box::new(Triangular::new(*min, *mode, *max)),
// ...
}
}
}
Step 3: Use in Configuration¶
# scenario.yaml
stations:
- id: "S1"
service_time_s:
distribution: triangular
min: 3.0
mode: 5.0
max: 10.0
Example: Bimodal Distribution¶
For scenarios with two distinct modes:
pub struct Bimodal {
dist1: Normal,
dist2: Normal,
weight1: f64, // Probability of sampling from dist1
}
impl Bimodal {
pub fn new(
mean1: f64, std1: f64,
mean2: f64, std2: f64,
weight1: f64,
) -> Self {
Self {
dist1: Normal::new(mean1, std1),
dist2: Normal::new(mean2, std2),
weight1,
}
}
}
impl Distribution for Bimodal {
fn sample(&self, rng: &mut impl Rng) -> f64 {
if rng.gen::<f64>() < self.weight1 {
self.dist1.sample(rng)
} else {
self.dist2.sample(rng)
}
}
fn mean(&self) -> f64 {
self.weight1 * self.dist1.mean()
+ (1.0 - self.weight1) * self.dist2.mean()
}
fn variance(&self) -> f64 {
// Mixture variance formula
let m1 = self.dist1.mean();
let m2 = self.dist2.mean();
let m = self.mean();
let w1 = self.weight1;
let w2 = 1.0 - w1;
w1 * (self.dist1.variance() + (m1 - m).powi(2))
+ w2 * (self.dist2.variance() + (m2 - m).powi(2))
}
}
Testing Distributions¶
Statistical Tests¶
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
#[test]
fn test_triangular_mean() {
let dist = Triangular::new(0.0, 5.0, 10.0);
let mut rng = ChaCha8Rng::seed_from_u64(42);
let samples: Vec<f64> = (0..10000)
.map(|_| dist.sample(&mut rng))
.collect();
let empirical_mean = samples.iter().sum::<f64>() / samples.len() as f64;
let theoretical_mean = dist.mean();
assert!((empirical_mean - theoretical_mean).abs() < 0.1);
}
#[test]
fn test_triangular_range() {
let dist = Triangular::new(2.0, 5.0, 8.0);
let mut rng = ChaCha8Rng::seed_from_u64(42);
for _ in 0..1000 {
let sample = dist.sample(&mut rng);
assert!(sample >= 2.0 && sample <= 8.0);
}
}
}
Visual Verification¶
// Generate histogram for manual verification
fn print_histogram(dist: &dyn Distribution, bins: usize) {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let samples: Vec<f64> = (0..10000)
.map(|_| dist.sample(&mut rng))
.collect();
let min = samples.iter().cloned().fold(f64::INFINITY, f64::min);
let max = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let bin_width = (max - min) / bins as f64;
// ... histogram logic
}
Best Practices¶
Determinism¶
Always use the provided RNG:
// Good
fn sample(&self, rng: &mut impl Rng) -> f64 {
rng.gen_range(0.0..1.0)
}
// Bad - not deterministic
fn sample(&self, _rng: &mut impl Rng) -> f64 {
rand::random() // Uses thread-local RNG
}
Validation¶
Validate parameters:
impl Triangular {
pub fn new(min: f64, mode: f64, max: f64) -> Self {
assert!(min <= mode, "min must be <= mode");
assert!(mode <= max, "mode must be <= max");
assert!(min < max, "min must be < max");
// ...
}
}
Documentation¶
Document the distribution:
/// Triangular distribution
///
/// PDF is triangular with peak at `mode`.
/// Useful when you know the likely value but not the exact distribution.
///
/// # Parameters
/// - `min`: Minimum possible value
/// - `mode`: Most likely value
/// - `max`: Maximum possible value