Skip to content

Commit

Permalink
Merge pull request #28 from KG32/feat/ndl-type
Browse files Browse the repository at this point in the history
feat: ndl type
  • Loading branch information
KG32 authored Sep 7, 2024
2 parents b6bb36f + e8dbf74 commit ebb991b
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 64 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dive-deco"
version = "3.1.0"
version = "3.2.0"
edition = "2021"
license = "MIT"
description = "A dive decompression models library (Buehlmann ZH-L 16C)"
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ The Bühlmann decompression set of parameters is an Haldanian mathematical model
- gradient factors
- surface pressure
- deco ascent rate
- NDL definition
- actual - taking into account off-gassing on ascent
- by ceiling - NDL time is determined by ceiling, it counts down to a condition where ceiling isn't equal to the surface

### Planned features

Expand Down Expand Up @@ -62,14 +65,14 @@ Current config options:

- `gradient_factors` - gradient factors settings (`[GFlow], [GFhigh])`default: `(100, 100)`)
- `surface_pressure` - atmospheric pressure at the surface at the time of model initialization and assumed constant throughout model's life
- `deco_ascent_rate` - ascent rate in m/s that is assumed to be followed when calculating deco obligations and simulations
- `deco_ascent_rate` - ascent rate in m/s that is assumed to be followed when calculating deco obligations and simulations. Default value: 10 m/min (33 ft/min)

```rust
// fluid-interface-like built config
let config = BuehlmannConfig::new()
.gradient_factors(30, 70)
.surface_pressure(1013)
.deco_ascent_rate(9.);
.deco_ascent_rate(10.);
let model = BuehlmannModel::new(config);
println!("{:?}", model.config()); // BuehlmannConfig { gf: (30, 70) }
```
Expand Down Expand Up @@ -277,7 +280,8 @@ All decompression stages calculated to clear deco obligations and resurface in a

The NDL is a theoretical time obtained by calculating inert gas uptake and release in the body that determines a time interval a diver may theoretically spend at given depth without aquiring any decompression obligations (given constant depth and gas mix).

