kizzasi_io/
calibration.rs

1//! Sensor calibration and data correction utilities
2//!
3//! This module provides tools for calibrating sensors and correcting systematic
4//! errors in measurement data.
5
6use crate::error::{IoError, IoResult};
7use scirs2_core::ndarray::Array1;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Calibration curve for non-linear sensor response
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CalibrationCurve {
14    /// Input points (raw sensor values)
15    pub input_points: Vec<f32>,
16    /// Output points (calibrated values)
17    pub output_points: Vec<f32>,
18}
19
20impl CalibrationCurve {
21    /// Create new calibration curve
22    pub fn new(input_points: Vec<f32>, output_points: Vec<f32>) -> IoResult<Self> {
23        if input_points.len() != output_points.len() {
24            return Err(IoError::InvalidConfig(
25                "Input and output points must have same length".to_string(),
26            ));
27        }
28        if input_points.len() < 2 {
29            return Err(IoError::InvalidConfig(
30                "Need at least 2 calibration points".to_string(),
31            ));
32        }
33
34        Ok(Self {
35            input_points,
36            output_points,
37        })
38    }
39
40    /// Apply calibration using linear interpolation
41    pub fn apply(&self, value: f32) -> f32 {
42        // Find bracketing points
43        let mut lower_idx = 0;
44        let mut upper_idx = self.input_points.len() - 1;
45
46        for (i, &input) in self.input_points.iter().enumerate() {
47            if value >= input {
48                lower_idx = i;
49            }
50            if value <= input && i < upper_idx {
51                upper_idx = i;
52                break;
53            }
54        }
55
56        if lower_idx == upper_idx {
57            return self.output_points[lower_idx];
58        }
59
60        // Linear interpolation
61        let x0 = self.input_points[lower_idx];
62        let x1 = self.input_points[upper_idx];
63        let y0 = self.output_points[lower_idx];
64        let y1 = self.output_points[upper_idx];
65
66        if (x1 - x0).abs() < 1e-10 {
67            return y0;
68        }
69
70        let t = (value - x0) / (x1 - x0);
71        y0 + t * (y1 - y0)
72    }
73}
74
75/// Calibration parameters for a sensor
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CalibrationParams {
78    /// Zero offset (bias)
79    pub offset: f32,
80    /// Scale factor (gain)
81    pub scale: f32,
82    /// Temperature coefficient (optional)
83    pub temp_coefficient: Option<f32>,
84    /// Non-linear calibration curve (optional)
85    pub curve: Option<CalibrationCurve>,
86    /// Custom parameters
87    pub custom: HashMap<String, f32>,
88}
89
90impl Default for CalibrationParams {
91    fn default() -> Self {
92        Self {
93            offset: 0.0,
94            scale: 1.0,
95            temp_coefficient: None,
96            curve: None,
97            custom: HashMap::new(),
98        }
99    }
100}
101
102impl CalibrationParams {
103    /// Create identity calibration (no correction)
104    pub fn identity() -> Self {
105        Self::default()
106    }
107
108    /// Create calibration from offset and scale
109    pub fn from_offset_scale(offset: f32, scale: f32) -> Self {
110        Self {
111            offset,
112            scale,
113            ..Default::default()
114        }
115    }
116
117    /// Apply calibration to single value
118    pub fn calibrate(&self, raw_value: f32, temperature: Option<f32>) -> f32 {
119        let mut value = raw_value;
120
121        // Apply offset and scale
122        value = (value - self.offset) * self.scale;
123
124        // Apply temperature compensation
125        if let (Some(temp_coef), Some(temp)) = (self.temp_coefficient, temperature) {
126            value *= 1.0 + temp_coef * (temp - 25.0); // 25°C reference
127        }
128
129        // Apply non-linear curve
130        if let Some(ref curve) = self.curve {
131            value = curve.apply(value);
132        }
133
134        value
135    }
136
137    /// Apply calibration to array of values
138    pub fn calibrate_array(&self, raw_values: &[f32], temperature: Option<f32>) -> Array1<f32> {
139        Array1::from_vec(
140            raw_values
141                .iter()
142                .map(|&v| self.calibrate(v, temperature))
143                .collect(),
144        )
145    }
146}
147
148/// Multi-point calibration using least squares
149pub struct MultiPointCalibrator {
150    /// Reference values
151    reference: Vec<f32>,
152    /// Measured values
153    measured: Vec<f32>,
154}
155
156impl MultiPointCalibrator {
157    /// Create new multi-point calibrator
158    pub fn new() -> Self {
159        Self {
160            reference: Vec::new(),
161            measured: Vec::new(),
162        }
163    }
164
165    /// Add calibration point
166    pub fn add_point(&mut self, reference: f32, measured: f32) {
167        self.reference.push(reference);
168        self.measured.push(measured);
169    }
170
171    /// Compute calibration parameters using least squares
172    pub fn compute_calibration(&self) -> IoResult<CalibrationParams> {
173        if self.reference.len() < 2 {
174            return Err(IoError::InvalidConfig(
175                "Need at least 2 calibration points".to_string(),
176            ));
177        }
178
179        // Compute mean
180        let ref_mean: f32 = self.reference.iter().sum::<f32>() / self.reference.len() as f32;
181        let meas_mean: f32 = self.measured.iter().sum::<f32>() / self.measured.len() as f32;
182
183        // Compute covariance and variance
184        let mut covariance = 0.0f32;
185        let mut variance = 0.0f32;
186
187        for (&r, &m) in self.reference.iter().zip(self.measured.iter()) {
188            let r_diff = r - ref_mean;
189            let m_diff = m - meas_mean;
190            covariance += r_diff * m_diff;
191            variance += m_diff * m_diff;
192        }
193
194        if variance.abs() < 1e-10 {
195            return Err(IoError::InvalidConfig(
196                "Measured values have zero variance".to_string(),
197            ));
198        }
199
200        // Compute slope and intercept
201        // Linear regression: reference = a * measured + b
202        // where a = Cov(ref, meas) / Var(meas), b = ref_mean - a * meas_mean
203        // Calibration formula: calibrated = (raw - offset) * scale = reference
204        // Matching: scale = a, offset = meas_mean - ref_mean / scale
205        let scale = covariance / variance;
206        let offset = meas_mean - ref_mean / scale;
207
208        Ok(CalibrationParams::from_offset_scale(offset, scale))
209    }
210
211    /// Get number of calibration points
212    pub fn num_points(&self) -> usize {
213        self.reference.len()
214    }
215
216    /// Clear all calibration points
217    pub fn clear(&mut self) {
218        self.reference.clear();
219        self.measured.clear();
220    }
221}
222
223impl Default for MultiPointCalibrator {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229/// Calibration manager for multiple sensors
230pub struct CalibrationManager {
231    calibrations: HashMap<String, CalibrationParams>,
232}
233
234impl CalibrationManager {
235    /// Create new calibration manager
236    pub fn new() -> Self {
237        Self {
238            calibrations: HashMap::new(),
239        }
240    }
241
242    /// Add calibration for a sensor
243    pub fn add_calibration(&mut self, sensor_id: String, params: CalibrationParams) {
244        self.calibrations.insert(sensor_id, params);
245    }
246
247    /// Get calibration for a sensor
248    pub fn get_calibration(&self, sensor_id: &str) -> Option<&CalibrationParams> {
249        self.calibrations.get(sensor_id)
250    }
251
252    /// Remove calibration for a sensor
253    pub fn remove_calibration(&mut self, sensor_id: &str) -> Option<CalibrationParams> {
254        self.calibrations.remove(sensor_id)
255    }
256
257    /// Calibrate value from a sensor
258    pub fn calibrate(&self, sensor_id: &str, raw_value: f32, temperature: Option<f32>) -> f32 {
259        self.calibrations
260            .get(sensor_id)
261            .map(|cal| cal.calibrate(raw_value, temperature))
262            .unwrap_or(raw_value) // Return raw if no calibration
263    }
264
265    /// Calibrate array from a sensor
266    pub fn calibrate_array(
267        &self,
268        sensor_id: &str,
269        raw_values: &[f32],
270        temperature: Option<f32>,
271    ) -> Array1<f32> {
272        self.calibrations
273            .get(sensor_id)
274            .map(|cal| cal.calibrate_array(raw_values, temperature))
275            .unwrap_or_else(|| Array1::from_vec(raw_values.to_vec()))
276    }
277
278    /// Get number of registered sensors
279    pub fn num_sensors(&self) -> usize {
280        self.calibrations.len()
281    }
282
283    /// Save calibrations to JSON
284    pub fn to_json(&self) -> IoResult<String> {
285        serde_json::to_string_pretty(&self.calibrations).map_err(IoError::JsonError)
286    }
287
288    /// Load calibrations from JSON
289    pub fn from_json(json: &str) -> IoResult<Self> {
290        let calibrations: HashMap<String, CalibrationParams> =
291            serde_json::from_str(json).map_err(IoError::JsonError)?;
292        Ok(Self { calibrations })
293    }
294}
295
296impl Default for CalibrationManager {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302/// Auto-calibration using known reference signal
303pub struct AutoCalibrator {
304    /// Expected reference signal
305    reference: Vec<f32>,
306    /// Collected samples for calibration
307    samples: Vec<Vec<f32>>,
308    /// Number of samples to collect
309    num_samples: usize,
310}
311
312impl AutoCalibrator {
313    /// Create new auto-calibrator
314    pub fn new(reference: Vec<f32>, num_samples: usize) -> Self {
315        Self {
316            reference,
317            samples: Vec::new(),
318            num_samples,
319        }
320    }
321
322    /// Add measured sample
323    pub fn add_sample(&mut self, sample: Vec<f32>) -> IoResult<()> {
324        if sample.len() != self.reference.len() {
325            return Err(IoError::InvalidConfig("Sample length mismatch".to_string()));
326        }
327        self.samples.push(sample);
328        Ok(())
329    }
330
331    /// Check if ready to calibrate
332    pub fn is_ready(&self) -> bool {
333        self.samples.len() >= self.num_samples
334    }
335
336    /// Compute calibration from collected samples
337    pub fn compute_calibration(&self) -> IoResult<CalibrationParams> {
338        if !self.is_ready() {
339            return Err(IoError::InvalidConfig(
340                "Not enough samples collected".to_string(),
341            ));
342        }
343
344        // Average all samples
345        let mut averaged = vec![0.0f32; self.reference.len()];
346        for sample in &self.samples {
347            for (avg, &val) in averaged.iter_mut().zip(sample.iter()) {
348                *avg += val;
349            }
350        }
351        for avg in &mut averaged {
352            *avg /= self.samples.len() as f32;
353        }
354
355        // Use multi-point calibration
356        let mut calibrator = MultiPointCalibrator::new();
357        for (&ref_val, &meas_val) in self.reference.iter().zip(averaged.iter()) {
358            calibrator.add_point(ref_val, meas_val);
359        }
360
361        calibrator.compute_calibration()
362    }
363
364    /// Get number of samples collected
365    pub fn num_collected(&self) -> usize {
366        self.samples.len()
367    }
368
369    /// Reset calibrator
370    pub fn reset(&mut self) {
371        self.samples.clear();
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_calibration_params_basic() {
381        let params = CalibrationParams::from_offset_scale(10.0, 2.0);
382
383        // Test calibration: (raw - offset) * scale
384        // (30 - 10) * 2.0 = 40.0
385        let calibrated = params.calibrate(30.0, None);
386        assert_eq!(calibrated, 40.0);
387    }
388
389    #[test]
390    fn test_calibration_curve() {
391        let curve = CalibrationCurve::new(vec![0.0, 1.0, 2.0], vec![0.0, 10.0, 30.0]).unwrap();
392
393        // Test interpolation
394        assert_eq!(curve.apply(0.0), 0.0);
395        assert_eq!(curve.apply(1.0), 10.0);
396        assert_eq!(curve.apply(0.5), 5.0);
397        assert_eq!(curve.apply(1.5), 20.0);
398    }
399
400    #[test]
401    fn test_multipoint_calibrator() {
402        let mut calibrator = MultiPointCalibrator::new();
403
404        // Add points where reference = measured * 2
405        // So measured values need calibration of scale=2, offset=0
406        calibrator.add_point(2.0, 1.0); // ref=2, meas=1
407        calibrator.add_point(4.0, 2.0); // ref=4, meas=2
408        calibrator.add_point(6.0, 3.0); // ref=6, meas=3
409
410        let params = calibrator.compute_calibration().unwrap();
411
412        // Verify calibration: (1.0 - offset) * scale ≈ 2.0
413        let calibrated = params.calibrate(1.0, None);
414        assert!((calibrated - 2.0).abs() < 0.2);
415    }
416
417    #[test]
418    fn test_calibration_manager() {
419        let mut manager = CalibrationManager::new();
420
421        let params1 = CalibrationParams::from_offset_scale(0.0, 2.0);
422        let params2 = CalibrationParams::from_offset_scale(10.0, 1.0);
423
424        manager.add_calibration("sensor1".to_string(), params1);
425        manager.add_calibration("sensor2".to_string(), params2);
426
427        assert_eq!(manager.num_sensors(), 2);
428
429        let cal1 = manager.calibrate("sensor1", 5.0, None);
430        assert_eq!(cal1, 10.0); // 5.0 * 2.0
431
432        let cal2 = manager.calibrate("sensor2", 20.0, None);
433        assert_eq!(cal2, 10.0); // (20.0 - 10.0) * 1.0
434    }
435
436    #[test]
437    fn test_calibration_json_roundtrip() {
438        let mut manager = CalibrationManager::new();
439        manager.add_calibration(
440            "test_sensor".to_string(),
441            CalibrationParams::from_offset_scale(1.0, 2.0),
442        );
443
444        let json = manager.to_json().unwrap();
445        let loaded = CalibrationManager::from_json(&json).unwrap();
446
447        assert_eq!(loaded.num_sensors(), 1);
448        let cal = loaded.calibrate("test_sensor", 5.0, None);
449        assert_eq!(cal, 8.0); // (5.0 - 1.0) * 2.0
450    }
451
452    #[test]
453    fn test_auto_calibrator() {
454        let reference = vec![1.0, 2.0, 3.0, 4.0, 5.0];
455        let mut calibrator = AutoCalibrator::new(reference.clone(), 3);
456
457        assert!(!calibrator.is_ready());
458
459        // Add samples with known relationship: measured = reference * 2
460        // So we need scale=0.5 to recover reference from measured
461        calibrator
462            .add_sample(vec![2.0, 4.0, 6.0, 8.0, 10.0])
463            .unwrap();
464        calibrator
465            .add_sample(vec![2.0, 4.0, 6.0, 8.0, 10.0])
466            .unwrap();
467        calibrator
468            .add_sample(vec![2.0, 4.0, 6.0, 8.0, 10.0])
469            .unwrap();
470
471        assert!(calibrator.is_ready());
472
473        let params = calibrator.compute_calibration().unwrap();
474
475        // Calibrate measured value 2.0 should give reference 1.0
476        let calibrated = params.calibrate(2.0, None);
477        assert!((calibrated - 1.0).abs() < 0.2);
478    }
479}