use crate::{Pressure, PressureUnit};
use core::fmt;
#[derive(Copy, Clone)]
pub enum Gas {
Nitrogen = 0,
Helium = 1,
}
impl Gas {
pub const ALL: [Gas; 2] = [Gas::Nitrogen, Gas::Helium];
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GasMixture {
oxygen_fraction: f64,
helium_fraction: f64,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GasMixtureError {
NonFiniteOxygenFraction,
NonFiniteHeliumFraction,
ZeroOxygenFraction,
NegativeOxygenFraction,
NegativeHeliumFraction,
FractionSumExceedsOne,
NonPositiveCriticalPartialPressureOfOxygen,
}
impl fmt::Display for GasMixtureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
GasMixtureError::NonFiniteOxygenFraction => {
f.write_str("oxygen fraction must be finite")
}
GasMixtureError::NonFiniteHeliumFraction => {
f.write_str("helium fraction must be finite")
}
GasMixtureError::NegativeOxygenFraction => {
f.write_str("oxygen fraction must be non-negative")
}
GasMixtureError::ZeroOxygenFraction => {
f.write_str("oxygen fraction must be greater than zero")
}
GasMixtureError::NegativeHeliumFraction => {
f.write_str("helium fraction must be non-negative")
}
GasMixtureError::FractionSumExceedsOne => {
f.write_str("oxygen and helium fractions must not sum above 1.0")
}
GasMixtureError::NonPositiveCriticalPartialPressureOfOxygen => {
f.write_str("critical partial pressure of oxygen must be positive")
}
}
}
}
impl GasMixture {
pub fn new(oxygen_fraction: f64, helium_fraction: f64) -> Result<Self, GasMixtureError> {
if !oxygen_fraction.is_finite() {
return Err(GasMixtureError::NonFiniteOxygenFraction);
}
if !helium_fraction.is_finite() {
return Err(GasMixtureError::NonFiniteHeliumFraction);
}
if oxygen_fraction < 0.0 {
return Err(GasMixtureError::NegativeOxygenFraction);
}
if oxygen_fraction == 0.0 {
return Err(GasMixtureError::ZeroOxygenFraction);
}
if helium_fraction < 0.0 {
return Err(GasMixtureError::NegativeHeliumFraction);
}
if oxygen_fraction + helium_fraction > 1.0 {
return Err(GasMixtureError::FractionSumExceedsOne);
}
Ok(Self {
oxygen_fraction,
helium_fraction,
})
}
pub const fn nitrogen_fraction(&self) -> f64 {
1. - self.oxygen_fraction - self.helium_fraction
}
pub const fn helium_fraction(&self) -> f64 {
self.helium_fraction
}
pub const fn oxygen_fraction(&self) -> f64 {
self.oxygen_fraction
}
pub const fn inert_fraction(&self, gas: Gas) -> f64 {
match gas {
Gas::Nitrogen => self.nitrogen_fraction(),
Gas::Helium => self.helium_fraction(),
}
}
pub fn max_operational_pressure(
&self,
critical_partial_pressure_of_oxygen: Pressure,
) -> Result<Pressure, GasMixtureError> {
if !(critical_partial_pressure_of_oxygen.to(PressureUnit::Pascal) > 0.0) {
return Err(GasMixtureError::NonPositiveCriticalPartialPressureOfOxygen);
}
Ok(critical_partial_pressure_of_oxygen / self.oxygen_fraction)
}
pub fn equivalent_narcotic_pressure(&self, ambient_pressure: Pressure) -> Pressure {
ambient_pressure * (self.oxygen_fraction() + self.nitrogen_fraction())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(actual: f64, expected: f64, epsilon: f64) {
let diff = (actual - expected).abs();
assert!(
diff <= epsilon,
"expected {actual} to be within {epsilon} of {expected}"
);
}
#[test]
fn gas_mixture_derives_nitrogen_fraction() {
let mixture = GasMixture::new(0.10, 0.20).expect("valid gas mixture");
assert_close(mixture.oxygen_fraction(), 0.10, 1E-12);
assert_close(mixture.helium_fraction(), 0.20, 1E-12);
assert_close(mixture.nitrogen_fraction(), 0.70, 1E-12);
assert_close(mixture.inert_fraction(Gas::Nitrogen), 0.70, 1E-12);
assert_close(mixture.inert_fraction(Gas::Helium), 0.20, 1E-12);
}
#[test]
fn max_operational_pressure_scales_oxygen_limit_by_oxygen_fraction() {
let nitrox_32 = GasMixture::new(0.32, 0.0).expect("valid gas mixture");
let mod_pressure = nitrox_32
.max_operational_pressure(Pressure::new_unchecked(1.4, crate::PressureUnit::Bar))
.expect("positive PPO2");
assert_close(mod_pressure.to(crate::PressureUnit::Bar), 4.375, 1E-12);
}
#[test]
fn equivalent_narcotic_pressure_preserves_air_pressure() {
let air = GasMixture::new(0.21, 0.0).expect("valid gas mixture");
let equivalent = air
.equivalent_narcotic_pressure(Pressure::new_unchecked(3.0, crate::PressureUnit::Bar));
assert_close(equivalent.to(crate::PressureUnit::Bar), 3.0, 1E-12);
}
#[test]
fn equivalent_narcotic_pressure_ignores_helium_fraction() {
let trimix_18_45 = GasMixture::new(0.18, 0.45).expect("valid gas mixture");
let equivalent = trimix_18_45
.equivalent_narcotic_pressure(Pressure::new_unchecked(6.0, crate::PressureUnit::Bar));
assert_close(equivalent.to(crate::PressureUnit::Bar), 3.3, 1E-12);
}
#[test]
fn gas_mixture_reports_invalid_inputs_as_errors() {
assert_eq!(
GasMixture::new(f64::NAN, 0.0),
Err(GasMixtureError::NonFiniteOxygenFraction)
);
assert_eq!(
GasMixture::new(0.21, f64::INFINITY),
Err(GasMixtureError::NonFiniteHeliumFraction)
);
assert_eq!(
GasMixture::new(-0.01, 0.0),
Err(GasMixtureError::NegativeOxygenFraction)
);
assert_eq!(
GasMixture::new(0.0, 1.0),
Err(GasMixtureError::ZeroOxygenFraction)
);
assert_eq!(
GasMixture::new(0.21, -0.01),
Err(GasMixtureError::NegativeHeliumFraction)
);
assert_eq!(
GasMixture::new(0.80, 0.30),
Err(GasMixtureError::FractionSumExceedsOne)
);
}
#[test]
fn max_operational_pressure_reports_non_positive_ppo2() {
let nitrox_32 = GasMixture::new(0.32, 0.0).expect("valid gas mixture");
assert_eq!(
nitrox_32.max_operational_pressure(Pressure::zero()),
Err(GasMixtureError::NonPositiveCriticalPartialPressureOfOxygen)
);
assert_eq!(
nitrox_32
.max_operational_pressure(Pressure::new_unchecked(-1.0, crate::PressureUnit::Bar)),
Err(GasMixtureError::NonPositiveCriticalPartialPressureOfOxygen)
);
assert_eq!(
nitrox_32.max_operational_pressure(Pressure::new_unchecked(
f64::NAN,
crate::PressureUnit::Bar
)),
Err(GasMixtureError::NonPositiveCriticalPartialPressureOfOxygen)
);
}
}