1use super::zhl_values::{ZHLParam, ZHLParams};
2use crate::{
3 common::{
4 powf, Depth, GradientFactor, InertGas, MbarPressure, PartialPressures, Pressure, RecordData,
5 },
6 BuhlmannConfig, Gas, Time,
7};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11#[derive(Copy, Clone, Debug, PartialEq)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct Compartment {
14 pub no: u8,
16 pub min_tolerable_amb_pressure: Pressure,
18 pub he_ip: Pressure,
20 pub n2_ip: Pressure,
22 pub total_ip: Pressure,
24 pub m_value_raw: Pressure,
26 pub m_value_calc: Pressure,
28 pub params: ZHLParams,
30 model_config: BuhlmannConfig,
32}
33
34#[derive(Debug, PartialEq, Clone)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub struct Supersaturation {
37 pub gf_99: f64,
38 pub gf_surf: f64,
39}
40
41impl Compartment {
42 pub fn new(no: u8, params: ZHLParams, model_config: BuhlmannConfig) -> Self {
43 let init_gas = Gas::air();
44 let init_gas_compound_pressures =
45 init_gas.inspired_partial_pressures(Depth::zero(), model_config.surface_pressure);
46 let n2_ip = init_gas_compound_pressures.n2;
47 let he_ip = init_gas_compound_pressures.he;
48
49 let mut compartment = Self {
50 no,
51 params,
52 n2_ip,
53 he_ip,
54 total_ip: he_ip + n2_ip,
55 m_value_raw: 0., m_value_calc: 0., min_tolerable_amb_pressure: 0.,
58 model_config,
59 };
60
61 let (_, gf_high) = compartment.model_config.gf;
63 compartment.m_value_raw = compartment.m_value(
64 Depth::zero(),
65 compartment.model_config.surface_pressure,
66 100,
67 );
68 compartment.m_value_calc = compartment.m_value_raw;
69 compartment.min_tolerable_amb_pressure = compartment.min_tolerable_amb_pressure(gf_high);
70
71 compartment
72 }
73
74 pub fn recalculate(
76 &mut self,
77 record: &RecordData,
78 max_gf: GradientFactor,
79 surface_pressure: MbarPressure,
80 ) {
81 let (he_inert_pressure, n2_inert_pressure) =
82 self.compartment_inert_pressure(record, surface_pressure);
83
84 self.he_ip = he_inert_pressure;
85 self.n2_ip = n2_inert_pressure;
86 self.total_ip = he_inert_pressure + n2_inert_pressure;
87
88 self.m_value_raw = self.m_value(record.depth, surface_pressure, 100);
90 self.m_value_calc = self.m_value(record.depth, surface_pressure, max_gf);
91
92 self.min_tolerable_amb_pressure = self.min_tolerable_amb_pressure(max_gf);
93 }
94
95 pub fn ceiling(&self) -> Depth {
97 let mut ceil = (self.min_tolerable_amb_pressure
98 - (self.model_config.surface_pressure as f64 / 1000.))
99 * 10.;
100 if ceil < 0. {
102 ceil = 0.;
103 }
104
105 Depth::from_meters(ceil)
106 }
107
108 pub fn supersaturation(&self, surface_pressure: MbarPressure, depth: Depth) -> Supersaturation {
110 let p_surf = (surface_pressure as f64) / 1000.;
111 let p_amb = p_surf + (depth.as_meters() / 10.);
112 let m_value = self.m_value_raw;
113 let m_value_surf = self.m_value(Depth::zero(), surface_pressure, 100);
114 let gf_99 = ((self.total_ip - p_amb) / (m_value - p_amb)) * 100.;
115 let gf_surf = ((self.total_ip - p_surf) / (m_value_surf - p_surf)) * 100.;
116
117 Supersaturation { gf_99, gf_surf }
118 }
119
120 fn m_value(
121 &self,
122 depth: Depth,
123 surface_pressure: MbarPressure,
124 max_gf: GradientFactor,
125 ) -> Pressure {
126 let weighted_zhl_params = self.weighted_zhl_params(self.he_ip, self.n2_ip);
127 let (_, a_coeff_adjusted, b_coeff_adjusted) =
128 self.max_gf_adjusted_zhl_params(weighted_zhl_params, max_gf);
129 let p_surf = (surface_pressure as f64) / 1000.;
130 let p_amb = p_surf + (depth.as_meters() / 10.);
131
132 a_coeff_adjusted + (p_amb / b_coeff_adjusted)
133 }
134
135 fn compartment_inert_pressure(
137 &self,
138 record: &RecordData,
139 surface_pressure: MbarPressure,
140 ) -> (Pressure, Pressure) {
141 let RecordData { depth, time, gas } = record;
143 let PartialPressures {
144 n2: n2_pp,
145 he: he_pp,
146 ..
147 } = gas.inspired_partial_pressures(*depth, surface_pressure);
148
149 let he_inspired_pp = he_pp;
151 let n2_inspired = n2_pp;
152
153 let (n2_half_time, _, _, he_half_time, ..) = self.params;
155 let he_p_comp_delta = self.compartment_pressure_delta_haldane(
156 InertGas::Helium,
157 he_inspired_pp,
158 *time,
159 he_half_time,
160 );
161 let n2_p_comp_delta = self.compartment_pressure_delta_haldane(
162 InertGas::Nitrogen,
163 n2_inspired,
164 *time,
165 n2_half_time,
166 );
167
168 let he_final = self.he_ip + he_p_comp_delta;
170 let n2_final = self.n2_ip + n2_p_comp_delta;
171
172 (he_final, n2_final)
173 }
174
175 fn compartment_pressure_delta_haldane(
177 &self,
178 inert_gas: InertGas,
179 gas_inspired_p: Pressure,
180 time: Time,
181 half_time: ZHLParam,
182 ) -> Pressure {
183 let inert_gas_load = match inert_gas {
184 InertGas::Helium => self.he_ip,
185 InertGas::Nitrogen => self.n2_ip,
186 };
187
188 let factor = 1. - powf(2.0, -(time.as_minutes()) / half_time);
190
191 (gas_inspired_p - inert_gas_load) * factor
192 }
193
194 fn min_tolerable_amb_pressure(&self, max_gf: GradientFactor) -> Pressure {
196 let weighted_zhl_params = self.weighted_zhl_params(self.he_ip, self.n2_ip);
197 let (_, a_coefficient_adjusted, b_coefficient_adjusted) =
198 self.max_gf_adjusted_zhl_params(weighted_zhl_params, max_gf);
199
200 (self.total_ip - a_coefficient_adjusted) * b_coefficient_adjusted
201 }
202
203 pub fn weighted_zhl_params(
205 &self,
206 he_pp: Pressure,
207 n2_pp: Pressure,
208 ) -> (ZHLParam, ZHLParam, ZHLParam) {
209 fn weighted_param(
210 he_param: ZHLParam,
211 he_pp: Pressure,
212 n2_param: ZHLParam,
213 n2_pp: Pressure,
214 ) -> ZHLParam {
215 ((he_param * he_pp) + (n2_param * n2_pp)) / (he_pp + n2_pp)
216 }
217 let (n2_half_time, n2_a_coeff, n2_b_coeff, he_half_time, he_a_coeff, he_b_coeff) =
218 self.params;
219 (
220 weighted_param(he_half_time, he_pp, n2_half_time, n2_pp),
221 weighted_param(he_a_coeff, he_pp, n2_a_coeff, n2_pp),
222 weighted_param(he_b_coeff, he_pp, n2_b_coeff, n2_pp),
223 )
224 }
225
226 fn max_gf_adjusted_zhl_params(
228 &self,
229 params: (ZHLParam, ZHLParam, ZHLParam),
230 max_gf: GradientFactor,
231 ) -> (ZHLParam, ZHLParam, ZHLParam) {
232 let (half_time, a_coeff, b_coeff) = params;
233 let max_gf_fraction = max_gf as f64 / 100.;
234 let a_coefficient_adjusted = a_coeff * max_gf_fraction;
235 let b_coefficient_adjusted =
236 b_coeff / (max_gf_fraction - (max_gf_fraction * b_coeff) + b_coeff);
237
238 (half_time, a_coefficient_adjusted, b_coefficient_adjusted)
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::{common::Gas, Time};
246
247 fn comp_1() -> Compartment {
248 let comp_1_params = (4., 1.2599, 0.5050, 1.51, 01.7424, 0.4245);
249 Compartment::new(1, comp_1_params, BuhlmannConfig::default())
250 }
251
252 fn comp_5() -> Compartment {
253 let comp_5_params = (27., 0.6200, 0.8126, 10.21, 0.9220, 0.7582);
254 Compartment::new(5, comp_5_params, BuhlmannConfig::default())
255 }
256
257 #[test]
258 fn test_constructor() {
259 let comp = comp_1();
260 assert_eq!(
261 comp,
262 Compartment {
263 no: 1,
264 min_tolerable_amb_pressure: -0.257127315,
265 he_ip: 0.0,
266 n2_ip: 0.750737,
267 total_ip: 0.750737,
268 m_value_raw: 3.265840594059406,
269 m_value_calc: 3.265840594059406,
270 params: (4.0, 1.2599, 0.505, 1.51, 1.7424, 0.4245),
271 model_config: BuhlmannConfig::default(),
273 }
274 );
275 }
276
277 #[test]
278 fn test_m_value_raw() {
279 let mut comp_1 = comp_1();
280 let mut comp_5 = comp_5();
281 let air = Gas::new(0.21, 0.);
282 let record = RecordData {
283 depth: Depth::zero(),
284 time: Time::from_seconds(1.),
285 gas: &air,
286 };
287 comp_1.recalculate(&record, 100, 1000);
288 comp_5.recalculate(&record, 100, 1000);
289 assert_eq!(comp_1.m_value_raw, 3.24009801980198);
290 assert_eq!(comp_5.m_value_raw, 1.8506177701206004);
291 }
292
293 #[test]
294 fn test_m_value_calc() {
295 let mut comp_1 = comp_1();
296 let mut comp_5 = comp_5();
297 let air = Gas::new(0.21, 0.);
298 let record = RecordData {
299 depth: Depth::zero(),
300 time: Time::from_seconds(1.),
301 gas: &air,
302 };
303 comp_1.recalculate(&record, 70, 1000);
304 comp_5.recalculate(&record, 70, 1000);
305 assert_eq!(comp_1.m_value_calc, 2.568068613861386);
306 assert_eq!(comp_5.m_value_calc, 1.5954324390844203);
307 }
308
309 #[test]
310 fn test_recalculation_ongassing() {
311 let mut comp = comp_5();
312 let air = Gas::new(0.21, 0.);
313 let record = RecordData {
314 depth: Depth::from_meters(30.),
315 time: Time::from_minutes(10.),
316 gas: &air,
317 };
318 comp.recalculate(&record, 100, 1000);
319 assert_eq!(comp.total_ip, 1.2850179204911072);
320 }
321
322 #[test]
323 fn test_weighted_params_trimix() {
324 let comp = comp_1();
325 let weighted_params = comp.weighted_zhl_params(0.5, 1. - (0.18 + 0.5));
326 assert_eq!(
327 weighted_params,
328 (2.481707317073171, 1.5541073170731705, 0.4559146341463414)
329 );
330 }
331
332 #[test]
333 fn test_min_pressure_calculation() {
334 let mut comp = comp_5();
335 let air = Gas::new(0.21, 0.);
336 let recprd = RecordData {
337 depth: Depth::from_meters(30.),
338 time: Time::from_minutes(10.),
339 gas: &air,
340 };
341 comp.recalculate(&recprd, 100, 100);
342 let min_tolerable_pressure = comp.min_tolerable_amb_pressure;
343 assert_eq!(min_tolerable_pressure, 0.40957969932131577);
344 }
345}