blr_active/
calibration_session.rs1use serde::{Deserialize, Serialize};
44
45use crate::goal_assessment::{assess_goal_feasibility, CalibrationGoal, GoalFeasibility};
46use crate::precision_tiers::{compute_precision_tiers, PrecisionLevel, PrecisionTierInfo};
47use blr_core::noise_estimation::{NoiseEstimate, SensorType};
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct ActiveLearningConfig {
57 pub target_std: f64,
59 pub noise_floor: f64,
61 pub max_iterations: usize,
63 pub noise_floor_tolerance_margin: f64,
66}
67
68impl Default for ActiveLearningConfig {
69 fn default() -> Self {
70 Self {
71 target_std: f64::INFINITY,
72 noise_floor: 0.0,
73 max_iterations: 100,
74 noise_floor_tolerance_margin: 0.05,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct NoiseCalibrationSession {
91 pub sensor_type: SensorType,
93 pub estimated_noise: NoiseEstimate,
95 pub precision_tiers: [PrecisionTierInfo; 4],
97 pub selected_goal: Option<CalibrationGoal>,
99 pub target_std: Option<f64>,
101}
102
103impl NoiseCalibrationSession {
104 pub fn new(sensor_type: SensorType, estimated_noise: NoiseEstimate) -> Self {
108 let precision_tiers = compute_precision_tiers(estimated_noise.point_estimate);
109 Self {
110 sensor_type,
111 estimated_noise,
112 precision_tiers,
113 selected_goal: None,
114 target_std: None,
115 }
116 }
117
118 pub fn set_goal(&mut self, level: PrecisionLevel) -> Result<(), String> {
132 let goal = assess_goal_feasibility(level, self.estimated_noise.point_estimate);
133
134 if goal.feasibility == GoalFeasibility::Unachievable {
135 return Err(format!("Cannot set goal: {}", goal.message));
136 }
137
138 self.target_std = Some(goal.target_std);
139 self.selected_goal = Some(goal);
140 Ok(())
141 }
142
143 pub fn to_active_learning_config(&self) -> Result<ActiveLearningConfig, String> {
149 let target_std = self.target_std.ok_or_else(|| {
150 "No goal selected — call set_goal() before to_active_learning_config()".to_string()
151 })?;
152
153 Ok(ActiveLearningConfig {
154 target_std,
155 noise_floor: self.estimated_noise.point_estimate,
156 max_iterations: 100,
157 noise_floor_tolerance_margin: 0.05,
158 })
159 }
160
161 pub fn goal_feasibility(&self) -> Option<&GoalFeasibility> {
163 self.selected_goal.as_ref().map(|g| &g.feasibility)
164 }
165
166 pub fn is_ready(&self) -> bool {
168 self.selected_goal.is_some()
169 }
170}
171
172#[cfg(test)]
175mod tests {
176 use super::*;
177
178 fn make_noise() -> NoiseEstimate {
179 NoiseEstimate {
180 point_estimate: 0.008,
181 lower_bound: 0.0072,
182 upper_bound: 0.0088,
183 confidence: "stable".to_string(),
184 }
185 }
186
187 #[test]
188 fn test_session_new_initialises_correctly() {
189 let session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
190 assert_eq!(session.sensor_type, SensorType::Hall);
191 assert!(session.selected_goal.is_none(), "goal should start as None");
192 assert!(
193 session.target_std.is_none(),
194 "target_std should start as None"
195 );
196 assert!(
197 !session.is_ready(),
198 "session should not be ready before set_goal"
199 );
200 }
201
202 #[test]
203 fn test_session_tiers_computed_from_noise() {
204 let session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
205 assert!(
206 (session.precision_tiers[1].absolute_tolerance - 0.024).abs() < 1e-10,
207 "Moderate tier = 3 × 0.008 = 0.024"
208 );
209 }
210
211 #[test]
212 fn test_set_goal_moderate_accepted() {
213 let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
214 assert!(
215 session.set_goal(PrecisionLevel::Moderate).is_ok(),
216 "Moderate (3.0×) must be accepted"
217 );
218 assert!(session.is_ready());
219 }
220
221 #[test]
222 fn test_set_goal_max_accepted_with_marginal() {
223 let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
224 assert!(
226 session.set_goal(PrecisionLevel::Max).is_ok(),
227 "Max (1.0×, Marginal) must be accepted (with warning in message)"
228 );
229 }
230
231 #[test]
232 fn test_set_goal_stores_target_std() {
233 let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
234 session.set_goal(PrecisionLevel::Moderate).unwrap();
235 let target = session.target_std.unwrap();
236 assert!(
237 (target - 0.024).abs() < 1e-10,
238 "target_std = 3.0 × 0.008 = 0.024"
239 );
240 }
241
242 #[test]
243 fn test_to_active_learning_config_requires_goal() {
244 let session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
245 let result = session.to_active_learning_config();
246 assert!(result.is_err(), "must fail before set_goal() is called");
247 }
248
249 #[test]
250 fn test_to_active_learning_config_correct_values() {
251 let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
252 session.set_goal(PrecisionLevel::Moderate).unwrap();
253 let config = session.to_active_learning_config().unwrap();
254 assert!((config.target_std - 0.024).abs() < 1e-10, "target_std");
255 assert!((config.noise_floor - 0.008).abs() < 1e-10, "noise_floor");
256 assert_eq!(config.max_iterations, 100);
257 }
258
259 #[test]
260 fn test_set_goal_can_be_changed() {
261 let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
262 session.set_goal(PrecisionLevel::Low).unwrap();
263 session.set_goal(PrecisionLevel::Moderate).unwrap();
264 let target = session.target_std.unwrap();
265 assert!(
266 (target - 0.024).abs() < 1e-10,
267 "goal update: target should reflect latest selection"
268 );
269 }
270}