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}