deimos 0.16.2

Control-loop and data pipeline for the Deimos data acquisition system
Documentation
//! Calculations that are run at each cycle during operation.
//!
//! `Calc` objects are registered with the `CalcOrchestrator` and serialized with the controller.
//! Each calc is a function consuming any number of inputs and producing any number of outputs.
use std::any::type_name;
use std::fmt::Debug;
use std::iter::Iterator;
use std::{collections::BTreeMap, ops::Range};

use serde::{Deserialize, Serialize};

mod orchestrator;
pub(crate) use orchestrator::CalcOrchestrator;

// Specific calc implementations

mod affine;
mod butter;
mod constant;
mod inverse_affine;
mod pid;
mod polynomial;
mod rtd_pt100;
mod sin;
mod tc_ktype;

pub mod sequence_machine;

pub use affine::Affine;
pub use butter::Butter2;
pub use constant::Constant;
pub use inverse_affine::InverseAffine;
pub use pid::Pid;
pub use polynomial::Polynomial;
pub use rtd_pt100::RtdPt100;
pub use sequence_machine::SequenceMachine;
pub use sin::Sin;
pub use tc_ktype::TcKtype;

use crate::ControllerCtx;

// Type aliases for clarification purposes, since
// there will be a lot of strings and usize ints
pub type PeripheralName = String;
pub type PeripheralInputName = String;
pub type FieldName = String;

pub type CalcName = String;
pub type CalcInputName = String;
pub type CalcOutputName = String;
pub type CalcConfigName = String;

pub type SrcIndex = usize;
pub type DstIndex = usize;

/// Clone isn't inherently object-safe, so to be able to clone dyn trait objects,
/// we send it for a loop through the serde typetag system, which provides an
/// automatically-assembled vtable to determine the downcasted type and clone into it.
impl Clone for Box<dyn Calc> {
    fn clone(&self) -> Box<dyn Calc> {
        let new: Box<dyn Calc> =
            serde_json::from_str(&serde_json::to_string(&self).unwrap()).unwrap();
        new
    }
}

/// A calculation that takes some inputs and produces some outputs
/// at each timestep, and may have some persistent internal state.
#[typetag::serde(tag = "type")]
pub trait Calc: Send + Sync + Debug {
    /// Reset internal state and register calc tape indices
    fn init(
        &mut self,
        ctx: ControllerCtx,
        input_indices: Vec<usize>,
        output_range: Range<usize>,
    ) -> Result<(), String>;

    /// Clear state to reset for another run
    fn terminate(&mut self) -> Result<(), String>;

    /// Run calcs for a cycle
    fn eval(&mut self, tape: &mut [f64]) -> Result<(), String>;

    /// 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>;

    /// Change a value in the input map
    fn update_input_map(&mut self, field: &str, source: &str) -> Result<(), String>;

    //
    // Everything below this point can be macro-generated

    /// Get flag for whether to save outputs
    fn get_save_outputs(&self) -> bool;

    /// Set flag for whether to save outputs
    fn set_save_outputs(&mut self, save_outputs: bool);

    /// Get config field values
    fn get_config(&self) -> BTreeMap<String, f64>;

    /// Apply config field values
    fn set_config(&mut self, cfg: &BTreeMap<String, f64>) -> Result<(), String>;

    //
    // These are needed to maintain strict ordering for indexed evaluation

    /// List of input field names in the order that they will be consumed
    fn get_input_names(&self) -> Vec<CalcInputName>;

    /// List of output field names in the order that they will be written out
    fn get_output_names(&self) -> Vec<CalcOutputName>;

    /// List of optional unit strings for each output, parallel to `get_output_names`.
    /// Must return a `Vec` whose length equals `get_output_names().len()`.
    /// `None` indicates the unit is unknown or not applicable for that output.
    ///
    /// The default implementation returns `vec![None; N]` where `N` is the number of outputs,
    /// which is correct for calcs that do not transform engineering units (pass-through or
    /// untyped outputs). Override when the calc declares concrete output units.
    fn get_output_units(&self) -> Vec<Option<String>> {
        vec![None; self.get_output_names().len()]
    }

