libcaliph/
routines.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2License, v. 2.0. If a copy of the MPL was not distributed with this
3file, You can obtain one at https://mozilla.org/MPL/2.0/.
4Copyright 2021 Peter Dunne */
5//! # Routines Module
6//! Provides the functions needed to calibrate a pH meter, and to perform the conversion of a measurement with a known calibration.
7
8use super::fit;
9use super::{PH10_STATIC, PH4_STATIC, TEMP_STATIC};
10use float_cmp::ApproxEq;
11use splines::{Interpolation, Key, Spline};
12
13/// Calibration struct as a convenience wrapper.
14///
15/// This includes optional elements for goodness of fit variables. The calibration model is linear, i.e. $`y  = m x + c`$
16pub struct Calibration<F> {
17    ///
18    pub slope: F,
19    pub offset: F,
20    pub rms: Option<F>,
21    pub rsq: Option<F>,
22}
23
24/// Implements ApproxEq trait for Calibration struct
25impl<'a, M: Copy + Default, F: Copy + ApproxEq<Margin = M>> ApproxEq for &'a Calibration<F> {
26    type Margin = M;
27
28    fn approx_eq<T: Into<Self::Margin>>(self, other: Self, margin: T) -> bool {
29        let margin = margin.into();
30        self.slope.approx_eq(other.slope, margin) && self.offset.approx_eq(other.offset, margin)
31    }
32}
33
34impl<F> Calibration<F>
35where
36    F: Copy,
37{
38    pub fn new(slope: F, offset: F, rms: Option<F>, rsq: Option<F>) -> Calibration<F> {
39        Calibration {
40            slope,
41            offset,
42            rms,
43            rsq,
44        }
45    }
46    /// Modifies the slope
47    pub fn with_slope(&self, slope: F) -> Calibration<F> {
48        Calibration {
49            slope,
50            offset: self.offset,
51            rms: self.rms,
52            rsq: self.rsq,
53        }
54    }
55
56    // Modifies with offset
57    pub fn with_offset(&self, offset: F) -> Calibration<F> {
58        Calibration {
59            slope: self.slope,
60            offset,
61            rms: self.rms,
62            rsq: self.rsq,
63        }
64    }
65}
66
67impl<F> Default for Calibration<F>
68where
69    F: Default,
70{
71    fn default() -> Self {
72        Calibration {
73            slope: F::default(),
74            offset: F::default(),
75            rms: None,
76            rsq: None,
77        }
78    }
79}
80
81/// Calculates the calibration values at give temperature for the measured pH values
82pub fn ph_calibration(ph_measured: &[f64; 2], temperature: &f64) -> Calibration<f64> {
83    let ph4_cal = interp_ph4(temperature).unwrap_or(4.01);
84    let ph10_cal = interp_ph10(temperature).unwrap_or(10.01);
85
86    let ph_cal = [ph4_cal, ph10_cal];
87
88    let calibration = fit::fit(ph_measured, &ph_cal);
89    let fit_eval = fit::evaluate(ph_measured, &ph_cal, &calibration);
90
91    Calibration::new(
92        calibration[0],
93        calibration[1],
94        Some(fit_eval[0]),
95        Some(fit_eval[1]),
96    )
97}
98
99/// Converts the measured pH to a calibrated one using a known calibration
100pub fn ph_convert(ph_measured: &f64, calibration: &[f64; 2]) -> f64 {
101    fit::predict(ph_measured, calibration)
102}
103
104/// Interpolates the temperature dependence of a pH 4.01 buffer solution to give an arbitrary pH value between 5 to 95˚C
105pub fn interp_ph4(temperature: &f64) -> Option<f64> {
106    let pairs_iter = TEMP_STATIC.iter().zip(PH4_STATIC.iter());
107    let zipped_points: Vec<_> = pairs_iter
108        .map(|(x, y)| Key::new(*x, *y, Interpolation::Linear))
109        .collect();
110
111    let spline = Spline::from_vec(zipped_points);
112
113    spline.sample(*temperature)
114}
115
116/// Interpolates the temperature dependence of a pH 10.01 buffer solution to give an arbitrary pH value between 5 to 95˚C
117pub fn interp_ph10(temperature: &f64) -> Option<f64> {
118    let pairs_iter = TEMP_STATIC.iter().zip(PH10_STATIC.iter());
119    let zipped_points: Vec<_> = pairs_iter
120        .map(|(x, y)| Key::new(*x, *y, Interpolation::Linear))
121        .collect();
122
123    let spline = Spline::from_vec(zipped_points);
124
125    spline.sample(*temperature)
126}
127
128#[cfg(test)]
129mod tests {
130    use float_cmp::approx_eq;
131
132    use crate::routines::Calibration;
133
134    use super::ph_calibration;
135
136    #[test]
137    fn test_ph_calibration() {
138        let temperature = 21.0;
139        let ph_measured = [3.75, 9.49];
140        let res = ph_calibration(&ph_measured, &temperature);
141        let slope = 1.053658536585366;
142        let offset = 0.05078048780487787;
143        let test_calib = Calibration::default().with_slope(slope).with_offset(offset);
144
145        assert!(approx_eq!(&Calibration<f64>, &res, &test_calib))
146    }
147}