Skip to main content

pacr_types/
estimate.rs

1//! Pillar: ALL. PACR field: ALL (shared measurement infrastructure).
2//!
3//! `Estimate<T>` encodes every physical quantity as a point estimate with a
4//! 95 % confidence interval `[lower, upper]`.  The interval width shrinks as
5//! measurement precision improves; the *schema* never changes.
6//!
7//! Physical axiom: all physical measurements carry finite-precision uncertainty
8//! (Heisenberg, thermodynamic fluctuations, finite clock resolution).
9
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13// ── Core type ───────────────────────────────────────────────────────────────
14
15/// A physical measurement: point estimate ± 95 % confidence interval.
16///
17/// **Invariant**: `lower ≤ point ≤ upper`
18///
19/// `Eq` and `Hash` are intentionally NOT derived for `Estimate<f64>`.
20/// Floating-point equality is physically meaningless for measurements.
21/// Use [`Estimate::is_consistent_with`] for physically meaningful comparison.
22#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
23pub struct Estimate<T: PartialOrd + Copy> {
24    /// Best single-value estimate (e.g. mean or median of samples).
25    pub point: T,
26    /// Lower bound of the 95 % confidence interval.
27    pub lower: T,
28    /// Upper bound of the 95 % confidence interval.
29    pub upper: T,
30}
31
32// ── Generic constructors ─────────────────────────────────────────────────────
33
34impl<T: PartialOrd + Copy + fmt::Display> Estimate<T> {
35    /// Constructs a new estimate, enforcing `lower ≤ point ≤ upper`.
36    ///
37    /// # Errors
38    /// Returns [`EstimateError::InvalidBounds`] if the invariant is violated.
39    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    /// Creates an exact estimate with zero uncertainty.
51    /// Use for quantities known with mathematical certainty (e.g. counting).
52    #[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
68// ── f64-specific physics operations ──────────────────────────────────────────
69
70impl Estimate<f64> {
71    /// Two estimates are physically consistent if their confidence intervals overlap.
72    ///
73    /// This is the correct notion of "agreement" between two uncertain measurements.
74    #[must_use]
75    pub fn is_consistent_with(&self, other: &Self) -> bool {
76        self.lower <= other.upper && other.lower <= self.upper
77    }
78
79    /// Relative uncertainty: `(upper − lower) / |point|`.
80    ///
81    /// Returns `f64::INFINITY` when `|point|` is below machine epsilon
82    /// (cannot compute a meaningful relative uncertainty for a near-zero quantity).
83    #[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    /// Returns a widened copy with interval expanded by `factor` (≥ 1.0).
92    ///
93    /// Useful when composing multiple uncertain quantities conservatively.
94    #[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// ── Error type ───────────────────────────────────────────────────────────────
109
110/// Error produced when constructing an [`Estimate`] with invalid bounds.
111#[derive(Debug, Clone, thiserror::Error)]
112#[non_exhaustive]
113pub enum EstimateError {
114    /// `lower ≤ point ≤ upper` was violated.
115    #[error("invalid bounds: required lower ≤ point ≤ upper")]
116    InvalidBounds,
117}
118
119// ── Unit tests ────────────────────────────────────────────────────────────────
120
121#[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// ── Property-based tests ──────────────────────────────────────────────────────
179
180#[cfg(test)]
181mod prop_tests {
182    use super::*;
183    use proptest::prelude::*;
184
185    proptest! {
186        /// new() must accept exactly the triples satisfying lower ≤ point ≤ upper.
187        #[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        /// exact() always satisfies lower = point = upper.
203        #[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}