1use alloc::vec;
2use alloc::vec::Vec;
3use core::{cmp::Ordering, fmt};
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7use crate::{DecoModel, Depth, DepthType, Gas, Time};
8
9use super::{ceil, DecoModelConfig, DiveState, MbarPressure, Sim};
10
11const DEFAULT_CEILING_WINDOW: DepthType = 3.;
13const DEFAULT_MAX_END_DEPTH: DepthType = 30.;
14
15#[derive(Copy, Clone, Debug, PartialEq)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17enum DecoAction {
18 AscentToCeil,
19 AscentToGasSwitchDepth,
20 SwitchGas,
21 Stop,
22}
23
24#[derive(Copy, Clone, Debug, PartialEq)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub enum DecoStageType {
27 Ascent,
28 DecoStop,
29 GasSwitch,
30}
31
32#[derive(Copy, Clone, Debug, PartialEq)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34pub struct DecoStage {
35 pub stage_type: DecoStageType,
36 pub start_depth: Depth,
37 pub end_depth: Depth,
38 pub duration: Time,
39 pub gas: Gas,
40}
41
42#[derive(Clone, Debug, Default)]
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
44pub struct Deco {
45 deco_stages: Vec<DecoStage>,
46 tts: Time,
47 sim: bool,
48}
49
50#[derive(Debug, PartialEq, Default, Clone)]
51#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
52pub struct DecoRuntime {
53 pub deco_stages: Vec<DecoStage>,
55 pub tts: Time,
57 pub tts_at_5: Time,
59 pub tts_delta_at_5: Time,
61}
62
63#[derive(Debug)]
64struct MissedDecoStopViolation;
65
66#[derive(Debug, PartialEq, Clone)]
67#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
68pub enum DecoCalculationError {
69 EmptyGasList,
70 CurrentGasNotInList,
71}
72
73impl fmt::Display for DecoCalculationError {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 match *self {
76 DecoCalculationError::EmptyGasList => {
77 write!(f, "At least one available gas mix required")
78 }
79 DecoCalculationError::CurrentGasNotInList => write!(
80 f,
81 "Available gas mixes must include current gas mix used by deco model"
82 ),
83 }
84 }
85}
86
87impl Sim for Deco {
88 fn fork(&self) -> Self {
89 Self {
90 sim: true,
91 ..self.clone()
92 }
93 }
94 fn is_sim(&self) -> bool {
95 self.sim
96 }
97}
98
99impl Deco {
100 pub fn new_sim() -> Self {
101 let deco = Self::default();
102 deco.fork()
103 }
104
105 pub fn calc<T: DecoModel + Clone + Sim>(
106 &mut self,
107 deco_model: T,
108 mut gas_mixes: Vec<Gas>,
109 ) -> Result<DecoRuntime, DecoCalculationError> {
110 Self::validate_gas_mixes(&deco_model, &gas_mixes)?;
112 gas_mixes.sort_by(|a, b| {
114 let x = a.gas_pressures_compound(1.);
115 let y = b.gas_pressures_compound(1.);
116 x.o2.partial_cmp(&y.o2).unwrap()
117 });
118
119 let mut sim_model: T = deco_model.clone();
121 let ascent_rate = sim_model.config().deco_ascent_rate();
122 loop {
123 let DiveState {
124 depth: pre_stage_depth,
125 time: pre_stage_time,
126 gas: pre_stage_gas,
127 ..
128 } = sim_model.dive_state();
129 let ceiling = sim_model.ceiling();
130
131 let next_deco_action = self.next_deco_action(&sim_model, gas_mixes.clone());
134 if let Err(e) = next_deco_action {
135 return match e {
136 MissedDecoStopViolation => {
137 sim_model.record(
138 self.deco_stop_depth(ceiling),
139 Time::zero(),
140 &pre_stage_gas,
141 );
142 self.calc(sim_model, gas_mixes)
143 }
144 };
145 }
146
147 let mut deco_stages: Vec<DecoStage> = vec![];
149 let (deco_action, next_switch_gas) = next_deco_action.unwrap();
150 match deco_action {
151 None => {
153 break;
154 }
155
156 Some(deco_action) => {
158 match deco_action {
159 DecoAction::AscentToCeil => {
161 sim_model.record_travel_with_rate(
162 self.deco_stop_depth(ceiling),
163 ascent_rate,
164 &pre_stage_gas,
165 );
166 let current_sim_state = sim_model.dive_state();
167 let current_sim_time = current_sim_state.time;
168 deco_stages.push(DecoStage {
169 stage_type: DecoStageType::Ascent,
170 start_depth: pre_stage_depth,
171 end_depth: current_sim_state.depth,
172 duration: current_sim_time - pre_stage_time,
173 gas: current_sim_state.gas,
174 })
175 }
176
177 DecoAction::AscentToGasSwitchDepth => {
179 if let Some(next_switch_gas) = next_switch_gas {
181 let switch_gas_mod = next_switch_gas.max_operating_depth(1.6);
183 sim_model.record_travel_with_rate(
184 switch_gas_mod,
185 ascent_rate,
186 &pre_stage_gas,
187 );
188 let DiveState {
189 depth: post_ascent_depth,
190 time: post_ascent_time,
191 ..
192 } = sim_model.dive_state();
193 deco_stages.push(DecoStage {
194 stage_type: DecoStageType::Ascent,
195 start_depth: pre_stage_depth,
196 end_depth: post_ascent_depth,
197 duration: post_ascent_time - pre_stage_time,
198 gas: pre_stage_gas,
199 });
200
201 sim_model.record(
203 sim_model.dive_state().depth,
204 Time::zero(),
205 &next_switch_gas,
206 );
207 let post_switch_state = sim_model.dive_state();
209 deco_stages.push(DecoStage {
210 stage_type: DecoStageType::GasSwitch,
211 start_depth: post_ascent_depth,
212 end_depth: post_switch_state.depth,
213 duration: Time::zero(),
214 gas: next_switch_gas,
215 });
216 }
217 }
218
219 DecoAction::SwitchGas => {
221 let switch_gas = next_switch_gas.unwrap();
222 sim_model.record(pre_stage_depth, Time::zero(), &switch_gas);
224 deco_stages.push(DecoStage {
225 stage_type: DecoStageType::GasSwitch,
226 start_depth: pre_stage_depth,
227 end_depth: pre_stage_depth,
228 duration: Time::zero(),
229 gas: switch_gas,
230 })
231 }
232
233 DecoAction::Stop => {
235 let stop_depth = self.deco_stop_depth(ceiling);
236
237 sim_model.record(
238 pre_stage_depth,
239 Time::from_seconds(1.),
240 &pre_stage_gas,
241 );
242 let sim_state = sim_model.dive_state();
243 deco_stages.push(DecoStage {
245 stage_type: DecoStageType::DecoStop,
246 start_depth: stop_depth,
247 end_depth: stop_depth,
248 duration: sim_state.time - pre_stage_time,
249 gas: sim_state.gas,
250 })
251 }
252 }
253 }
254 }
255 deco_stages
257 .into_iter()
258 .for_each(|deco_stage| self.register_deco_stage(deco_stage));
259 }
260
261 let tts = self.tts;
262 let mut tts_at_5 = Time::zero();
263 let mut tts_delta_at_5 = Time::zero();
264 if !self.is_sim() {
265 let mut nested_sim_deco = Deco::new_sim();
266 let mut nested_sim_model = deco_model.clone();
267 let DiveState {
268 depth: sim_depth,
269 gas: sim_gas,
270 ..
271 } = nested_sim_model.dive_state();
272 nested_sim_model.record(sim_depth, Time::from_minutes(5.), &sim_gas);
273 let nested_deco = nested_sim_deco.calc(nested_sim_model, gas_mixes.clone())?;
274 tts_at_5 = nested_deco.tts;
275 tts_delta_at_5 = tts_at_5 - tts;
276 }
277
278 Ok(DecoRuntime {
279 deco_stages: self.deco_stages.clone(),
280 tts,
281 tts_at_5,
282 tts_delta_at_5,
283 })
284 }
285
286 fn next_deco_action(
287 &self,
288 sim_model: &impl DecoModel,
289 gas_mixes: Vec<Gas>,
290 ) -> Result<(Option<DecoAction>, Option<Gas>), MissedDecoStopViolation> {
291 let DiveState {
292 depth: current_depth,
293 gas: current_gas,
294 ..
295 } = sim_model.dive_state();
296 let surface_pressure = sim_model.config().surface_pressure();
297
298 if current_depth <= Depth::zero() {
300 return Ok((None, None));
301 }
302
303 let ceiling = sim_model.ceiling();
304
305 match ceiling.partial_cmp(&Depth::zero()) {
306 Some(Ordering::Equal | Ordering::Less) => Ok((Some(DecoAction::AscentToCeil), None)),
307 Some(Ordering::Greater) => {
308 if current_depth < self.deco_stop_depth(ceiling) {
310 return Err(MissedDecoStopViolation);
311 }
312
313 let next_switch_gas =
314 self.next_switch_gas(current_depth, ¤t_gas, gas_mixes, surface_pressure);
315 if let Some(switch_gas) = next_switch_gas {
317 let gas_mod = switch_gas.max_operating_depth(1.6);
319 let gas_end = switch_gas.equivalent_narcotic_depth(current_depth);
320 if (switch_gas != current_gas)
321 && (current_depth <= gas_mod)
322 && (gas_end <= Depth::from_meters(DEFAULT_MAX_END_DEPTH))
323 {
324 return Ok((Some(DecoAction::SwitchGas), Some(switch_gas)));
325 }
326 }
327
328 let stop_depth = self.deco_stop_depth(ceiling);
330 if current_depth == stop_depth {
331 Ok((Some(DecoAction::Stop), None))
332 } else {
333 if let Some(next_switch_gas) = next_switch_gas {
335 if next_switch_gas.max_operating_depth(1.6) >= ceiling {
336 return Ok((
337 Some(DecoAction::AscentToGasSwitchDepth),
338 Some(next_switch_gas),
339 ));
340 }
341 }
342 Ok((Some(DecoAction::AscentToCeil), None))
343 }
344 }
345 None => panic!("Ceiling and depth uncomparable"),
346 }
347 }
348
349 fn next_switch_gas(
351 &self,
352 current_depth: Depth,
353 current_gas: &Gas,
354 gas_mixes: Vec<Gas>,
355 surface_pressure: MbarPressure,
356 ) -> Option<Gas> {
357 let current_gas_partial_pressures =
358 current_gas.partial_pressures(current_depth, surface_pressure);
359 let switch_gasses = gas_mixes
361 .into_iter()
362 .filter(|gas| {
363 let partial_pressures = gas.partial_pressures(current_depth, surface_pressure);
364 partial_pressures.o2 > current_gas_partial_pressures.o2
365 })
366 .collect::<Vec<Gas>>();
367
368 switch_gasses.first().copied()
370 }
371
372 fn register_deco_stage(&mut self, stage: DecoStage) {
373 let mut push_new = true;
375 let last_stage = self.deco_stages.last_mut();
376 if let Some(last_stage) = last_stage {
377 if last_stage.stage_type == stage.stage_type {
378 last_stage.duration += stage.duration;
379 last_stage.end_depth = stage.end_depth;
380 push_new = false;
381 }
382 }
383 if push_new {
384 self.deco_stages.push(stage);
385 }
386
387 self.tts += stage.duration;
389 }
390
391 fn deco_stop_depth(&self, ceiling: Depth) -> Depth {
393 let depth = DEFAULT_CEILING_WINDOW * ceil(ceiling.as_meters() / DEFAULT_CEILING_WINDOW);
394 Depth::from_meters(depth)
395 }
396
397 fn validate_gas_mixes<T: DecoModel>(
398 deco_model: &T,
399 gas_mixes: &[Gas],
400 ) -> Result<(), DecoCalculationError> {
401 if gas_mixes.is_empty() {
402 return Err(DecoCalculationError::EmptyGasList);
403 }
404 let current_gas = deco_model.dive_state().gas;
405 let current_gas_in_available = gas_mixes.iter().find(|gas_mix| **gas_mix == current_gas);
406 if current_gas_in_available.is_none() {
407 return Err(DecoCalculationError::CurrentGasNotInList);
408 }
409 Ok(())
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::{BuhlmannConfig, BuhlmannModel};
417
418 #[test]
419 fn test_ceiling_rounding() {
420 let test_cases: Vec<(DepthType, DepthType)> = vec![
421 (0., 0.),
422 (2., 3.),
423 (2.999, 3.),
424 (3., 3.),
425 (3.00001, 6.),
426 (12., 12.),
427 ];
428 let deco = Deco::default();
429 for case in test_cases.into_iter() {
430 let (input_depth, expected_depth) = case;
431 let res = deco.deco_stop_depth(Depth::from_meters(input_depth));
432 assert_eq!(res, Depth::from_meters(expected_depth));
433 }
434 }
435
436 #[test]
437 fn test_next_switch_gas() {
438 let air = Gas::air();
439 let ean_50 = Gas::new(0.5, 0.);
440 let oxygen = Gas::new(1., 0.);
441 let trimix = Gas::new(0.5, 0.2);
442
443 let test_cases: Vec<(DepthType, Gas, Vec<Gas>, Option<Gas>)> = vec![
447 (10., air, vec![air], None),
449 (10., air, vec![air, ean_50], Some(ean_50)),
451 (30., air, vec![air, ean_50], Some(ean_50)),
453 (20., air, vec![air, ean_50, oxygen], Some(ean_50)),
455 (5.5, ean_50, vec![air, ean_50, oxygen], Some(oxygen)),
457 (30., air, vec![air, trimix], Some(trimix)),
459 ];
460
461 let deco = Deco::default();
462 for case in test_cases.into_iter() {
463 let (current_depth, current_gas, available_gas_mixes, expected_switch_gas) = case;
464 let res = deco.next_switch_gas(
465 Depth::from_meters(current_depth),
466 ¤t_gas,
467 available_gas_mixes,
468 1000,
469 );
470 assert_eq!(res, expected_switch_gas);
471 }
472 }
473
474 #[test]
475 fn should_err_on_empty_gas_mixes() {
476 let mut deco = Deco::default();
477 let deco_model = BuhlmannModel::default();
478 let deco_res = deco.calc(deco_model, vec![]);
479 assert_eq!(deco_res, Err(DecoCalculationError::EmptyGasList));
480 }
481
482 #[test]
483 fn should_err_on_gas_mixes_without_current_mix() {
484 let mut deco = Deco::default();
485 let mut deco_model = BuhlmannModel::default();
486 let air = Gas::air();
487 let ean50 = Gas::new(0.50, 0.);
488 let tmx2135 = Gas::new(0.21, 0.35);
489 deco_model.record_travel_with_rate(Depth::from_meters(40.), 10., &air);
490 let deco_res = deco.calc(deco_model, vec![ean50, tmx2135]);
491 assert_eq!(deco_res, Err(DecoCalculationError::CurrentGasNotInList));
492 }
493
494 #[test]
495 fn no_duplicated_stops_within_deco_padding() {
496 let mut deco = Deco::default();
497 let mut deco_model =
498 BuhlmannModel::new(BuhlmannConfig::default().with_gradient_factors(30, 70));
499 let air = Gas::air();
500 let ean50 = Gas::new(0.50, 0.);
501 deco_model.record_travel_with_rate(Depth::from_meters(40.), 10., &air);
502 deco_model.record(Depth::from_meters(40.), Time::from_minutes(27.), &air);
503 deco_model.record_travel_with_rate(Depth::from_meters(16.), 10., &air);
504 deco_model.record(
505 Depth::from_meters(16.),
506 Time::from_minutes(1.),
507 &Gas::new(0.50, 0.),
508 );
509
510 let mut deco_stop_depths: Vec<Depth> = vec![];
511 let deco_res = deco.calc(deco_model, vec![air, ean50]).unwrap();
512 for deco_stage in deco_res.deco_stages {
513 if deco_stage.stage_type == DecoStageType::DecoStop {
514 let deco_stop_depth = deco_stage.start_depth;
515 assert_eq!(
516 deco_stop_depths.contains(&deco_stop_depth),
517 false,
518 "{}m deco stop should not repeat",
519 deco_stop_depth
520 );
521 deco_stop_depths.push(deco_stop_depth);
522 }
523 }
524 }
525}