    /// Get the type name, which is guaranteed to be unique among implementations of the trait
    /// because of the use of a global vtable for serialization, and guaranteed not to include
    /// non-'static lifetimes due to trait bounds.
    fn kind(&self) -> String {
        type_name::<Self>().split(":").last().unwrap().into()
    }
}

/// Build functions for getting and setting calc config fields
#[macro_export]
macro_rules! calc_config {
    ($( $field:ident ),*) => {
        /// Get flag for whether to save outputs
        fn get_save_outputs(&self) -> bool {
            self.save_outputs
        }

        /// Set flag for whether to save outputs
        fn set_save_outputs(&mut self, save_outputs: bool) {
            self.save_outputs = save_outputs;
        }

        /// Get config field values
        fn get_config(&self) -> BTreeMap<String, f64> {
            #[allow(unused_mut)]
            let mut cfg = BTreeMap::<String, f64>::new();
            $({cfg.insert(stringify!($field).to_owned(), self.$field);})*

            cfg
        }

        /// Apply config field values
        #[allow(unused)]
        fn set_config(&mut self, cfg: &BTreeMap<String, f64>) -> Result<(), String> {
            $({
                let f = stringify!($field);
                self.$field = *cfg.get(f).ok_or(format!("Config missing key `{f}`"))?;
            })*

            Ok(())
        }
    }
}

/// Build function for getting calc input field names
#[macro_export]
macro_rules! calc_input_names {
    ($( $field:ident ),*) => {
        /// List of input field names in the order that they will be consumed
        fn get_input_names(&self) -> Vec<CalcInputName> {
            #[allow(unused_mut)]
            let mut names = vec![];
            $({
                names.push(stringify!($field).to_owned());
            })*

            names
        }
    }
}

/// Build function for getting calc output field names
#[macro_export]
macro_rules! calc_output_names {
    ($( $field:ident ),*) => {
        /// List of input field names in the order that they will be consumed
        fn get_output_names(&self) -> Vec<CalcOutputName> {
            let mut names = vec![];
            $({
                names.push(stringify!($field).to_owned());
            })*

            names
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn assert_units_len_matches_names_len(calc: &dyn Calc) {
        assert_eq!(
            calc.get_output_units().len(),
            calc.get_output_names().len(),
            "get_output_units().len() != get_output_names().len() for {}",
            calc.kind()
        );
    }

    #[test]
    fn affine_units_len_matches_names_len() {
        let calc = Affine::new("x".to_owned(), 1.0, 0.0, false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn inverse_affine_units_len_matches_names_len() {
        let calc = InverseAffine::new("x".to_owned(), 1.0, 0.0, false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn polynomial_units_len_matches_names_len() {
        let calc = Polynomial::new("x".to_owned(), vec![1.0, 2.0], String::new(), false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn constant_units_len_matches_names_len() {
        let calc = Constant::new(0.0, false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn sin_units_len_matches_names_len() {
        let calc = Sin::new(1.0, 0.0, -1.0, 1.0, false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn butter2_units_len_matches_names_len() {
        let calc = Butter2::new("x".to_owned(), 10.0, false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn pid_units_len_matches_names_len() {
        let calc = Pid::new(
            "measurement".to_owned(),
            "setpoint".to_owned(),
            1.0,
            0.0,
            0.0,
            100.0,
            false,
        );
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn rtd_pt100_units_len_matches_names_len() {
        let calc = RtdPt100::new("resistance_ohm".to_owned(), false);
        assert_units_len_matches_names_len(&*calc);
    }

    #[test]
    fn tc_ktype_units_len_matches_names_len() {
        let calc = TcKtype::new("voltage_V".to_owned(), "cold_junction_K".to_owned(), false);
        assert_units_len_matches_names_len(&*calc);
    }
}