1use super::fit;
9use super::{PH10_STATIC, PH4_STATIC, TEMP_STATIC};
10use float_cmp::ApproxEq;
11use splines::{Interpolation, Key, Spline};
12
13pub struct Calibration<F> {
17 pub slope: F,
19 pub offset: F,
20 pub rms: Option<F>,
21 pub rsq: Option<F>,
22}
23
24impl<'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 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 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
81pub 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
99pub fn ph_convert(ph_measured: &f64, calibration: &[f64; 2]) -> f64 {
101 fit::predict(ph_measured, calibration)
102}
103
104pub 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
116pub 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}