deimos 0.16.2

Control-loop and data pipeline for the Deimos data acquisition system
Documentation
//! Derive input voltage from linear amplifier reading

#[cfg(feature = "python")]
use pyo3::prelude::*;

use super::*;
use crate::{calc_config, calc_input_names, calc_output_names, py_json_methods};

/// Derive input voltage from linear amplifier reading
///
/// First subtracts the output offset, then divides by the slope.
#[cfg_attr(feature = "python", pyclass)]
#[derive(Serialize, Deserialize, Default, Debug)]
pub struct InverseAffine {
    // User inputs
    input_name: String,
    slope: f64,
    offset: f64,
    save_outputs: bool,
    #[serde(default)]
    output_unit: Option<String>,

    // Values provided by calc orchestrator during init
    #[serde(skip)]
    input_index: usize,

    #[serde(skip)]
    output_index: usize,
}

impl InverseAffine {
    pub fn new(input_name: String, slope: f64, offset: f64, save_outputs: bool) -> Box<Self> {
        // These will be set during init.
        // Use default indices that will cause an error on the first call if not initialized properly
        let input_index = usize::MAX;
        let output_index = usize::MAX;

        Box::new(Self {
            input_name,
            slope,
            offset,
            save_outputs,
            output_unit: None,

            input_index,
            output_index,
        })
    }

    /// Attach an output unit label (builder method).
    pub fn with_output_unit(mut self: Box<Self>, unit: impl Into<String>) -> Box<Self> {
        self.output_unit = Some(unit.into());
        self
    }
}

py_json_methods!(
    InverseAffine,
    Calc,
    #[new]
    #[pyo3(signature = (input_name, slope, offset, save_outputs, output_unit = None))]
    fn py_new(
        input_name: String,
        slope: f64,
        offset: f64,
        save_outputs: bool,
        output_unit: Option<String>,
    ) -> Self {
        let mut calc = Self::new(input_name, slope, offset, save_outputs);
        calc.output_unit = output_unit;
        *calc
    }
);

#[typetag::serde]
impl Calc for InverseAffine {
    /// Reset internal state and register calc tape indices
    fn init(
        &mut self,
        _: ControllerCtx,
        input_indices: Vec<usize>,
        output_range: Range<usize>,
    ) -> Result<(), String> {
        self.input_index = input_indices[0];
        self.output_index = output_range.clone().next().unwrap();
        Ok(())
    }

    fn terminate(&mut self) -> Result<(), String> {
        self.input_index = usize::MAX;
        self.output_index = usize::MAX;
        Ok(())
    }

    /// Run calcs for a cycle
    fn eval(&mut self, tape: &mut [f64]) -> Result<(), String> {
        let x = tape[self.input_index];
        let y = (x - self.offset) / self.slope;

        tape[self.output_index] = y;
        Ok(())
    }

    /// Map from input field names (like `v`, without prefix) to the state name
    /// that the input should draw from (like `peripheral_0.output_1`, with prefix)
    fn get_input_map(&self) -> BTreeMap<CalcInputName, FieldName> {
        let mut map = BTreeMap::new();
        map.insert("x".to_owned(), self.input_name.clone());
        map
    }

    /// Change a value in the input map
    fn update_input_map(&mut self, field: &str, source: &str) -> Result<(), String> {
        if field == "x" {
            self.input_name = source.to_owned();
        } else {
            return Err(format!("Unrecognized field {field}"));
        }

        Ok(())
    }

    fn get_output_units(&self) -> Vec<Option<String>> {
        vec![self.output_unit.clone()]
    }

    calc_config!(slope, offset);
    calc_input_names!(x);
    calc_output_names!(y);
}