blr_active/active_learning/
multi_sensor.rs1use crate::active_learning::acquisition::RecommendedSample;
8use crate::active_learning::orchestration::{CalibrationSession, IterationOutcome, SessionConfig};
9
10#[derive(Debug, Clone)]
12pub struct MultiSensorRecommendation {
13 pub sensor_id: String,
15 pub recommendations: Vec<RecommendedSample>,
17 pub current_p95_std: f64,
19 pub goal_met: bool,
21}
22
23pub struct MultiSensorSession {
29 sessions: Vec<(String, CalibrationSession)>,
30}
31
32impl MultiSensorSession {
33 pub fn new() -> Self {
35 Self {
36 sessions: Vec::new(),
37 }
38 }
39
40 pub fn add_sensor(
48 &mut self,
49 sensor_id: impl Into<String>,
50 config: SessionConfig,
51 feature_fn: impl Fn(f64) -> Vec<f64> + 'static,
52 feature_dim: usize,
53 ) {
54 let session = CalibrationSession::new(config, feature_fn, feature_dim);
55 self.sessions.push((sensor_id.into(), session));
56 }
57
58 pub fn add_measurement(&mut self, sensor_id: &str, raw_input: f64, measured_output: f64) {
60 if let Some((_, session)) = self.sessions.iter_mut().find(|(id, _)| id == sensor_id) {
61 session.add_measurement(raw_input, measured_output);
62 }
63 }
64
65 pub fn next_iteration_all(&mut self) -> Vec<MultiSensorRecommendation> {
71 let mut results = Vec::new();
72
73 for (sensor_id, session) in &mut self.sessions {
74 let outcome = session.next_iteration();
75
76 let (recs, goal_met) = match &outcome {
77 IterationOutcome::RecommendNext(r) => (r.clone(), false),
78 IterationOutcome::PrecisionMet => (Vec::new(), true),
79 IterationOutcome::NoiseFloorHit(_) => (Vec::new(), false),
80 IterationOutcome::MaxIterationsReached | IterationOutcome::FitError(_) => {
81 (Vec::new(), false)
82 }
83 };
84
85 let current_p95 = session
86 .precision_history
87 .last()
88 .map(|r| r.percentile_95_std)
89 .unwrap_or(f64::INFINITY);
90
91 results.push(MultiSensorRecommendation {
92 sensor_id: sensor_id.clone(),
93 recommendations: recs,
94 current_p95_std: current_p95,
95 goal_met,
96 });
97 }
98
99 results
100 }
101
102 pub fn all_goals_met(&self) -> bool {
104 self.sessions.iter().all(|(_, session)| {
105 session
106 .precision_history
107 .last()
108 .map(|r| r.goal_met)
109 .unwrap_or(false)
110 })
111 }
112
113 pub fn precision_summary(&self) -> Vec<(String, f64)> {
115 self.sessions
116 .iter()
117 .map(|(id, session)| {
118 let p95 = session
119 .precision_history
120 .last()
121 .map(|r| r.percentile_95_std)
122 .unwrap_or(f64::INFINITY);
123 (id.clone(), p95)
124 })
125 .collect()
126 }
127}
128
129impl Default for MultiSensorSession {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn polynomial_feature(x: f64) -> Vec<f64> {
140 vec![1.0, x, x * x]
141 }
142
143 fn make_config(input_max: f64, target: f64) -> SessionConfig {
144 SessionConfig {
145 target_precision: target,
146 max_iterations: 10,
147 top_k: 2,
148 grid_resolution: 20,
149 input_range: (0.0, input_max),
150 ..Default::default()
151 }
152 }
153
154 #[test]
156 fn test_independent_precision_tracking() {
157 let mut ms = MultiSensorSession::new();
158 ms.add_sensor("hall", make_config(100.0, 0.5), polynomial_feature, 3);
159 ms.add_sensor("gas", make_config(50.0, 0.3), polynomial_feature, 3);
160
161 for i in 0..5 {
163 ms.add_measurement("hall", i as f64 * 20.0, i as f64 * 2.0 + 0.01);
164 }
165 for i in 0..5 {
167 ms.add_measurement("gas", i as f64 * 10.0, i as f64 * 0.5 + 0.005);
168 }
169
170 let results = ms.next_iteration_all();
171 assert_eq!(results.len(), 2, "should have results for both sensors");
172
173 let hall = results.iter().find(|r| r.sensor_id == "hall").unwrap();
175 let gas = results.iter().find(|r| r.sensor_id == "gas").unwrap();
176 assert!(hall.current_p95_std.is_finite());
179 assert!(gas.current_p95_std.is_finite());
180 }
181
182 #[test]
184 fn test_all_goals_not_met_initially() {
185 let mut ms = MultiSensorSession::new();
186 ms.add_sensor("hall", make_config(100.0, 0.0001), polynomial_feature, 3); ms.add_measurement("hall", 0.0, 0.0);
188 ms.add_measurement("hall", 50.0, 1.0);
189 ms.next_iteration_all();
190 assert!(!ms.all_goals_met());
191 }
192}