Skip to main content

pamoja_kit/
calibration.rs

1//! Turning a raw reading into real-world units.
2
3/// A linear map from raw sensor counts to calibrated units.
4///
5/// Most cheap analog sensors report arbitrary counts - ADC steps, a raw voltage -
6/// that mean nothing until they are converted to real units. A [`Calibration`]
7/// applies the line `value = scale * raw + offset`. Build it from two readings
8/// whose true values are known, then apply it to every sample.
9///
10/// # Examples
11///
12/// ```
13/// use pamoja_kit::Calibration;
14///
15/// // A humidity probe reads 0.5 V at 0 % and 2.5 V at 100 %.
16/// let humidity = Calibration::two_point(0.5, 0.0, 2.5, 100.0);
17/// assert_eq!(humidity.apply(1.5), 50.0);
18/// ```
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Calibration {
21    scale: f32,
22    offset: f32,
23}
24
25impl Calibration {
26    /// Builds a calibration from a scale and offset directly.
27    ///
28    /// # Arguments
29    ///
30    /// * `scale` - the multiplier applied to a raw reading.
31    /// * `offset` - the constant added after scaling.
32    ///
33    /// # Returns
34    ///
35    /// The calibration `value = scale * raw + offset`.
36    pub fn linear(scale: f32, offset: f32) -> Self {
37        Self { scale, offset }
38    }
39
40    /// Builds a calibration from two known `(raw, value)` points.
41    ///
42    /// # Arguments
43    ///
44    /// * `raw_low` - a raw reading.
45    /// * `value_low` - the true value at `raw_low`.
46    /// * `raw_high` - another raw reading.
47    /// * `value_high` - the true value at `raw_high`.
48    ///
49    /// # Returns
50    ///
51    /// The line through both points. If the two raw readings are equal the slope is
52    /// undefined, so the calibration falls back to the constant `value_low`.
53    pub fn two_point(raw_low: f32, value_low: f32, raw_high: f32, value_high: f32) -> Self {
54        let span = raw_high - raw_low;
55        let scale = if span == 0.0 {
56            0.0
57        } else {
58            (value_high - value_low) / span
59        };
60        Self {
61            scale,
62            offset: value_low - scale * raw_low,
63        }
64    }
65
66    /// Converts a raw reading into calibrated units.
67    ///
68    /// # Arguments
69    ///
70    /// * `raw` - the uncalibrated sensor reading.
71    ///
72    /// # Returns
73    ///
74    /// The calibrated value.
75    pub fn apply(&self, raw: f32) -> f32 {
76        self.scale * raw + self.offset
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn two_point_maps_its_endpoints_exactly() {
86        let calibration = Calibration::two_point(0.5, 0.0, 2.5, 100.0);
87        assert!((calibration.apply(0.5) - 0.0).abs() < 1e-4);
88        assert!((calibration.apply(2.5) - 100.0).abs() < 1e-4);
89    }
90
91    #[test]
92    fn two_point_interpolates_linearly() {
93        let calibration = Calibration::two_point(0.5, 0.0, 2.5, 100.0);
94        assert!((calibration.apply(1.5) - 50.0).abs() < 1e-4);
95    }
96
97    #[test]
98    fn equal_raw_points_fall_back_to_a_constant() {
99        let calibration = Calibration::two_point(1.0, 42.0, 1.0, 99.0);
100        assert!((calibration.apply(5.0) - 42.0).abs() < 1e-4);
101    }
102
103    #[test]
104    fn linear_applies_scale_and_offset() {
105        let calibration = Calibration::linear(2.0, -1.0);
106        assert!((calibration.apply(3.0) - 5.0).abs() < 1e-4);
107    }
108}