Skip to main content

blr_active/
precision_tiers.rs

1//! Precision-tier definitions for noise-aware calibration goals.
2//!
3//! Precision tiers frame calibration targets as multiples of the sensor's
4//! inherent noise floor (σ_noise). Four tiers are provided:
5//!
6//! | Tier     | Factor | Samples (est.) | Use case                        |
7//! |----------|--------|----------------|---------------------------------|
8//! | Low      | 5.0×   | 10             | Rapid commissioning             |
9//! | Moderate | 3.0×   | 25             | Typical production calibration  |
10//! | High     | 1.5×   | 60             | Demanding applications          |
11//! | Max      | 1.0×   | 200            | Theoretical sensor limit        |
12//!
13//! **Factor rationale:**
14//! - 5× ensures a comfortable margin well above the noise floor; achievable
15//!   with very few samples.
16//! - 3× is the classical rule-of-thumb ("3-sigma") margin used in measurement
17//!   science to separate signal from noise reliably.
18//! - 1.5× provides a tight margin; requires significantly more data and is
19//!   statistically risky near the noise ceiling.
20//! - 1× is the theoretical sensor limit; rarely achievable in practice with
21//!   finite data.
22//!
23//! Sample estimates use the heuristic `ceil(50 / factor²)`, which is derived
24//! empirically from typical active Learning convergence rates in BLR+ARD.
25//!
26//! # Example
27//!
28//! ```rust
29//! use blr_active::precision_tiers::{PrecisionLevel, compute_precision_tiers};
30//!
31//! let sigma_noise = 0.008; // V
32//! let tiers = compute_precision_tiers(sigma_noise);
33//!
34//! // Low tier: ±0.040 V (5.0×)
35//! assert!((tiers[0].absolute_tolerance - 0.040).abs() < 1e-10);
36//! // Moderate tier: ±0.024 V (3.0×)
37//! assert!((tiers[1].absolute_tolerance - 0.024).abs() < 1e-10);
38//! // High tier: ±0.012 V (1.5×)
39//! assert!((tiers[2].absolute_tolerance - 0.012).abs() < 1e-10);
40//! // Max tier: ±0.008 V (1.0×)
41//! assert!((tiers[3].absolute_tolerance - 0.008).abs() < 1e-10);
42//! ```
43
44use serde::{Deserialize, Serialize};
45use std::fmt;
46
47// ─── PrecisionLevel ───────────────────────────────────────────────────────────
48
49/// Calibration precision goal expressed as a multiple of the sensor noise floor.
50///
51/// Choose based on the application's tolerance for measurement uncertainty:
52/// - **Low** — fastest, coarsest; suitable for rapid commissioning.
53/// - **Moderate** — balanced; covers most production use cases.
54/// - **High** — demanding; requires substantially more measurements.
55/// - **Max** — theoretical limit; rarely achievable with finite data.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum PrecisionLevel {
58    /// Target precision = 5× sensor noise. Low accuracy; fast convergence.
59    Low,
60    /// Target precision = 3× sensor noise. Balanced accuracy and sample cost.
61    Moderate,
62    /// Target precision = 1.5× sensor noise. High accuracy; significant sample cost.
63    High,
64    /// Target precision = 1× sensor noise. Sensor-limited; theoretical maximum.
65    Max,
66}
67
68impl PrecisionLevel {
69    /// Noise multiplier for this tier (absolute_tolerance = σ_noise × factor).
70    pub fn factor(self) -> f64 {
71        match self {
72            PrecisionLevel::Low => 5.0,
73            PrecisionLevel::Moderate => 3.0,
74            PrecisionLevel::High => 1.5,
75            PrecisionLevel::Max => 1.0,
76        }
77    }
78
79    /// Human-readable description of this tier.
80    pub fn description(self) -> &'static str {
81        match self {
82            PrecisionLevel::Low => "Low (5× noise — easy, few samples)",
83            PrecisionLevel::Moderate => "Moderate (3× noise — balanced, typical)",
84            PrecisionLevel::High => "High (1.5× noise — challenging, many samples)",
85            PrecisionLevel::Max => "Maximum (1× noise — sensor-limited, theoretical)",
86        }
87    }
88
89    /// Heuristic number of samples estimated to reach this tier.
90    ///
91    /// Derived from the approximation `ceil(50 / factor²)`, calibrated against
92    /// typical BLR+ARD active learning convergence rates.
93    pub fn estimated_samples(self) -> usize {
94        match self {
95            PrecisionLevel::Low => 10,
96            PrecisionLevel::Moderate => 25,
97            PrecisionLevel::High => 60,
98            PrecisionLevel::Max => 200,
99        }
100    }
101}
102
103impl fmt::Display for PrecisionLevel {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "{}", self.description())
106    }
107}
108
109// ─── Feasibility ──────────────────────────────────────────────────────────────
110
111/// Whether a precision goal is generically reachable given sensor hardware.
112///
113/// Used in `PrecisionTierInfo` to flag tiers that are inherently at or below
114/// the noise floor (Max tier always returns `Marginal` in `compute_precision_tiers`).
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub enum Feasibility {
117    /// Goal is comfortably above the sensor noise floor. Achievable with data.
118    Achievable,
119    /// Goal is very close to the noise floor. Risky; may not converge.
120    Marginal,
121    /// Goal is at or below the noise floor. Impossible with this sensor.
122    Unachievable,
123}
124
125impl fmt::Display for Feasibility {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self {
128            Feasibility::Achievable => write!(f, "Achievable"),
129            Feasibility::Marginal => write!(f, "Marginal (close to noise floor)"),
130            Feasibility::Unachievable => write!(f, "Unachievable (below noise floor)"),
131        }
132    }
133}
134
135// ─── PrecisionTierInfo ────────────────────────────────────────────────────────
136
137/// Computed information for one precision tier given a specific σ_noise.
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PrecisionTierInfo {
140    /// Which tier this entry describes.
141    pub level: PrecisionLevel,
142    /// Absolute std target in signal units (σ_noise × factor).
143    pub absolute_tolerance: f64,
144    /// Heuristic number of samples required to reach this tier.
145    pub estimated_samples: usize,
146    /// Whether this tier is generically reachable given the noise floor.
147    pub feasibility: Feasibility,
148}
149
150impl fmt::Display for PrecisionTierInfo {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        write!(
153            f,
154            "{:<10} ±{:.4}  ~{:>3} samples  [{}]",
155            format!("{:?}", self.level),
156            self.absolute_tolerance,
157            self.estimated_samples,
158            self.feasibility,
159        )
160    }
161}
162
163// ─── compute_precision_tiers ─────────────────────────────────────────────────
164
165/// Compute all four precision tiers for a given sensor noise level.
166///
167/// Returns an array of four [`PrecisionTierInfo`] structs ordered from loosest
168/// (Low) to tightest (Max).
169///
170/// The Max tier is always marked `Marginal` because achieving 1× noise
171/// precision requires an impractically large number of samples and the posterior
172/// variance asymptotically approaches (but rarely reaches) the noise floor.
173///
174/// # Example
175///
176/// ```rust
177/// use blr_active::precision_tiers::{PrecisionLevel, compute_precision_tiers};
178///
179/// let tiers = compute_precision_tiers(0.008);
180/// assert!((tiers[1].absolute_tolerance - 0.024).abs() < 1e-10,
181///         "Moderate tier: 3.0 × 0.008 = 0.024");
182/// ```
183pub fn compute_precision_tiers(sigma_noise: f64) -> [PrecisionTierInfo; 4] {
184    let make_tier = |level: PrecisionLevel, feasibility: Feasibility| -> PrecisionTierInfo {
185        PrecisionTierInfo {
186            absolute_tolerance: sigma_noise * level.factor(),
187            estimated_samples: level.estimated_samples(),
188            level,
189            feasibility,
190        }
191    };
192
193    [
194        make_tier(PrecisionLevel::Low, Feasibility::Achievable),
195        make_tier(PrecisionLevel::Moderate, Feasibility::Achievable),
196        make_tier(PrecisionLevel::High, Feasibility::Achievable),
197        make_tier(PrecisionLevel::Max, Feasibility::Marginal),
198    ]
199}
200
201// ─── Tests ────────────────────────────────────────────────────────────────────
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    const SIGMA: f64 = 0.008;
208
209    #[test]
210    fn test_precision_tier_low() {
211        let tiers = compute_precision_tiers(SIGMA);
212        assert!(
213            (tiers[0].absolute_tolerance - SIGMA * 5.0).abs() < 1e-10,
214            "Low tier factor 5.0"
215        );
216        assert_eq!(tiers[0].level, PrecisionLevel::Low);
217    }
218
219    #[test]
220    fn test_precision_tier_moderate() {
221        let tiers = compute_precision_tiers(SIGMA);
222        assert!(
223            (tiers[1].absolute_tolerance - SIGMA * 3.0).abs() < 1e-10,
224            "Moderate: 3.0 × 0.008 = 0.024"
225        );
226        assert_eq!(tiers[1].level, PrecisionLevel::Moderate);
227    }
228
229    #[test]
230    fn test_precision_tier_high() {
231        let tiers = compute_precision_tiers(SIGMA);
232        assert!(
233            (tiers[2].absolute_tolerance - SIGMA * 1.5).abs() < 1e-10,
234            "High tier factor 1.5"
235        );
236        assert_eq!(tiers[2].level, PrecisionLevel::High);
237    }
238
239    #[test]
240    fn test_precision_tier_max() {
241        let tiers = compute_precision_tiers(SIGMA);
242        assert!(
243            (tiers[3].absolute_tolerance - SIGMA * 1.0).abs() < 1e-10,
244            "Max tier factor 1.0"
245        );
246        assert_eq!(tiers[3].level, PrecisionLevel::Max);
247    }
248
249    #[test]
250    fn test_precision_tier_max_is_marginal() {
251        let tiers = compute_precision_tiers(SIGMA);
252        assert_eq!(
253            tiers[3].feasibility,
254            Feasibility::Marginal,
255            "Max tier is inherently marginal (at noise floor)"
256        );
257    }
258
259    #[test]
260    fn test_precision_tiers_are_monotonically_decreasing() {
261        let tiers = compute_precision_tiers(SIGMA);
262        for i in 0..3 {
263            assert!(
264                tiers[i].absolute_tolerance > tiers[i + 1].absolute_tolerance,
265                "Tiers should be ordered from loosest to tightest"
266            );
267        }
268    }
269
270    #[test]
271    fn test_precision_tier_estimated_samples() {
272        assert_eq!(PrecisionLevel::Low.estimated_samples(), 10);
273        assert_eq!(PrecisionLevel::Moderate.estimated_samples(), 25);
274        assert_eq!(PrecisionLevel::High.estimated_samples(), 60);
275        assert_eq!(PrecisionLevel::Max.estimated_samples(), 200);
276    }
277
278    #[test]
279    fn test_factor_values() {
280        assert!((PrecisionLevel::Low.factor() - 5.0).abs() < 1e-10);
281        assert!((PrecisionLevel::Moderate.factor() - 3.0).abs() < 1e-10);
282        assert!((PrecisionLevel::High.factor() - 1.5).abs() < 1e-10);
283        assert!((PrecisionLevel::Max.factor() - 1.0).abs() < 1e-10);
284    }
285}