Skip to main content

stats_claw/distributions/symmetric/
uniform.rs

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