#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2011-2012 Thomas Chiroux
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.
# If not, see <http://www.gnu.org/licenses/gpl.html>
#
# This module is part of dipplanner, a Dive planning Tool written in python
"""Buhlmann model module
Contains:
Model -- class
"""
__authors__ = [
# alphabetical order by last name
'Thomas Chiroux', ]
import logging
import copy
# local imports
from dipplanner import settings
from dipplanner.model.buhlmann.compartment import Compartment
from dipplanner.model.buhlmann.gradient import Gradient
from dipplanner.model.buhlmann.oxygen_toxicity import OxTox
from dipplanner import tools
[docs]class Model(object):
"""Represents a Buhlmann model.
Composed of a tissue array of Compartment[]
Has an OxTox and Gradient object
Can throw a ModelStateException propagated from a Compartment if pressures
or time is out of bounds.
Models are initialised by initModel() if they are new models, or
validated by validateModel() if they are rebuild from a saved model.
The model is capable of ascending or descending via ascDec() using the
ascDec() method of Compartment,
or accounting for a constant depth using the
constDepth() method of Compartment.
Attributes:
* tissues (list)-- a list of Compartments
* gradient (Gradient) -- gradient factor object
* ox_tox (OxTox) -- OxTox object
* metadata (str) -- Stores infos about where the model was created
* units (str) -- only 'metric' allowed
* COMPS (int) -- static info : number of compartments
* MODEL_VALIDATION_SUCCESS (int) -- static const for validation success
* MODEL_VALIDATION_FAILURE (int) -- static const for validation failure
"""
COMPS = 16
#TODO: SUPPRIMER CES DEUX ELEMENTS ET REMPLACER PAR DES RAISE
MODEL_VALIDATION_SUCCESS = 1
MODEL_VALIDATION_FAILURE = 0
[docs] def __init__(self):
"""Constructor for model class
*Keyword arguments:*
<none>
*Returns:*
<nothing>
*Raise:*
<nothing>
"""
#initiate class logger
self.logger = logging.getLogger(
"dipplanner.model.buhlmann.model.Model")
self.logger.debug("creating an instance of Model")
self.units = 'metric'
self.tissues = []
self.ox_tox = OxTox()
self.gradient = None
self.init_gradient()
for _ in range(0, self.COMPS):
self.tissues.append(Compartment())
self.set_time_constants()
for comp in self.tissues:
comp.set_pp(0.0, 0.79 *
(settings.AMBIANT_PRESSURE_SURFACE -
tools.calculate_pp_h2o_surf(settings.SURFACE_TEMP)))
#TODO: Check above : 0.79 or settings.DEFAULT_AIR_PPN2 (tdb) ?
self.metadata = "(none)"
[docs] def __deepcopy__(self, memo):
"""deepcopy method will be called by copy.deepcopy
Used for "cloning" the object into another new object.
*Keyword Arguments:*
:memo: -- not used here
*Returns:*
Model -- Model object copy of itself
*Raise:*
<nothing>
"""
newobj = Model()
newobj.units = self.units
newobj.ox_tox = copy.deepcopy(self.ox_tox)
newobj.gradient = copy.deepcopy(self.gradient)
newobj.metadata = self.metadata
for i in range(0, len(self.tissues)):
newobj.tissues[i] = copy.deepcopy(self.tissues[i])
return newobj
def __repr__(self):
"""Returns a string representing the model
*Keyword Arguments:*
<none>
*Returns:*
str -- string representation of the model
*Raise:*
<nothing>
"""
model_string = "" # "Compartment pressures:\n"
for comp_number in range(0, self.COMPS):
model_string += "C:%s He:%s N2:%s gf:%s \
mv_at:%s max_amb:%s MV:%s\n" % \
(comp_number,
self.tissues[comp_number].pp_he,
self.tissues[comp_number].pp_n2,
self.gradient.gf,
self.tissues[comp_number].get_m_value_at(
settings.AMBIANT_PRESSURE_SURFACE),
(self.tissues[comp_number].get_max_amb(
self.gradient.gf)) * 1,
self.tissues[comp_number].get_mv(
settings.AMBIANT_PRESSURE_SURFACE))
#model_string += "Ceiling: %s\n" % self.ceiling()
#model_string += "Max surface M-Value: %s\n" % self.m_value(0.0)
#model_string += "OTUs accumulated: %s" % self.ox_tox.otu
return model_string
[docs] def __str__(self):
"""Return a human readable name of the segment
*Keyword Arguments:*
<none>
*Returns:*
str -- string representation of the model
*Raise:*
<nothing>
"""
return self.__repr__()
[docs] def __unicode__(self):
"""Return a human readable name of the segment in unicode
*Keyword Arguments:*
<none>
*Returns:*
ustr -- unicode string representation of the model
*Raise:*
<nothing>
"""
return u"%s" % self.__repr__()
[docs] def init_gradient(self):
"""Initialise the gradient attribute
uses the default settings parameters for gf_low and high
*Keyword arguments:*
<none>
*Returns:*
<nothing>
*Raise:*
<nothing>
"""
self.gradient = Gradient(settings.GF_LOW, settings.GF_HIGH)
[docs] def set_time_constants(self, deco_model=settings.DECO_MODEL):
"""Initialize time constants in buhlmann tissue list
Only for metric values
*Keyword arguments:*
:deco_model: (str) -- "ZHL16b" or "ZHL16c"
*Returns:*
<nothing>
*Raise:*
<nothing>
"""
# note: comparing with buhlmann original (1990) ZH-L16 a coeficient,
# there is here a x10 factor for a coeficient
#h_he, h_n2, a_he, b_he, a_n2, b_n2
if deco_model == "ZHL16c":
self.logger.info("model used: Buhlmann ZHL16c")
self.tissues[0].set_compartment_time_constants(
1.88, 5.0, 16.189, 0.4770, 11.696, 0.5578)
self.tissues[1].set_compartment_time_constants(
3.02, 8.0, 13.83, 0.5747, 10.0, 0.6514)
self.tissues[2].set_compartment_time_constants(
4.72, 12.5, 11.919, 0.6527, 8.618, 0.7222)
self.tissues[3].set_compartment_time_constants(
6.99, 18.5, 10.458, 0.7223, 7.562, 0.7825)
self.tissues[4].set_compartment_time_constants(
10.21, 27.0, 9.220, 0.7582, 6.667, 0.8126)
self.tissues[5].set_compartment_time_constants(
14.48, 38.3, 8.205, 0.7957, 5.60, 0.8434)
self.tissues[6].set_compartment_time_constants(
20.53, 54.3, 7.305, 0.8279, 4.947, 0.8693)
self.tissues[7].set_compartment_time_constants(
29.11, 77.0, 6.502, 0.8553, 4.5, 0.8910)
self.tissues[8].set_compartment_time_constants(
41.20, 109.0, 5.950, 0.8757, 4.187, 0.9092)
self.tissues[9].set_compartment_time_constants(
55.19, 146.0, 5.545, 0.8903, 3.798, 0.9222)
self.tissues[10].set_compartment_time_constants(
70.69, 187.0, 5.333, 0.8997, 3.497, 0.9319)
self.tissues[11].set_compartment_time_constants(
90.34, 239.0, 5.189, 0.9073, 3.223, 0.9403)
self.tissues[12].set_compartment_time_constants(
115.29, 305.0, 5.181, 0.9122, 2.850, 0.9477)
self.tissues[13].set_compartment_time_constants(
147.42, 390.0, 5.176, 0.9171, 2.737, 0.9544)
self.tissues[14].set_compartment_time_constants(
188.24, 498.0, 5.172, 0.9217, 2.523, 0.9602)
self.tissues[15].set_compartment_time_constants(
240.03, 635.0, 5.119, 0.9267, 2.327, 0.9653)
elif deco_model == "ZHL16b":
self.logger.info("model used: Buhlmann ZHL16b")
self.tissues[0].set_compartment_time_constants(
1.88, 5.0, 16.189, 0.4770, 11.696, 0.5578)
self.tissues[1].set_compartment_time_constants(
3.02, 8.0, 13.83, 0.5747, 10.0, 0.6514)
self.tissues[2].set_compartment_time_constants(
4.72, 12.5, 11.919, 0.6527, 8.618, 0.7222)
self.tissues[3].set_compartment_time_constants(
6.99, 18.5, 10.458, 0.7223, 7.562, 0.7825)
self.tissues[4].set_compartment_time_constants(
10.21, 27.0, 9.220, 0.7582, 6.667, 0.8126)
self.tissues[5].set_compartment_time_constants(
14.48, 38.3, 8.205, 0.7957, 5.933, 0.8434)
self.tissues[6].set_compartment_time_constants(
20.53, 54.3, 7.305, 0.8279, 5.282, 0.8693)
self.tissues[7].set_compartment_time_constants(
29.11, 77.0, 6.502, 0.8553, 4.71, 0.8910)
self.tissues[8].set_compartment_time_constants(
41.20, 109.0, 5.950, 0.8757, 4.187, 0.9092)
self.tissues[9].set_compartment_time_constants(
55.19, 146.0, 5.545, 0.8903, 3.798, 0.9222)
self.tissues[10].set_compartment_time_constants(
70.69, 187.0, 5.333, 0.8997, 3.497, 0.9319)
self.tissues[11].set_compartment_time_constants(
90.34, 239.0, 5.189, 0.9073, 3.223, 0.9403)
self.tissues[12].set_compartment_time_constants(
115.29, 305.0, 5.181, 0.9122, 2.971, 0.9477)
self.tissues[13].set_compartment_time_constants(
147.42, 390.0, 5.176, 0.9171, 2.737, 0.9544)
self.tissues[14].set_compartment_time_constants(
188.24, 498.0, 5.172, 0.9217, 2.523, 0.9602)
self.tissues[15].set_compartment_time_constants(
240.03, 635.0, 5.119, 0.9267, 2.327, 0.9653)
[docs] def validate_model(self):
"""Validate model - checks over the model and looks for corruption
This is needed to check a model that has been loaded from XML
Resets time constants
*Keyword arguments:*
<none>
*Returns:*
self.MODEL_VALIDATION_SUCCESS -- if OK
self.MODEL_VALIDATION_FAILURE -- if not OK
*Raise:*
<nothing>
"""
time_constant_zero = False # need for resetting time constants
for comp in self.tissues:
if comp.pp_n2 <= 0.0:
return self.MODEL_VALIDATION_FAILURE
if comp.pp_he < 0.0:
return self.MODEL_VALIDATION_FAILURE
if comp.k_he == 0.0 or \
comp.k_n2 == 0.0 or \
comp.a_he == 0.0 or \
comp.b_he == 0.0 or \
comp.a_n2 == 0.0 or \
comp.b_n2 == 0.0:
time_constant_zero = True
if time_constant_zero:
self.set_time_constants()
return self.MODEL_VALIDATION_SUCCESS
[docs] def control_compartment(self):
"""Determine the controlling compartment at ceiling (1-16)
*Keyword arguments:*
<none>
*Returns:*
integer -- reference number of the controlling
compartment (between 1 to 16)
*Raise:*
<nothing>
"""
control_compartment_number = 0
max_pressure = 0.0
for comp_number in range(0, self.COMPS):
pressure = self.tissues[comp_number].get_max_amb(
self.gradient.gf) - settings.AMBIANT_PRESSURE_SURFACE
#self.logger.debug("pressure:%s" % pressure)
if pressure > max_pressure:
control_compartment_number = comp_number
max_pressure = pressure
return control_compartment_number + 1
[docs] def ceiling(self):
"""Determine the current ceiling depth
*Keyword arguments:*
<none>
*Returns:*
float -- ceiling depth in meter
*Raise:*
<nothing>
"""
pressure = 0.0
for comp in self.tissues:
#Get compartment tolerated ambient pressure and convert from
# absolute pressure to depth
comp_pressure = comp.get_max_amb(
self.gradient.gf) - settings.AMBIANT_PRESSURE_SURFACE
if comp_pressure > pressure:
pressure = comp_pressure
return tools.pressure_to_depth(pressure)
[docs] def ceiling_in_pabs(self):
"""Determine the current ceiling
*Keyword arguments:*
<none>
*Returns:*
float -- ceiling in bar (absolute pressure)
*Raise:*
<nothing>
"""
pressure = 0.0
for comp in self.tissues:
#Get compartment tolerated ambient pressure and convert from
#absolute pressure to depth
comp_pressure = comp.get_max_amb(self.gradient.gf)
if comp_pressure > pressure:
pressure = comp_pressure
return pressure
[docs] def m_value(self, pressure):
"""Determine the maximum M-Value for a given depth (pressure)
*Keyword arguments:*
:pressure: (float) -- in bar
*Returns*
float -- max M-Value
*Raise:*
<nothing>
"""
p_absolute = pressure + settings.AMBIANT_PRESSURE_SURFACE
compartment_mv = 0.0
max_mv = 0.0
for comp in self.tissues:
compartment_mv = comp.get_mv(p_absolute)
if compartment_mv > max_mv:
max_mv = compartment_mv
#self.logger.debug("max mv : %s" % max_mv)
return max_mv
[docs] def const_depth(self, pressure, seg_time, f_he, f_n2, pp_o2):
"""Constant depth profile.
Calls Compartment.constDepth for each compartment to update the model.
*Keyword arguments:*
:pressure: (float)-- pressure of this depth of segment in bar
:seg_time: (float) -- Time of segment in seconds
:f_he: (float) -- fraction of inert gas Helium in inspired gas mix
:f_n2: (float) -- fraction of inert gas Nitrogen in
inspired gas mix
:pp_o2: (float) -- For CCR mode, partial pressure of oxygen in bar.
If == 0.0, then open circuit
*Returns:*
<nothing>
*Raise:*
ModelStateException
"""
ambiant_pressure = pressure + settings.AMBIANT_PRESSURE_SURFACE
if pp_o2 > 0.0:
# CCR mode
#Determine pInert by subtracting absolute oxygen pressure and pH2O
#Note that if f_he and f_n2 == 0.0 then need to force pp's to zero
if f_he + f_n2 > 0.0:
p_inert = ambiant_pressure - pp_o2 - \
tools.calculate_pp_h2o_surf(settings.SURFACE_TEMP)
else:
p_inert = 0.0
#Verify that pInert is positive. If the setpoint is close to or
# less than the depth then there is no inert gas.
if p_inert > 0.0:
pp_he_inspired = (p_inert * f_he) / (f_he + f_n2)
pp_n2_inspired = (p_inert * f_n2) / (f_he + f_n2)
else:
pp_he_inspired = 0.0
pp_n2_inspired = 0.0
# update OxTox object
pp_o2_inspired = pp_o2
#Check that ppO2Inspired is not greater than the depth.
#This occurs in shallow deco when the setpoint specified is > depth
if pp_o2_inspired <= ambiant_pressure and p_inert > 0.0:
# pp_o2 is the setpoint
self.ox_tox.add_o2(seg_time, pp_o2)
else:
# pp_o2 is equal to the depth, also true is there is
# no inert gaz
self.ox_tox.add_o2(seg_time, ambiant_pressure -
tools.calculate_pp_h2o_surf(
settings.SURFACE_TEMP))
else:
# OC mode
pp_he_inspired = (ambiant_pressure -
tools.calculate_pp_h2o_surf(
settings.SURFACE_TEMP)) * f_he
pp_n2_inspired = (ambiant_pressure -
tools.calculate_pp_h2o_surf(
settings.SURFACE_TEMP)) * f_n2
# update ox_tox
if pressure == 0.0: # surface
self.ox_tox.remove_o2(seg_time)
else:
self.ox_tox.add_o2(
seg_time,
(ambiant_pressure -
tools.calculate_pp_h2o_surf(settings.SURFACE_TEMP))
* (1.0 - f_he - f_n2))
if seg_time > 0:
for comp in self.tissues:
comp.const_depth(pp_he_inspired, pp_n2_inspired, seg_time)
[docs] def asc_desc(self, start, finish, rate, f_he, f_n2, pp_o2):
"""Ascend/Descend profile.
Calls Compartment.asc_desc to update compartments
*Keyword arguments:*
:start: (float) -- start pressure of this segment in bar
(WARNING: not meter ! it's a pressure)
:finish: (float) -- finish pressure of this segment in bar
(WARNING: not meter ! it's a pressure)
:rate: (float) -- rate of ascent or descent in m/s
:f_he: (float) -- Fraction of inert gas Helium in inspired gas mix
:f_n2: (float) -- Fraction of inert gas Nitrogen
in inspired gas mix
:pp_o2: (float) -- For CCR mode, partial pressure of oxygen in bar.
If == 0.0, then open circuit
*Returns:*
<nothing>
*Raise:*
ModelStateException
"""
# rem: here we do not bother of PP_H2O like in constant_depth : WHY ?
start_ambiant_pressure = start + settings.AMBIANT_PRESSURE_SURFACE
finish_ambiant_pressure = finish + settings.AMBIANT_PRESSURE_SURFACE
seg_time = abs(float(finish) - float(start)) / rate
if pp_o2 > 0.0:
# CCR mode
# Calculate inert gas partial pressure == pAmb - pO2 - pH2O
p_inert_start = start_ambiant_pressure - pp_o2 - \
tools.calculate_pp_h2o_surf(settings.SURFACE_TEMP)
p_inert_finish = finish_ambiant_pressure - pp_o2 - \
tools.calculate_pp_h2o_surf(settings.SURFACE_TEMP)
# Check that it doesn't go less than zero.
# Could be due to shallow deco or starting on high setpoint
if p_inert_start < 0.0:
p_inert_start = 0.0
if p_inert_finish < 0.0:
p_inert_finish = 0.0
# Separate into He and N2 components, checking that we are not
# on pure O2 (or we get an arithmetic error)
if f_he + f_n2 > 0.0:
pp_he_inspired = (p_inert_start * f_he) / (f_he + f_n2)
pp_n2_inspired = (p_inert_start * f_n2) / (f_he + f_n2)
# calculate rate of change of each inert gas
rate_he = ((p_inert_finish * f_he) / (f_he + f_n2) -
pp_he_inspired) / (seg_time)
rate_n2 = ((p_inert_finish * f_n2) / (f_he + f_n2) -
pp_n2_inspired) / (seg_time)
else:
pp_he_inspired = 0.0
pp_n2_inspired = 0.0
rate_he = 0.0
rate_n2 = 0.0
# update ox_tox, constant pp_o2
# TODO - what if depth is less than pO2 in msw ?
self.ox_tox.add_o2(seg_time, pp_o2)
else:
# OC mode
# calculate He and N2 components
pp_he_inspired = (start_ambiant_pressure -
tools.calculate_pp_h2o_surf(
settings.SURFACE_TEMP)) * f_he
pp_n2_inspired = (start_ambiant_pressure -
tools.calculate_pp_h2o_surf(
settings.SURFACE_TEMP)) * f_n2
rate_he = rate * f_he
rate_n2 = rate * f_n2
# update ox_tox, use average pp_o2
pp_o2_inspired_avg = ((start_ambiant_pressure -
finish_ambiant_pressure) / 2
+ finish_ambiant_pressure -
tools.calculate_pp_h2o_surf(
settings.SURFACE_TEMP)) \
* (1.0 - f_he - f_n2)
self.ox_tox.add_o2(seg_time, pp_o2_inspired_avg)
for comp in self.tissues:
comp.asc_desc(pp_he_inspired, pp_n2_inspired,
rate_he, rate_n2, seg_time)