Skip to main content

uncertain_numerics/
measure.rs

1//! Normalized one-dimensional probability measures.
2
3use core::f64::consts::TAU;
4
5use crate::MeasureError;
6
7/// Contract for a normalized continuous probability measure on the real line.
8///
9/// Implementations expose pointwise density evaluation. The normalization
10/// requirement is semantic: implementors represent probability measures whose
11/// total mass is one.
12pub trait ContinuousProbabilityMeasure {
13    /// Evaluate the probability density at `x`.
14    #[must_use]
15    fn density(&self, x: f64) -> f64;
16}
17
18/// Gaussian probability measure \(N(\mu, \sigma^2)\).
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct GaussianMeasure {
21    mean: f64,
22    variance: f64,
23}
24
25impl GaussianMeasure {
26    /// Construct a Gaussian probability measure.
27    ///
28    /// # Errors
29    ///
30    /// Returns [`MeasureError`] when the mean is non-finite or the variance is
31    /// non-finite or not strictly positive.
32    pub fn new(mean: f64, variance: f64) -> Result<Self, MeasureError> {
33        if !mean.is_finite() {
34            return Err(MeasureError::NonFiniteMean);
35        }
36        if !variance.is_finite() {
37            return Err(MeasureError::NonFiniteVariance);
38        }
39        if variance <= 0.0 {
40            return Err(MeasureError::NonPositiveVariance);
41        }
42
43        Ok(Self { mean, variance })
44    }
45
46    /// Return the Gaussian mean \(\mu\).
47    #[must_use]
48    pub const fn mean(&self) -> f64 {
49        self.mean
50    }
51
52    /// Return the Gaussian variance \(\sigma^2\).
53    #[must_use]
54    pub const fn variance(&self) -> f64 {
55        self.variance
56    }
57
58    /// Return the Gaussian standard deviation \(\sigma\).
59    #[must_use]
60    pub fn standard_deviation(&self) -> f64 {
61        self.variance.sqrt()
62    }
63}
64
65impl ContinuousProbabilityMeasure for GaussianMeasure {
66    fn density(&self, x: f64) -> f64 {
67        let centered = x - self.mean;
68        let exponent = -(centered * centered) / (2.0 * self.variance);
69        exponent.exp() / (TAU * self.variance).sqrt()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::{ContinuousProbabilityMeasure, GaussianMeasure};
76    use crate::MeasureError;
77
78    const TOLERANCE: f64 = 1.0e-12;
79
80    fn assert_close(actual: f64, expected: f64) {
81        let scale = expected.abs().max(1.0);
82        assert!(
83            (actual - expected).abs() <= TOLERANCE * scale,
84            "expected {expected:.16e}, got {actual:.16e}"
85        );
86    }
87
88    #[test]
89    fn rejects_non_finite_mean() {
90        assert_eq!(
91            GaussianMeasure::new(f64::NAN, 1.0),
92            Err(MeasureError::NonFiniteMean)
93        );
94        assert_eq!(
95            GaussianMeasure::new(f64::INFINITY, 1.0),
96            Err(MeasureError::NonFiniteMean)
97        );
98    }
99
100    #[test]
101    fn rejects_invalid_variance() {
102        assert_eq!(
103            GaussianMeasure::new(0.0, f64::NAN),
104            Err(MeasureError::NonFiniteVariance)
105        );
106        assert_eq!(
107            GaussianMeasure::new(0.0, f64::INFINITY),
108            Err(MeasureError::NonFiniteVariance)
109        );
110        assert_eq!(
111            GaussianMeasure::new(0.0, 0.0),
112            Err(MeasureError::NonPositiveVariance)
113        );
114        assert_eq!(
115            GaussianMeasure::new(0.0, -1.0),
116            Err(MeasureError::NonPositiveVariance)
117        );
118    }
119
120    #[test]
121    fn exposes_parameters() {
122        let measure = GaussianMeasure::new(1.5, 4.0).expect("parameters are valid");
123
124        assert_close(measure.mean(), 1.5);
125        assert_close(measure.variance(), 4.0);
126        assert_close(measure.standard_deviation(), 2.0);
127    }
128
129    #[test]
130    fn density_matches_standard_normal_at_mean() {
131        let measure = GaussianMeasure::new(0.0, 1.0).expect("parameters are valid");
132        let expected = 1.0 / (2.0 * core::f64::consts::PI).sqrt();
133
134        assert_close(measure.density(0.0), expected);
135    }
136
137    #[test]
138    fn density_is_symmetric_around_mean() {
139        let measure = GaussianMeasure::new(2.0, 1.5).expect("parameters are valid");
140
141        assert_close(measure.density(1.25), measure.density(2.75));
142    }
143
144    #[test]
145    fn density_decreases_away_from_mean() {
146        let measure = GaussianMeasure::new(-0.5, 2.0).expect("parameters are valid");
147        let at_mean = measure.density(-0.5);
148        let one_away = measure.density(0.5);
149        let two_away = measure.density(1.5);
150
151        assert!(at_mean > one_away);
152        assert!(one_away > two_away);
153        assert!(two_away > 0.0);
154    }
155
156    #[test]
157    fn density_is_finite_for_finite_inputs() {
158        let measure = GaussianMeasure::new(0.0, 0.25).expect("parameters are valid");
159
160        for x in [-100.0, -2.0, 0.0, 3.0, 100.0] {
161            assert!(measure.density(x).is_finite());
162            assert!(measure.density(x) >= 0.0);
163        }
164    }
165}