uncertain_numerics/
measure.rs1use core::f64::consts::TAU;
4
5use crate::MeasureError;
6
7pub trait ContinuousProbabilityMeasure {
13 #[must_use]
15 fn density(&self, x: f64) -> f64;
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct GaussianMeasure {
21 mean: f64,
22 variance: f64,
23}
24
25impl GaussianMeasure {
26 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 #[must_use]
48 pub const fn mean(&self) -> f64 {
49 self.mean
50 }
51
52 #[must_use]
54 pub const fn variance(&self) -> f64 {
55 self.variance
56 }
57
58 #[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}