Skip to main content

stats_claw/distributions/symmetric/
uniform.rs

1//! Continuous uniform distribution numerics, for the
2//! [`UniformDistribution`].
3//!
4//! Equivalent to `scipy.stats.uniform(loc=lower_bound, scale=upper_bound −
5//! lower_bound)`: constant density on `[lower_bound, upper_bound]`, zero outside.
6//! Every function is closed form; sampling is an affine map of a single uniform
7//! draw.
8//!
9//! # Examples
10//!
11//! ```
12//! use stats_claw::distributions::{Cdf, Pdf};
13//! use stats_claw::distributions::UniformDistribution;
14//!
15//! let d = UniformDistribution { lower_bound: 0.0, upper_bound: 4.0, ..Default::default() };
16//! // Density on [0, 4] is 1/4 = 0.25.
17//! assert!((d.pdf(2.0) - 0.25).abs() < 1e-12, "pdf was {}", d.pdf(2.0));
18//! // CDF at the midpoint is 0.5.
19//! assert!((d.cdf(2.0) - 0.5).abs() < 1e-12);
20//! ```
21
22use super::super::{Cdf, Moments, Pdf, Quantile, Sample};
23use crate::distributions::UniformDistribution;
24use crate::rng::SplitMix64;
25
26impl UniformDistribution {
27    /// Width of the support, `upper_bound − lower_bound`.
28    fn width(&self) -> f64 {
29        self.upper_bound - self.lower_bound
30    }
31}
32
33impl Pdf for UniformDistribution {
34    fn pdf(&self, x: f64) -> f64 {
35        if x < self.lower_bound || x > self.upper_bound {
36            0.0
37        } else {
38            1.0 / self.width()
39        }
40    }
41}
42
43impl Cdf for UniformDistribution {
44    fn cdf(&self, x: f64) -> f64 {
45        if x <= self.lower_bound {
46            0.0
47        } else if x >= self.upper_bound {
48            1.0
49        } else {
50            (x - self.lower_bound) / self.width()
51        }
52    }
53}
54
55impl Quantile for UniformDistribution {
56    fn quantile(&self, p: f64) -> f64 {
57        self.width().mul_add(p, self.lower_bound)
58    }
59}
60
61impl Moments for UniformDistribution {
62    fn mean(&self) -> Option<f64> {
63        Some(0.5 * (self.lower_bound + self.upper_bound))
64    }
65    fn variance(&self) -> Option<f64> {
66        let w = self.width();
67        Some(w * w / 12.0)
68    }
69}
70
71impl Sample for UniformDistribution {
72    fn sample(&self, rng: &mut SplitMix64) -> f64 {
73        self.width().mul_add(rng.next_f64(), self.lower_bound)
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    /// Density is the reciprocal width inside the support and zero outside.
82    #[test]
83    fn density_is_reciprocal_width_inside() {
84        let d = UniformDistribution {
85            lower_bound: 0.0,
86            upper_bound: 4.0,
87            ..Default::default()
88        };
89        assert!((d.pdf(2.0) - 0.25).abs() < 1e-12);
90        assert!(d.pdf(-1.0).abs() < 1e-12, "outside support must be 0");
91    }
92
93    /// The midpoint is the mean; variance is `width²/12`.
94    #[test]
95    fn moments_are_closed_form() {
96        let d = UniformDistribution {
97            lower_bound: -2.0,
98            upper_bound: 3.0,
99            ..Default::default()
100        };
101        assert_eq!(d.mean(), Some(0.5));
102        assert!(matches!(d.variance(), Some(v) if (v - 25.0 / 12.0).abs() < 1e-12));
103    }
104}