deimos/calc/
polynomial.rs

1//! Evaluate an Nth order polynomial calibration curve.
2
3#[cfg(feature = "python")]
4use pyo3::prelude::*;
5
6use super::*;
7use crate::{
8    calc_config, calc_input_names, calc_output_names,
9    math::{polyfit, polyval},
10    py_json_methods,
11};
12
13/// Polynomial calibration: y = c0 + c1*x + c2*x^2 + ...
14/// with an attached note that should include traceability info
15/// like a sensor serial number.
16/// Coefficients ordered by increasing polynomial order.
17#[cfg_attr(feature = "python", pyclass)]
18#[derive(Serialize, Deserialize, Debug, Default)]
19pub struct Polynomial {
20    // User inputs
21    input_name: String,
22    coefficients: Vec<f64>,
23    note: String,
24    save_outputs: bool,
25
26    // Values provided by calc orchestrator during init
27    #[serde(skip)]
28    input_index: usize,
29
30    #[serde(skip)]
31    output_index: usize,
32}
33
34impl Polynomial {
35    pub fn new(
36        input_name: String,
37        coefficients: Vec<f64>,
38        note: String,
39        save_outputs: bool,
40    ) -> Box<Self> {
41        Box::new(Self {
42            input_name,
43            coefficients,
44            note,
45            save_outputs,
46            input_index: usize::MAX,
47            output_index: usize::MAX,
48        })
49    }
50
51    pub fn fit_from_points(
52        input_name: &str,
53        points: &[(f64, f64)],
54        order: usize,
55        note: &str,
56        save_outputs: bool,
57    ) -> Result<Box<Self>, String> {
58        let coefficients = polyfit(points, order)?;
59
60        Ok(Self::new(
61            input_name.into(),
62            coefficients,
63            note.into(),
64            save_outputs,
65        ))
66    }
67}
68
69py_json_methods!(
70    Polynomial,
71    Calc,
72    #[new]
73    fn py_new(
74        input_name: String,
75        coefficients: Vec<f64>,
76        note: String,
77        save_outputs: bool,
78    ) -> Self {
79        *Self::new(input_name, coefficients, note, save_outputs)
80    }
81);
82
83#[typetag::serde]
84impl Calc for Polynomial {
85    fn init(
86        &mut self,
87        _: ControllerCtx,
88        input_indices: Vec<usize>,
89        output_range: Range<usize>,
90    ) -> Result<(), String> {
91        if self.coefficients.is_empty() {
92            return Err("Polynomial coefficients cannot be empty".to_string());
93        }
94        self.input_index = input_indices
95            .first()
96            .copied()
97            .ok_or_else(|| "Polynomial calc missing input index".to_string())?;
98        self.output_index = output_range
99            .clone()
100            .next()
101            .ok_or_else(|| "Polynomial calc missing output index".to_string())?;
102        Ok(())
103    }
104
105    fn terminate(&mut self) -> Result<(), String> {
106        self.input_index = usize::MAX;
107        self.output_index = usize::MAX;
108        Ok(())
109    }
110
111    fn eval(&mut self, tape: &mut [f64]) -> Result<(), String> {
112        let x = tape[self.input_index];
113        let y = polyval(x, &self.coefficients);
114        tape[self.output_index] = y;
115        Ok(())
116    }
117
118    fn get_input_map(&self) -> BTreeMap<CalcInputName, FieldName> {
119        let mut map = BTreeMap::new();
120        map.insert("x".to_owned(), self.input_name.clone());
121        map
122    }
123
124    fn update_input_map(&mut self, field: &str, source: &str) -> Result<(), String> {
125        if field == "x" {
126            self.input_name = source.to_owned();
127            Ok(())
128        } else {
129            Err(format!("Unrecognized field {field}"))
130        }
131    }
132
133    calc_config!();
134    calc_input_names!(x);
135    calc_output_names!(y);
136}