1use serde::{Deserialize, Serialize};
11use std::fmt;
12
13#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23pub struct Estimate<T: PartialOrd + Copy> {
24 pub point: T,
26 pub lower: T,
28 pub upper: T,
30}
31
32impl<T: PartialOrd + Copy + fmt::Display> Estimate<T> {
35 pub fn new(point: T, lower: T, upper: T) -> Result<Self, EstimateError> {
40 if lower > point || point > upper {
41 return Err(EstimateError::InvalidBounds);
42 }
43 Ok(Self {
44 point,
45 lower,
46 upper,
47 })
48 }
49
50 #[must_use]
53 pub fn exact(value: T) -> Self {
54 Self {
55 point: value,
56 lower: value,
57 upper: value,
58 }
59 }
60}
61
62impl<T: PartialOrd + Copy + fmt::Display> fmt::Display for Estimate<T> {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 write!(f, "{} [{}, {}]", self.point, self.lower, self.upper)
65 }
66}
67
68impl Estimate<f64> {
71 #[must_use]
75 pub fn is_consistent_with(&self, other: &Self) -> bool {
76 self.lower <= other.upper && other.lower <= self.upper
77 }
78
79 #[must_use]
84 pub fn relative_uncertainty(&self) -> f64 {
85 if self.point.abs() < f64::EPSILON {
86 return f64::INFINITY;
87 }
88 (self.upper - self.lower) / self.point.abs()
89 }
90
91 #[must_use]
95 pub fn with_extra_uncertainty(&self, factor: f64) -> Self {
96 debug_assert!(factor >= 1.0, "widening factor must be ≥ 1.0");
97 let half_width = (self.upper - self.lower) * 0.5 * factor;
98 let lower = (self.point - half_width).min(self.lower);
99 let upper = (self.point + half_width).max(self.upper);
100 Self {
101 point: self.point,
102 lower,
103 upper,
104 }
105 }
106}
107
108#[derive(Debug, Clone, thiserror::Error)]
112#[non_exhaustive]
113pub enum EstimateError {
114 #[error("invalid bounds: required lower ≤ point ≤ upper")]
116 InvalidBounds,
117}
118
119#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn new_accepts_valid_bounds() {
127 assert!(Estimate::new(1.0_f64, 0.5, 1.5).is_ok());
128 }
129
130 #[test]
131 fn new_rejects_lower_gt_point() {
132 assert!(Estimate::new(1.0_f64, 1.5, 2.0).is_err());
133 }
134
135 #[test]
136 fn new_rejects_point_gt_upper() {
137 assert!(Estimate::new(1.0_f64, 0.5, 0.8).is_err());
138 }
139
140 #[test]
141 fn exact_has_zero_uncertainty() {
142 let e = Estimate::exact(42.0_f64);
143 assert_eq!(e.point, e.lower);
144 assert_eq!(e.lower, e.upper);
145 assert_eq!(e.relative_uncertainty(), 0.0);
146 }
147
148 #[test]
149 fn relative_uncertainty_near_zero_returns_infinity() {
150 let e = Estimate::exact(0.0_f64);
151 assert!(e.relative_uncertainty().is_infinite());
152 }
153
154 #[test]
155 fn consistency_overlapping_intervals() {
156 let a = Estimate::new(1.0_f64, 0.5, 1.5).unwrap();
157 let b = Estimate::new(1.2_f64, 0.8, 1.6).unwrap();
158 assert!(a.is_consistent_with(&b));
159 }
160
161 #[test]
162 fn consistency_non_overlapping_intervals() {
163 let a = Estimate::new(1.0_f64, 0.5, 1.5).unwrap();
164 let c = Estimate::new(3.0_f64, 2.0, 4.0).unwrap();
165 assert!(!a.is_consistent_with(&c));
166 }
167
168 #[test]
169 fn widen_increases_interval() {
170 let e = Estimate::new(1.0_f64, 0.8, 1.2).unwrap();
171 let w = e.with_extra_uncertainty(2.0);
172 assert!(w.lower <= e.lower);
173 assert!(w.upper >= e.upper);
174 assert!((w.point - e.point).abs() < f64::EPSILON);
175 }
176}
177
178#[cfg(test)]
181mod prop_tests {
182 use super::*;
183 use proptest::prelude::*;
184
185 proptest! {
186 #[test]
188 fn estimate_new_iff_invariant_holds(
189 lower in -1e10_f64..1e10_f64,
190 delta1 in 0.0_f64..1e6_f64,
191 delta2 in 0.0_f64..1e6_f64,
192 ) {
193 let point = lower + delta1;
194 let upper = point + delta2;
195 let result = Estimate::new(point, lower, upper);
196 prop_assert!(result.is_ok());
197 let e = result.unwrap();
198 prop_assert!(e.lower <= e.point);
199 prop_assert!(e.point <= e.upper);
200 }
201
202 #[test]
204 fn exact_invariant(v in -1e15_f64..1e15_f64) {
205 let e = Estimate::exact(v);
206 prop_assert_eq!(e.lower, e.point);
207 prop_assert_eq!(e.point, e.upper);
208 }
209 }
210}