- `ndl()` - no-decompression limit for current model state in minutes, assuming constant depth and gas mix. This method has a cut-off at 99 minutes
- `ndl()` - no-decompression limit for current model state in minutes, assuming constant depth and gas mix. This method has a cut-off at 99 minutes.
NDL controllable by `ndl_type` model config. By default (`Actual`), it takes into account off-gassing during ascent and it's defined as a maximum time at given depth that won't create any decompression obligations (i.e. even on existing ceiling, limit occures when a direct ascent with configured ascent rate doesn't cause any tissue to intersect with its M-Value at a given time).

```rust
use dive_deco::{DecoModel, BuehlmannModel, BuehlmannConfig, Gas};
Expand All @@ -298,7 +302,8 @@ fn main() {

// current NDL (no-decompression limit)
let current_ndl = model.ndl();
println!("NDL: {} min", current_ndl); // output: NDL: 5 min
println!("NDL: {} min", current_ndl); // output: NDL: 9 min
// if we used a `ByCeiling` ndl_type config that consideres only ceiling depth, the output would be 5 min
}
```

Expand Down
9 changes: 5 additions & 4 deletions examples/config_example.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
use dive_deco::{ BuehlmannConfig, BuehlmannModel, DecoModel };
use dive_deco::{ BuehlmannConfig, BuehlmannModel, DecoModel, NDLType };

fn main() {
// model with default config (GF 100/100)
let default_config = BuehlmannConfig::default();
let model_1 = BuehlmannModel::new(default_config);
println!("{:?}", model_1.config()); // BuehlmannConfig { gf: (100, 100) }

// model with config instance
// model with full config instance
let config_instance = BuehlmannConfig {
gf: (85, 85),
surface_pressure: 1013,
deco_ascent_rate: 9.
deco_ascent_rate: 9.,
ndl_type: NDLType::Actual,
};
let model_2 = BuehlmannModel::new(config_instance);
println!("{:?}", model_2.config());


// model with fluent-interface-like config
let config_with_gf = BuehlmannConfig::new().gradient_factors(30, 70);
let config_with_gf = BuehlmannConfig::default().gradient_factors(30, 70);
let model_3 = BuehlmannModel::new(config_with_gf);
println!("{:?}", model_3.config()); // BuehlmannConfig { gf: (30, 70) }
}
23 changes: 20 additions & 3 deletions src/buehlmann/buehlmann_config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::common::{AscentRatePerMinute, ConfigValidationErr, DecoModelConfig, GradientFactors, MbarPressure};
use crate::common::{AscentRatePerMinute, ConfigValidationErr, DecoModelConfig, GradientFactors, MbarPressure, NDLType};

const GF_RANGE_ERR_MSG: &str = "GF values have to be in 1-100 range";
const GF_ORDER_ERR_MSG: &str = "GFLow can't be higher than GFHigh";
Expand All @@ -10,6 +10,7 @@ pub struct BuehlmannConfig {
pub gf: GradientFactors,
pub surface_pressure: MbarPressure,
pub deco_ascent_rate: AscentRatePerMinute,
pub ndl_type: NDLType,
}

impl BuehlmannConfig {
Expand All @@ -31,21 +32,31 @@ impl BuehlmannConfig {
self.deco_ascent_rate = deco_ascent_rate;
self
}

pub fn ndl_type(mut self, ndl_type: NDLType) -> Self {
self.ndl_type = ndl_type;
self
}
}

impl Default for BuehlmannConfig {
fn default() -> Self {
Self {
gf: (100, 100),
surface_pressure: 1013,
deco_ascent_rate: 9.,
deco_ascent_rate: 10.,
ndl_type: NDLType::Actual,
}
}
}

impl DecoModelConfig for BuehlmannConfig {
fn validate(&self) -> Result<(), ConfigValidationErr> {
let Self { gf, surface_pressure, deco_ascent_rate } = self;
let Self { gf,
surface_pressure,
deco_ascent_rate,
..
} = self;

self.validate_gradient_factors(gf)?;
self.validate_surface_pressure(surface_pressure)?;
Expand All @@ -61,6 +72,12 @@ impl DecoModelConfig for BuehlmannConfig {
fn deco_ascent_rate(&self) -> AscentRatePerMinute {
self.deco_ascent_rate
}

fn ndl_type(&self) -> NDLType {
self.ndl_type.clone()
}


}

impl BuehlmannConfig {
Expand Down
4 changes: 3 additions & 1 deletion src/buehlmann/buehlmann_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ impl DecoModel for BuehlmannModel {

fn deco(&self, gas_mixes: Vec<Gas>) -> DecoRuntime {
let mut deco = Deco::default();
deco.calc(self.fork(), gas_mixes)
let deco = deco.calc(self.fork(), gas_mixes);
deco

}

fn config(&self) -> BuehlmannConfig {
Expand Down
36 changes: 15 additions & 21 deletions src/common/deco.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ impl Deco {
..
} = sim_model.dive_state();
let ceiling = sim_model.ceiling();
let next_deco_stage = self.next_deco_action(&sim_model, gas_mixes.clone());
let (deco_action, next_switch_gas) = next_deco_stage;
let next_deco_action = self.next_deco_action(&sim_model, gas_mixes.clone());
let (deco_action, next_switch_gas) = next_deco_action;
let mut deco_stages: Vec<DecoStage> = vec![];

// handle deco actions
Expand Down Expand Up @@ -207,7 +207,8 @@ impl Deco {
return (None, None);
}

if !sim_model.in_deco() {
let ceiling = sim_model.ceiling();
if !(ceiling > 0.) {
// no deco obligations - linear ascent
return (Some(DecoAction::AscentToCeil), None);
} else {
Expand All @@ -223,7 +224,6 @@ impl Deco {
}
}

let ceiling = sim_model.ceiling();
let ceiling_padding = current_depth - ceiling;

// within deco window
Expand Down Expand Up @@ -268,23 +268,17 @@ impl Deco {
}

fn register_deco_stage(&mut self, stage: DecoStage) {
let push_new_stage = match stage.stage_type {
// dedupe iterative deco stops and merge into one
DecoStageType::DecoStop => {
let mut push_new = true;
let last_stage = self.deco_stages.last_mut();
if let Some(last_stage) = last_stage {
if last_stage.stage_type == stage.stage_type {
last_stage.duration += stage.duration;
push_new = false;
}
}
push_new
},
_ => true
};

if push_new_stage {
// dedupe iterative deco stops and merge into one
let mut push_new = true;
let last_stage = self.deco_stages.last_mut();
if let Some(last_stage) = last_stage {
if last_stage.stage_type == stage.stage_type {
last_stage.duration += stage.duration;
last_stage.end_depth = stage.end_depth;
push_new = false;
}
}
if push_new {
self.deco_stages.push(stage);
}

Expand Down
17 changes: 14 additions & 3 deletions src/common/deco_model.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::common::{Depth, Gas, Seconds, Minutes, AscentRatePerMinute, CNSPercent};
use super::{ox_tox::OxTox, DecoRuntime, MbarPressure};
use crate::common::{AscentRatePerMinute, CNSPercent, Depth, Gas, Minutes, Seconds};
use super::{global_types::NDLType, ox_tox::OxTox, DecoRuntime, MbarPressure};

#[derive(Debug, PartialEq)]
pub struct ConfigValidationErr<'a> {
Expand All @@ -11,6 +11,7 @@ pub trait DecoModelConfig {
fn validate(&self) -> Result<(), ConfigValidationErr>;
fn surface_pressure(&self) -> MbarPressure;
fn deco_ascent_rate(&self) -> AscentRatePerMinute;
fn ndl_type(&self) -> NDLType;
}

#[derive(Debug)]
Expand Down Expand Up @@ -59,7 +60,17 @@ pub trait DecoModel {

/// is in deco check
fn in_deco(&self) -> bool {
self.ceiling() > 0.
let ndl_type_config = self.config().ndl_type();
match ndl_type_config {
NDLType::Actual => {
let current_gas = self.dive_state().gas;
let runtime = self.deco(vec![current_gas]);
let deco_stages = runtime.deco_stages;
deco_stages.len() > 1

},
NDLType::ByCeiling => self.ceiling() > 0.,
}
}


Expand Down
6 changes: 6 additions & 0 deletions src/common/global_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ pub type GradientFactors = (u8, u8);
pub type MbarPressure = u16;
pub type AscentRatePerMinute = f64;
pub type CNSPercent = f64;

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum NDLType {
Actual, // take into consideration off-gassing during ascent
ByCeiling // treat NDL as a point when ceiling > 0.
}
3 changes: 2 additions & 1 deletion src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ pub use global_types::{
GradientFactor,
MbarPressure,
AscentRatePerMinute,
CNSPercent
CNSPercent,
NDLType,
};
pub use record::RecordData;
pub use deco::{Deco, DecoStage, DecoStageType, DecoRuntime};
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use common::{
DecoStage,
DecoStageType,
DecoRuntime,
Sim
Sim,
NDLType,
};

47 changes: 32 additions & 15 deletions tests/buehlmann_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
use dive_deco::{ BuehlmannConfig, BuehlmannModel, DecoModel, Gas, Minutes, Supersaturation };
use dive_deco::{ BuehlmannConfig, BuehlmannModel, DecoModel, Gas, Minutes, Supersaturation, NDLType };
pub mod fixtures;

// general high-level model tests
#[test]
#[should_panic]
fn test_should_panic_on_invalid_depth() {
let mut model = fixtures::model_default();
model.record(-10., 1, &fixtures::gas_air());
}

#[test]
fn test_ceiling() {
Expand Down Expand Up @@ -61,7 +67,25 @@ fn test_model_records_equality() {

#[test]
fn test_ndl_calculation() {
let mut model = fixtures::model_default();
let config = BuehlmannConfig::default().ndl_type(NDLType::ByCeiling);
let mut model = BuehlmannModel::new(config);

let air = Gas::new(0.21, 0.);
let depth = 30.;

// with 21/00 at 30m expect NDL 16
model.record(depth, 0, &air);
assert_eq!(model.ndl(), 16);

// expect NDL 15 after 1 min
model.record(depth, 60, &air);
assert_eq!(model.ndl(), 15);
}

#[test]
fn test_ndl_calculation_by_ceiling() {
let config = BuehlmannConfig::default().ndl_type(NDLType::ByCeiling);
let mut model = BuehlmannModel::new(config);

let air = Gas::new(0.21, 0.);
let depth = 30.;
Expand Down Expand Up @@ -94,21 +118,21 @@ fn test_multi_gas_ndl() {
let ean_28 = Gas::new(0.28, 0.);

model.record(30., 0 * 60, &air);
assert_eq!(model.ndl(), 16);
assert_eq!(model.ndl(), 19);

model.record(30., 10 * 60, &air);
assert_eq!(model.ndl(), 6);
assert_eq!(model.ndl(), 9);

model.record(30., 0 * 60, &ean_28);
assert_eq!(model.ndl(), 10);
assert_eq!(model.ndl(), 13);
}

#[test]
fn test_ndl_with_gf() {
let mut model = fixtures::model_gf((70, 70));
let air = Gas::new(0.21, 0.);
model.record(20., 0 * 60, &air);
assert_eq!(model.ndl(), 21);
assert_eq!(model.ndl(), 24);
}

#[test]
Expand All @@ -121,7 +145,7 @@ fn test_altitude() {
}

#[test]
fn test_example_deco_start() {
fn test_example_ceiling_start() {
let mut model = BuehlmannModel::new(
BuehlmannConfig::new()
.gradient_factors(30, 70)
Expand All @@ -137,7 +161,7 @@ fn test_example_deco_start() {
}

#[test]
fn test_example_deco() {
fn test_example_ceiling() {
let mut model = BuehlmannModel::new(
BuehlmannConfig::new()
.gradient_factors(30, 70)
Expand All @@ -153,10 +177,3 @@ fn test_example_deco() {

assert_eq!(model.ceiling(), 12.455491216740299);
}

#[test]
#[should_panic]
fn test_should_panic_on_invalid_depth() {
let mut model = fixtures::model_default();
model.record(-10., 1, &fixtures::gas_air());
}
Loading

0 comments on commit ebb991b

Please sign in to comment.