Skip to main content

capra/modes/
open_circuit.rs

1use crate::parameters::DiveParameters;
2use crate::plan::DivePlan;
3use crate::result::DiveResult;
4use crate::{PPO2_MAXIMUM_DECO, PPO2_MINIMUM};
5use capra_core::common::dive_segment::SegmentType::{AscDesc, DecoStop};
6use capra_core::common::dive_segment::{DiveSegment, SegmentType};
7use capra_core::common::gas::Gas;
8use capra_core::common::tank::Tank;
9use capra_core::common::time_taken;
10use capra_core::deco::deco_algorithm::DecoAlgorithm;
11use std::cmp::Ordering;
12use std::collections::HashMap;
13use std::iter;
14use time::Duration;
15
16/// An open circuit dive plan.
17#[derive(Copy, Clone, Debug)]
18pub struct OpenCircuit<'a, T: DecoAlgorithm> {
19    /// Decompression algorithm to use
20    deco_algorithm: T,
21    /// Deco gases to use, and an optional corresponding Maximum Operating Depth
22    deco_gases: &'a [(Gas, Option<usize>)],
23    /// Bottom segments of the dive and corresponding gas used
24    bottom_segments: &'a [(DiveSegment, Gas)],
25    /// Parameters
26    parameters: DiveParameters,
27}
28
29impl<'a, T: DecoAlgorithm> OpenCircuit<'a, T> {
30    /// Returns a open circuit dive plan with the given parameters.
31    /// # Arguments
32    /// * `deco_algorithm` - Decompression algorithm to use
33    /// * `deco_gases` - Deco gases to use, and an optional corresponding Maximum Operating Depth
34    /// * `bottom_segments` - Bottom segments of the dive and corresponding gas used
35    /// * `ascent_rate` - Ascent rate to use throughout the dive
36    /// * `descent_rate` - Descent rate to use throughout the dive
37    /// * `water_density` - Density of water during the dive (measured in kg m^-3)
38    /// * `sac_bottom` - Surface Air Consumption on bottom segments (measured in bar min^-1)
39    /// * `sac_deco` - Surface Air Consumption during decompression (measured in bar min^-1)
40    pub fn new(
41        deco_algorithm: T,
42        deco_gases: &'a [(Gas, Option<usize>)],
43        bottom_segments: &'a [(DiveSegment, Gas)],
44        parameters: DiveParameters,
45    ) -> Self {
46        OpenCircuit {
47            deco_algorithm,
48            deco_gases,
49            bottom_segments,
50            parameters,
51        }
52    }
53
54    /// Returns gases that are suitable for use at a dive segment
55    fn filter_gases<'b>(
56        segment: &DiveSegment,
57        gases: &'b [(Gas, Option<usize>)],
58        metres_per_bar: f64,
59    ) -> Vec<&'b Gas> {
60        let mut candidates = gases
61            .iter()
62            .filter(|x| x.1.map_or(true, |t| t >= segment.start_depth()))
63            .map(|x| &x.0)
64            .filter(|a| a.in_ppo2_range(segment.start_depth(), PPO2_MINIMUM, PPO2_MAXIMUM_DECO))
65            .filter(|a| a.equivalent_narcotic_depth(segment.start_depth()) <= segment.start_depth())
66            .collect::<Vec<&Gas>>();
67
68        candidates.sort_by(|a, b| {
69            a.pp_o2(segment.start_depth(), metres_per_bar)
70                .partial_cmp(&b.pp_o2(segment.start_depth(), metres_per_bar))
71                .unwrap()
72        }); // sort by descending order of ppo2
73
74        candidates
75    }
76
77    /// Return a gas switch point (if exists) in a list of dive segments.
78    fn find_gas_switch_point<'c>(
79        segments: &'c [DiveSegment],
80        current_gas: &Gas,
81        gases: &'c [(Gas, Option<usize>)],
82        metres_per_bar: f64,
83    ) -> Option<(&'c DiveSegment, &'c Gas)> {
84        // Best gasplan is the gasplan that has the highest ppO2 (not over max allowed), and not over equivalent_narcotic_depth.
85        for stop in segments.iter().filter(|x| x.segment_type() != AscDesc) {
86            let candidate_gases = <OpenCircuit<'a, T>>::filter_gases(stop, gases, metres_per_bar);
87            if candidate_gases.is_empty() {
88                // there no fitting candidate gases.
89                continue;
90            }
91            if candidate_gases[candidate_gases.len() - 1] != current_gas {
92                return Some((stop, &candidate_gases[candidate_gases.len() - 1]));
93            }
94        }
95        None
96    }
97
98    /// Perform an open circuit dive plan from the `start` segment to the `end` segment.
99    /// The `end` segment is set to `None` if the end is the surface.
100    pub(crate) fn level_to_level(
101        &self,
102        mut deco: T,
103        start: &(DiveSegment, Gas),
104        end: Option<&(DiveSegment, Gas)>,
105        stops_performed: &mut Vec<(DiveSegment, Gas)>,
106    ) -> T {
107        // Check if there is any depth change
108        if let Some(t) = end {
109            match start.0.end_depth().cmp(&t.0.start_depth()) {
110                Ordering::Less => {
111                    // Create a segment with the next segment's gas
112                    let descent = DiveSegment::new(
113                        SegmentType::AscDesc,
114                        start.0.end_depth(),
115                        t.0.start_depth(),
116                        time_taken(
117                            self.parameters.descent_rate,
118                            start.0.end_depth(),
119                            t.0.start_depth(),
120                        ),
121                        self.parameters.ascent_rate,
122                        self.parameters.descent_rate,
123                    )
124                    .unwrap();
125                    deco.add_dive_segment(&descent, &start.1, self.parameters.metres_per_bar);
126                    stops_performed.push((descent, start.1));
127                    return deco;
128                }
129                Ordering::Equal => {
130                    // There cannot be any more segments to add.
131                    return deco;
132                }
133                Ordering::Greater => {} // Continue to main algorithm
134            }
135        }
136
137        let mut virtual_deco = deco;
138        // Find the stops between start and end using start gas
139        let end_depth = match end {
140            Some(t) => t.0.start_depth(),
141            None => 0,
142        };
143        let stops = virtual_deco
144            .surface(
145                self.parameters.ascent_rate,
146                self.parameters.descent_rate,
147                &start.1,
148                self.parameters.metres_per_bar,
149            )
150            .into_iter()
151            .take_while(|x| x.start_depth() > end_depth)
152            .collect::<Vec<DiveSegment>>();
153
154        let switch_gases: Vec<(Gas, Option<usize>)> = match end {
155            Some(t) => vec![(start.1, None), (t.1, None)],
156            None => self.deco_gases.to_vec(),
157        };
158
159        let switch_point = <OpenCircuit<'a, T>>::find_gas_switch_point(
160            &stops,
161            &start.1,
162            &switch_gases,
163            self.parameters.metres_per_bar,
164        );
165
166        // If there are deco stops in between
167        if let (Some(switch), true) = (
168            switch_point,
169            stops.iter().any(|x| x.segment_type() == DecoStop),
170        ) {
171            // Rewind the algorithm
172            virtual_deco = deco;
173
174            // Replay between stops until gas switch point
175            for stop in stops
176                .iter()
177                .take_while(|x| x.start_depth() > switch.0.start_depth())
178            {
179                virtual_deco.add_dive_segment(&stop, &start.1, self.parameters.metres_per_bar);
180                stops_performed.push((*stop, start.1));
181            }
182
183            // At gas switch point, use new gas and calculate new deco schedule
184            let new_stop = match virtual_deco
185                .get_stops(
186                    self.parameters.ascent_rate,
187                    self.parameters.descent_rate,
188                    &switch.1,
189                    self.parameters.metres_per_bar,
190                )
191                .into_iter()
192                .find(|x| x.segment_type() == DecoStop && x.start_depth() == switch.0.start_depth())
193            {
194                Some(t) => t,
195                None => DiveSegment::new(
196                    SegmentType::DecoStop,
197                    switch.0.start_depth(),
198                    switch.0.end_depth(),
199                    Duration::minute(),
200                    self.parameters.ascent_rate,
201                    self.parameters.descent_rate,
202                )
203                .unwrap(),
204            };
205
206            virtual_deco.add_dive_segment(&new_stop, switch.1, self.parameters.metres_per_bar);
207            stops_performed.push((new_stop, *switch.1));
208
209            // Call recursively with first new gas stop as start, end same
210            deco = virtual_deco;
211            self.level_to_level(deco, &(new_stop, *switch.1), end, stops_performed)
212        } else {
213            // Push segments and return
214            stops_performed.append(&mut stops.into_iter().zip(iter::repeat(start.1)).collect());
215            deco = virtual_deco;
216            deco
217        }
218    }
219}
220
221impl<'a, T: DecoAlgorithm> DivePlan<T> for OpenCircuit<'a, T> {
222    fn plan(&self) -> DiveResult<T> {
223        let mut total_segments: Vec<(DiveSegment, Gas)> = Vec::new();
224        let mut deco = self.deco_algorithm;
225
226        // Create the AscDesc to the first segment
227        let descent_to_beginning = DiveSegment::new(
228            AscDesc,
229            0,
230            self.bottom_segments[0].0.start_depth(),
231            time_taken(
232                self.parameters.descent_rate,
233                0,
234                self.bottom_segments[0].0.start_depth(),
235            ),
236            self.parameters.ascent_rate,
237            self.parameters.descent_rate,
238        )
239        .unwrap();
240
241        deco.add_dive_segment(
242            &descent_to_beginning,
243            &self.bottom_segments[0].1,
244            self.parameters.metres_per_bar,
245        );
246        total_segments.push((descent_to_beginning, self.bottom_segments[0].1));
247
248        for win in self.bottom_segments.windows(2) {
249            let mut stops_performed: Vec<(DiveSegment, Gas)> = Vec::new();
250            let start = win[0];
251            let end = win[1];
252
253            deco.add_dive_segment(&start.0, &start.1, self.parameters.metres_per_bar);
254            total_segments.push(start);
255
256            deco = self.level_to_level(deco, &start, Some(&end), &mut stops_performed);
257            total_segments.append(&mut stops_performed);
258        }
259
260        // However the sliding window does not capture the final element.
261        let final_stop = self.bottom_segments.last().unwrap();
262        deco.add_dive_segment(&final_stop.0, &final_stop.1, self.parameters.metres_per_bar);
263        total_segments.push(*final_stop);
264
265        let mut stops_performed: Vec<(DiveSegment, Gas)> = Vec::new();
266        deco = self.level_to_level(deco, &final_stop, None, &mut stops_performed);
267        total_segments.append(&mut stops_performed);
268
269        // Gas planning
270        let mut gas_plan: HashMap<Gas, usize> = HashMap::new();
271        for (segment, gas) in &total_segments {
272            let gas_consumed =
273                match segment.segment_type() {
274                    SegmentType::DecoStop => segment
275                        .gas_consumed(self.parameters.sac_deco, self.parameters.metres_per_bar),
276                    SegmentType::NoDeco => 0, // No deco segments aren't actually segments
277                    _ => segment
278                        .gas_consumed(self.parameters.sac_bottom, self.parameters.metres_per_bar),
279                };
280            let gas_needed = *(gas_plan.entry(*gas).or_insert(0)) + gas_consumed;
281            gas_plan.insert(*gas, gas_needed);
282        }
283
284        DiveResult::new(deco, total_segments, gas_plan)
285    }
286
287    fn plan_backwards(&self, _tanks: &[Tank]) -> DiveResult<T> {
288        unimplemented!()
289    }
290}