zhl16 0.1.0

A no_std Rust implementation of core Bühlmann ZHL-16 tissue loading primitives.
Documentation
use crate::gas::{Gas, GasMixture};
use crate::quantities::{Frequency, FrequencyUnit, Pressure, PressureUnit, Time, TimeUnit};
use crate::tissue_constants::TISSUE_CONSTANTS;
use core::array::from_fn;
use core::fmt;

const WATER_VAPOR_PRESSURE: Pressure = Pressure::new_unchecked(0.0627, PressureUnit::Bar);

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum BodyModelError {
    NonPositiveTimeStep,
    InvalidGradientFactor,
}

impl fmt::Display for BodyModelError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BodyModelError::NonPositiveTimeStep => f.write_str("time step must be positive"),
            BodyModelError::InvalidGradientFactor => {
                f.write_str("gradient factor must be between 0.0 and 1.0")
            }
        }
    }
}

#[derive(Copy, Clone)]
pub(crate) struct TissueModel {
    gases: [TissueModelPerGas; 2],
}

impl TissueModel {
    pub(crate) const fn new(nitrogen: TissueModelPerGas, helium: TissueModelPerGas) -> Self {
        Self {
            gases: [nitrogen, helium],
        }
    }

    #[cfg(test)]
    pub(crate) fn get_gas(&self, gas: Gas) -> TissueModelPerGas {
        self.gases[gas as usize]
    }
}

#[derive(Copy, Clone)]
pub(crate) struct TissueModelPerGas {
    k: Frequency,
    a: Pressure,
    b: f64,
}

impl TissueModelPerGas {
    pub const fn new(half_time: Time, a: Pressure, b: f64) -> Self {
        Self {
            k: Frequency::new_unchecked(
                core::f64::consts::LN_2 / half_time.to(TimeUnit::Seconds),
                FrequencyUnit::Hertz,
            ),
            a,
            b,
        }
    }
    #[cfg(test)]
    pub(crate) fn get_k(&self) -> Frequency {
        self.k
    }
    pub fn get_a(&self) -> Pressure {
        self.a
    }
    pub fn get_b(&self) -> f64 {
        self.b
    }
}

#[derive(Copy, Clone)]
pub struct CompartmentState {
    pressures: [Pressure; 2],
    tissue_model: TissueModel,
}

impl CompartmentState {
    pub(crate) fn new(
        nitrogen_pressures: Pressure,
        helium_pressures: Pressure,
        tissue_model: TissueModel,
    ) -> Self {
        let mut pressures = [Pressure::zero(); 2];
        pressures[Gas::Nitrogen as usize] = nitrogen_pressures;
        pressures[Gas::Helium as usize] = helium_pressures;
        Self {
            pressures,
            tissue_model,
        }
    }

    pub(crate) fn evolve(
        &mut self,
        constants: &TissueModel,
        ambient_pressure: Pressure,
        gas_fractions: &GasMixture,
        dt: Time,
    ) {
        for gas in Gas::ALL {
            let idx = gas as usize;
            let tissue = &constants.gases[idx];
            let pressure = &mut self.pressures[idx];
            let fraction = gas_fractions.inert_fraction(gas);

            let inspired = (ambient_pressure - WATER_VAPOR_PRESSURE) * fraction;
            let rate = (inspired - *pressure) * tissue.k;
            *pressure += rate * dt;
        }
    }

    pub fn get_nitrogen_pressure(&self) -> Pressure {
        self.pressures[Gas::Nitrogen as usize]
    }

    pub fn get_helium_pressure(&self) -> Pressure {
        self.pressures[Gas::Helium as usize]
    }

    pub fn safe_ambient_pressure(&self, gf: f64) -> Result<Pressure, BodyModelError> {
        if !(0.0..=1.0).contains(&gf) {
            return Err(BodyModelError::InvalidGradientFactor);
        }

        let p_n2 = self.pressures[0];
        let p_he = self.pressures[1];
        let p_total = p_n2 + p_he;

        if p_total < Pressure::new_unchecked(0.001, PressureUnit::Bar) {
            return Ok(Pressure::zero());
        }

        let model_n2 = self.tissue_model.gases[0];
        let model_he = self.tissue_model.gases[1];

        let a = (p_n2 / p_total) * model_n2.get_a() + (p_he / p_total) * model_he.get_a();

        let b = (model_n2.get_b() * p_n2 + model_he.get_b() * p_he) / p_total;

        let numerator = p_total - a * gf;
        let denominator = gf / b + (1.0 - gf);

        Ok(numerator / denominator)
    }
}

pub struct BodyState {
    pub(crate) compartments: [CompartmentState; 16],
}

impl BodyState {
    pub fn new(nitrogen_pressure: Pressure, helium_pressure: Pressure) -> Self {
        Self {
            compartments: from_fn(|i| {
                CompartmentState::new(nitrogen_pressure, helium_pressure, TISSUE_CONSTANTS[i])
            }),
        }
    }

    pub fn compartments(&self) -> &[CompartmentState; 16] {
        &self.compartments
    }

    pub fn evolve(
        &mut self,
        ambient_pressure: Pressure,
        gas_mixture: &GasMixture,
        dt: Time,
    ) -> Result<(), BodyModelError> {
        if !(dt.to(TimeUnit::Seconds) > 0.0) {
            return Err(BodyModelError::NonPositiveTimeStep);
        }

        for (i, compartment) in self.compartments.iter_mut().enumerate() {
            let constants = &TISSUE_CONSTANTS[i];
            compartment.evolve(constants, ambient_pressure, gas_mixture, dt);
        }

        Ok(())
    }
}