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
8const 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 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 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 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 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 pub fn equivalent_narcotic_depth(&self, depth: Depth) -> Depth {
105 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 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 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 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}