use crate::active_learning::acquisition::RecommendedSample;
use crate::active_learning::orchestration::{CalibrationSession, IterationOutcome, SessionConfig};
#[derive(Debug, Clone)]
pub struct MultiSensorRecommendation {
pub sensor_id: String,
pub recommendations: Vec<RecommendedSample>,
pub current_p95_std: f64,
pub goal_met: bool,
}
pub struct MultiSensorSession {
sessions: Vec<(String, CalibrationSession)>,
}
impl MultiSensorSession {
pub fn new() -> Self {
Self {
sessions: Vec::new(),
}
}
pub fn add_sensor(
&mut self,
sensor_id: impl Into<String>,
config: SessionConfig,
feature_fn: impl Fn(f64) -> Vec<f64> + 'static,
feature_dim: usize,
) {
let session = CalibrationSession::new(config, feature_fn, feature_dim);
self.sessions.push((sensor_id.into(), session));
}
pub fn add_measurement(&mut self, sensor_id: &str, raw_input: f64, measured_output: f64) {
if let Some((_, session)) = self.sessions.iter_mut().find(|(id, _)| id == sensor_id) {
session.add_measurement(raw_input, measured_output);
}
}
pub fn next_iteration_all(&mut self) -> Vec<MultiSensorRecommendation> {
let mut results = Vec::new();
for (sensor_id, session) in &mut self.sessions {
let outcome = session.next_iteration();
let (recs, goal_met) = match &outcome {
IterationOutcome::RecommendNext(r) => (r.clone(), false),
IterationOutcome::PrecisionMet => (Vec::new(), true),
IterationOutcome::NoiseFloorHit(_) => (Vec::new(), false),
IterationOutcome::MaxIterationsReached | IterationOutcome::FitError(_) => {
(Vec::new(), false)
}
};
let current_p95 = session
.precision_history
.last()
.map(|r| r.percentile_95_std)
.unwrap_or(f64::INFINITY);
results.push(MultiSensorRecommendation {
sensor_id: sensor_id.clone(),
recommendations: recs,
current_p95_std: current_p95,
goal_met,
});
}
results
}
pub fn all_goals_met(&self) -> bool {
self.sessions.iter().all(|(_, session)| {
session
.precision_history
.last()
.map(|r| r.goal_met)
.unwrap_or(false)
})
}
pub fn precision_summary(&self) -> Vec<(String, f64)> {
self.sessions
.iter()
.map(|(id, session)| {
let p95 = session
.precision_history
.last()
.map(|r| r.percentile_95_std)
.unwrap_or(f64::INFINITY);
(id.clone(), p95)
})
.collect()
}
}
impl Default for MultiSensorSession {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn polynomial_feature(x: f64) -> Vec<f64> {
vec![1.0, x, x * x]
}
fn make_config(input_max: f64, target: f64) -> SessionConfig {
SessionConfig {
target_precision: target,
max_iterations: 10,
top_k: 2,
grid_resolution: 20,
input_range: (0.0, input_max),
..Default::default()
}
}
#[test]
fn test_independent_precision_tracking() {
let mut ms = MultiSensorSession::new();
ms.add_sensor("hall", make_config(100.0, 0.5), polynomial_feature, 3);
ms.add_sensor("gas", make_config(50.0, 0.3), polynomial_feature, 3);
for i in 0..5 {
ms.add_measurement("hall", i as f64 * 20.0, i as f64 * 2.0 + 0.01);
}
for i in 0..5 {
ms.add_measurement("gas", i as f64 * 10.0, i as f64 * 0.5 + 0.005);
}
let results = ms.next_iteration_all();
assert_eq!(results.len(), 2, "should have results for both sensors");
let hall = results.iter().find(|r| r.sensor_id == "hall").unwrap();
let gas = results.iter().find(|r| r.sensor_id == "gas").unwrap();
assert!(hall.current_p95_std.is_finite());
assert!(gas.current_p95_std.is_finite());
}
#[test]
fn test_all_goals_not_met_initially() {
let mut ms = MultiSensorSession::new();
ms.add_sensor("hall", make_config(100.0, 0.0001), polynomial_feature, 3); ms.add_measurement("hall", 0.0, 0.0);
ms.add_measurement("hall", 50.0, 1.0);
ms.next_iteration_all();
assert!(!ms.all_goals_met());
}
}