dive_deco/common/
gas.rs

1use crate::common::global_types::{MbarPressure, Pressure};
2use alloc::string::String;
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use super::{round, Depth};
7
8// alveolar water vapor pressure assuming 47 mm Hg at 37C (Buhlmann's value)
9const ALVEOLI_WATER_VAPOR_PRESSURE: f64 = 0.0627;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct Gas {
14    o2_pp: Pressure,
15    n2_pp: Pressure,
16    he_pp: Pressure,
17}
18
19#[derive(Debug, PartialEq, PartialOrd)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub struct PartialPressures {
22    pub o2: Pressure,
23    pub n2: Pressure,
24    pub he: Pressure,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub enum InertGas {
30    Helium,
31    Nitrogen,
32}
33
34impl core::fmt::Display for Gas {
35    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36        write!(f, "{:.0}/{:.0}", self.o2_pp * 100., self.he_pp * 100.)
37    }
38}
39
40impl Gas {
41    /// init new gas with partial pressures (eg. 0.21, 0. for air)
42    pub fn new(o2_pp: Pressure, he_pp: Pressure) -> Self {
43        if !(0. ..=1.).contains(&o2_pp) {
44            panic!("Invalid O2 partial pressure");
45        }
46        if !(0. ..=1.).contains(&he_pp) {
47            panic!("Invalid He partial pressure [{he_pp}]");
48        }
49        if (o2_pp + he_pp) > 1. {
50            panic!("Invalid partial pressures, can't exceed 1ATA in total");
51        }
52
53        Self {
54            o2_pp,
55            he_pp,
56            n2_pp: round((1. - (o2_pp + he_pp)) * 100.0) / 100.0,
57        }
58    }
59
60    pub fn id(&self) -> String {
61        let mut s = String::new();
62        let _ = core::fmt::write(
63            &mut s,
64            format_args!("{:.0}/{:.0}", self.o2_pp * 100., self.he_pp * 100.),
65        );
66        s
67    }
68
69    /// gas partial pressures
70    pub fn partial_pressures(
71        &self,
72        depth: Depth,
73        surface_pressure: MbarPressure,
74    ) -> PartialPressures {
75        let gas_pressure = (surface_pressure as f64 / 1000.) + (depth.as_meters() / 10.);
76        self.gas_pressures_compound(gas_pressure)
77    }
78
79    /// gas partial pressures in alveoli taking into account alveolar water vapor pressure
80    pub fn inspired_partial_pressures(
81        &self,
82        depth: Depth,
83        surface_pressure: MbarPressure,
84    ) -> PartialPressures {
85        let gas_pressure = ((surface_pressure as f64 / 1000.) + (depth.as_meters() / 10.))
86            - ALVEOLI_WATER_VAPOR_PRESSURE;
87        self.gas_pressures_compound(gas_pressure)
88    }
89
90    pub fn gas_pressures_compound(&self, gas_pressure: f64) -> PartialPressures {
91        PartialPressures {
92            o2: self.o2_pp * gas_pressure,
93            n2: self.n2_pp * gas_pressure,
94            he: self.he_pp * gas_pressure,
95        }
96    }
97
98    /// MOD
99    pub fn max_operating_depth(&self, pp_o2_limit: Pressure) -> Depth {
100        Depth::from_meters(10. * ((pp_o2_limit / self.o2_pp) - 1.))
101    }
102
103    /// END
104    pub fn equivalent_narcotic_depth(&self, depth: Depth) -> Depth {
105        // @todo refactor
106        let mut end = (depth + Depth::from_meters(10.)) * Depth::from_meters(1. - self.he_pp)
107            - Depth::from_meters(10.);
108        if end < Depth::zero() {
109            end = Depth::zero();
110        }
111        end
112    }
113
114    // TODO standard nitrox (bottom and deco) and trimix gasses
115    pub fn air() -> Self {
116        Self::new(0.21, 0.)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_valid_gas_air() {
126        let air = Gas::new(0.21, 0.);
127        assert_eq!(air.o2_pp, 0.21);
128        assert_eq!(air.n2_pp, 0.79);
129        assert_eq!(air.he_pp, 0.);
130    }
131
132    #[test]
133    fn test_valid_gas_tmx() {
134        let tmx = Gas::new(0.18, 0.35);
135        assert_eq!(tmx.o2_pp, 0.18);
136        assert_eq!(tmx.he_pp, 0.35);
137        assert_eq!(tmx.n2_pp, 0.47);
138    }
139
140    #[test]
141    #[should_panic]
142    fn test_invalid_o2_high() {
143        Gas::new(1.1, 0.);
144    }
145
146    #[test]
147    #[should_panic]
148    fn test_invalid_o2_low() {
149        Gas::new(-3., 0.);
150    }
151
152    #[test]
153    #[should_panic]
154    fn test_invalid_partial_pressures() {
155        Gas::new(0.5, 0.51);
156    }
157
158    #[test]
159    fn test_partial_pressures_air() {
160        let air = Gas::new(0.21, 0.);
161        let partial_pressures = air.partial_pressures(Depth::from_meters(10.), 1000);
162        assert_eq!(
163            partial_pressures,
164            PartialPressures {
165                o2: 0.42,
166                n2: 1.58,
167                he: 0.
168            }
169        );
170    }
171
172    #[test]
173    fn partial_pressures_tmx() {
174        let tmx = Gas::new(0.21, 0.35);
175        let partial_pressures = tmx.partial_pressures(Depth::from_meters(10.), 1000);
176        assert_eq!(
177            partial_pressures,
178            PartialPressures {
179                o2: 0.42,
180                he: 0.70,
181                n2: 0.88
182            }
183        )
184    }
185
186    #[test]
187    fn test_inspired_partial_pressures() {
188        let air = Gas::new(0.21, 0.);
189        let inspired_partial_pressures =
190            air.inspired_partial_pressures(Depth::from_meters(10.), 1000);
191        assert_eq!(
192            inspired_partial_pressures,
193            PartialPressures {
194                o2: 0.406833,
195                n2: 1.530467,
196                he: 0.0
197            }
198        );
199    }
200
201    #[test]
202    fn test_mod() {
203        // o2, he, max_ppo2, MOD
204        let test_cases = [
205            (0.21, 0., 1.4, 56.66666666666666),
206            (0.50, 0., 1.6, 22.),
207            (0.21, 0.35, 1.4, 56.66666666666666),
208            (0., 0., 1.4, f64::INFINITY),
209        ];
210        for (pp_o2, pe_he, max_pp_o2, expected_mod) in test_cases {
211            let gas = Gas::new(pp_o2, pe_he);
212            let calculated_mod = gas.max_operating_depth(max_pp_o2);
213            assert_eq!(calculated_mod, Depth::from_meters(expected_mod));
214        }
215    }
216
217    #[test]
218    fn test_end() {
219        // depth, o2, he, END
220        let test_cases = [
221            (60., 0.21, 0.40, 32.),
222            (0., 0.21, 0.40, 0.),
223            (40., 0.21, 0., 40.),
224        ];
225        for (depth, o2_pp, he_pp, expected_end) in test_cases {
226            let tmx = Gas::new(o2_pp, he_pp);
227            let calculated_end = tmx.equivalent_narcotic_depth(Depth::from_meters(depth));
228            assert_eq!(calculated_end, Depth::from_meters(expected_end));
229        }
230    }
231
232    #[test]
233    fn test_id() {
234        let ean32 = Gas::new(0.32, 0.);
235        assert_eq!(ean32.id(), "32/0");
236        let tmx2135 = Gas::new(0.21, 0.35);
237        assert_eq!(tmx2135.id(), "21/35");
238    }
239}