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}