use serde::{Deserialize, Serialize};
use crate::goal_assessment::{assess_goal_feasibility, CalibrationGoal, GoalFeasibility};
use crate::precision_tiers::{compute_precision_tiers, PrecisionLevel, PrecisionTierInfo};
use blr_core::noise_estimation::{NoiseEstimate, SensorType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveLearningConfig {
pub target_std: f64,
pub noise_floor: f64,
pub max_iterations: usize,
pub noise_floor_tolerance_margin: f64,
}
impl Default for ActiveLearningConfig {
fn default() -> Self {
Self {
target_std: f64::INFINITY,
noise_floor: 0.0,
max_iterations: 100,
noise_floor_tolerance_margin: 0.05,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoiseCalibrationSession {
pub sensor_type: SensorType,
pub estimated_noise: NoiseEstimate,
pub precision_tiers: [PrecisionTierInfo; 4],
pub selected_goal: Option<CalibrationGoal>,
pub target_std: Option<f64>,
}
impl NoiseCalibrationSession {
pub fn new(sensor_type: SensorType, estimated_noise: NoiseEstimate) -> Self {
let precision_tiers = compute_precision_tiers(estimated_noise.point_estimate);
Self {
sensor_type,
estimated_noise,
precision_tiers,
selected_goal: None,
target_std: None,
}
}
pub fn set_goal(&mut self, level: PrecisionLevel) -> Result<(), String> {
let goal = assess_goal_feasibility(level, self.estimated_noise.point_estimate);
if goal.feasibility == GoalFeasibility::Unachievable {
return Err(format!("Cannot set goal: {}", goal.message));
}
self.target_std = Some(goal.target_std);
self.selected_goal = Some(goal);
Ok(())
}
pub fn to_active_learning_config(&self) -> Result<ActiveLearningConfig, String> {
let target_std = self.target_std.ok_or_else(|| {
"No goal selected — call set_goal() before to_active_learning_config()".to_string()
})?;
Ok(ActiveLearningConfig {
target_std,
noise_floor: self.estimated_noise.point_estimate,
max_iterations: 100,
noise_floor_tolerance_margin: 0.05,
})
}
pub fn goal_feasibility(&self) -> Option<&GoalFeasibility> {
self.selected_goal.as_ref().map(|g| &g.feasibility)
}
pub fn is_ready(&self) -> bool {
self.selected_goal.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_noise() -> NoiseEstimate {
NoiseEstimate {
point_estimate: 0.008,
lower_bound: 0.0072,
upper_bound: 0.0088,
confidence: "stable".to_string(),
}
}
#[test]
fn test_session_new_initialises_correctly() {
let session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
assert_eq!(session.sensor_type, SensorType::Hall);
assert!(session.selected_goal.is_none(), "goal should start as None");
assert!(
session.target_std.is_none(),
"target_std should start as None"
);
assert!(
!session.is_ready(),
"session should not be ready before set_goal"
);
}
#[test]
fn test_session_tiers_computed_from_noise() {
let session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
assert!(
(session.precision_tiers[1].absolute_tolerance - 0.024).abs() < 1e-10,
"Moderate tier = 3 × 0.008 = 0.024"
);
}
#[test]
fn test_set_goal_moderate_accepted() {
let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
assert!(
session.set_goal(PrecisionLevel::Moderate).is_ok(),
"Moderate (3.0×) must be accepted"
);
assert!(session.is_ready());
}
#[test]
fn test_set_goal_max_accepted_with_marginal() {
let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
assert!(
session.set_goal(PrecisionLevel::Max).is_ok(),
"Max (1.0×, Marginal) must be accepted (with warning in message)"
);
}
#[test]
fn test_set_goal_stores_target_std() {
let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
session.set_goal(PrecisionLevel::Moderate).unwrap();
let target = session.target_std.unwrap();
assert!(
(target - 0.024).abs() < 1e-10,
"target_std = 3.0 × 0.008 = 0.024"
);
}
#[test]
fn test_to_active_learning_config_requires_goal() {
let session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
let result = session.to_active_learning_config();
assert!(result.is_err(), "must fail before set_goal() is called");
}
#[test]
fn test_to_active_learning_config_correct_values() {
let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
session.set_goal(PrecisionLevel::Moderate).unwrap();
let config = session.to_active_learning_config().unwrap();
assert!((config.target_std - 0.024).abs() < 1e-10, "target_std");
assert!((config.noise_floor - 0.008).abs() < 1e-10, "noise_floor");
assert_eq!(config.max_iterations, 100);
}
#[test]
fn test_set_goal_can_be_changed() {
let mut session = NoiseCalibrationSession::new(SensorType::Hall, make_noise());
session.set_goal(PrecisionLevel::Low).unwrap();
session.set_goal(PrecisionLevel::Moderate).unwrap();
let target = session.target_std.unwrap();
assert!(
(target - 0.024).abs() < 1e-10,
"goal update: target should reflect latest selection"
);
}
}