File Coverage

blib/lib/Deco/Tissue.pm
Criterion Covered Total %
statement 188 190 98.9
branch 37 48 77.0
condition 20 24 83.3
subroutine 29 29 100.0
pod 19 19 100.0
total 293 310 94.5


line stmt bran cond sub pod time code
1             #######################################
2             # Module : Tissue.pm
3             # Author : Jaap Voets
4             # Date : 04-05-2006
5             # $Revision$
6             #######################################
7             package Deco::Tissue;
8              
9 5     5   920 use strict;
  5         9  
  5         182  
10 5     5   25 use warnings;
  5         10  
  5         150  
11 5     5   25 use Carp;
  5         9  
  5         306  
12 5     5   4184 use POSIX qw( pow );
  5         39803  
  5         39  
13             our $VERSION = '0.5';
14              
15             # water vapor at 37 degrees celsius (i.e. our lungs)
16 5     5   6301 use constant WATER_VAPOR_PRESSURE => 0.0627;
  5         17  
  5         373  
17              
18             # carbon dioxide pressure for normal persons
19 5     5   24 use constant CO2_PRESSURE => 0.0534;
  5         10  
  5         12871  
20              
21             # properties of the object that can be set by the user
22             our @PROPERTIES = ('halftime','m0', 'deltam', 'o2fraction', 'waterfactor','topsidepressure','offgasfactor','RQ', 'nr');
23              
24             # supported GASES, with fractions in breathing mix
25             our %GASES = ( 'o2' => 0.21,
26             'n2' => 0.78,
27             'he' => 0);
28              
29             # Constructor
30             sub new {
31 179     179 1 9258 my $class = shift;
32 179         754 my %args = @_;
33              
34 179         296 my $self = {};
35              
36             # defaults
37             # O2 fraction
38 179         566 $self->{o2}->{fraction} = 0.21;
39            
40             # N2 fraction
41 179         411 $self->{n2}->{fraction} = 0.78;
42            
43             # pressure at start of dive, normally 1 bar sea level
44 179         283 $self->{topsidepressure} = 1; #bar
45            
46             # specific weight for the type of water, fresh = 1.000, salt = 1.030
47 179         255 $self->{waterfactor} = 1.000;
48              
49             # offgassing might be slower than ongassing
50 179         237 $self->{offgasfactor} = 1;
51            
52             # respiratory quotient RQ. This is the amount of CO2 produced per O2
53             # schreiner uses 0.8 (more conservative), US Navy 0.9 , Buhlmann 1.0
54 179         250 $self->{RQ} = 0.8;
55            
56 179         247 $self->{nr} = 0;
57            
58             # we need a few things for a valid tissue
59             # half time (in minutes)
60             # M0 and deltaM value (in bar)
61 179         473 foreach my $arg (keys %args) {
62 709 50       1032 if ( grep {/$arg/i} @PROPERTIES) {
  6381         18534  
63 709         2128 $self->{ lc($arg) } = $args{ $arg };
64             } else {
65 0         0 carp "Argument $arg is not valid";
66             }
67             }
68            
69 179         493 bless $self, $class;
70              
71              
72             # additional properties, these are for finetuning
73            
74             # current internal pressure for N2, assumes equilibrium at current level (sea or higher)
75 179         411 $self->{n2}->{pressure} = $self->_alveolarPressure( depth=>0 , gas=>'n2');
76              
77             # defaults to sea level in bar
78 179         359 $self->{ambientpressure} = $self->{topsidepressure};
79              
80             # current depth in meters
81 179         246 $self->{depth} = 0;
82              
83             # previous depth
84 179         228 $self->{previousdepth} = 0;
85              
86             # some timestamps in seconds
87 179         413 $self->{time}->{current} = 0;
88 179         284 $self->{time}->{previous} = 0;
89 179         247 $self->{time}->{lastdepthchange} = 0;
90              
91             # oxygen exposure tracking through OTU's
92 179         264 $self->{otu} = 0;
93              
94             # haldane formula for current parameters, returns a ref to a sub
95 179         358 $self->{haldane} = $self->_haldanePressure();
96              
97 179         1152 return $self;
98             }
99              
100             # get the tissue nr
101             sub nr {
102 666     666 1 1281 my $self = shift;
103 666 50       1515 croak "Tissue numbers not set yet" unless ($self->{nr});
104 666         5381 return $self->{nr};
105             }
106              
107             # return the current internal for requested gas pressure
108             sub internalpressure {
109 1099     1099 1 2532 my $self = shift;
110 1099         1971 my %opt = @_;
111 1099   100     29746 my $gas = lc($opt{gas}) || 'n2';
112 1099 50       3302 croak "Asking for unsupported gas $gas" unless exists $GASES{$gas};
113 1099         6604 return $self->{$gas}->{pressure};
114             }
115              
116             # percentage of tissue pressure compared to allowed M0
117             sub percentage {
118 549     549 1 681 my $self = shift;
119 549         758 my %opt = @_;
120 549   50     30573 my $gas = lc($opt{gas}) || 'n2';
121 549         2105 return ( 100 * $self->internalpressure( gas => $gas) / $self->{m0}) ;
122             }
123              
124             # return the current OTU (Oxygen Toxicity Unit)
125             sub otu {
126 2     2 1 11 my $self = shift;
127 2         22 return $self->{otu};
128             }
129              
130             # calculate otu's, should be called after changing depth/time
131             # if pO2 > 0.5 then OTU = minutes x ( ( pO2 -0.5 ) / 0.5) ^ 0.83
132             sub calculate_otu {
133 552     552 1 652 my $self = shift;
134 552         1372 my $minutes = ($self->{time}->{current} - $self->{time}->{previous} ) / 60;
135 552         604 my $otu = 0;
136 552         993 my $pO2 = $self->ambientpressure() * $self->{o2}->{fraction};
137 552 100       1185 if ($pO2 > 0.5) {
138 38         1132 $otu = $minutes * POSIX::pow( 2 * $pO2 - 1, 0.83);
139             }
140 552         1708 $self->{otu} += $otu;
141 552         3719 return $self->{otu};
142             }
143              
144             # set time, depth combination
145             # this way we control the order of time/depth changes
146             # during descent, we want to change depth first, the time
147             # during ascent we want to change the time first then the depth
148             # this way we calculate the 'outer' profile in rectangles of the real profile
149             # and thus we are more conservative
150             sub point {
151 710     710 1 3400 my $self = shift;
152 710         1109 my ($time, $depth) = @_;
153              
154 710 100       2270 if ( $depth > $self->{previousdepth} ) {
    100          
155             # descending, so depth first
156 437         950 $self->depth( $depth );
157 437         1033 $self->time( $time );
158              
159             } elsif ( $depth < $self->{previousdepth} ) {
160             # ascending, so time first
161 255         501 $self->time( $time );
162 255         483 $self->depth( $depth );
163             } else {
164             # same depth, only time change
165 18         41 $self->time( $time );
166             }
167              
168 710         1693 return 1;
169             }
170              
171             # set gas fractions
172             sub gas {
173 12     12 1 186 my $self = shift;
174 12         37 my %gaslist = @_;
175 12         32 foreach my $keygas (keys %gaslist) {
176 22         41 my $gas = lc($keygas);
177 22 100       63 if (! exists $GASES{$gas} ) {
178 2         48 croak "Can't use gas $gas";
179             }
180 20         32 my $fraction = $gaslist{$keygas};
181             # if using percentage, convert to fractions
182 20 100       52 if ($fraction > 1) {
183 11         18 $fraction = $fraction / 100;
184             }
185              
186 20         64 $self->{$gas}->{fraction} = $fraction;
187             }
188 10         44 return 1;
189             }
190              
191             # set new depth point
192             sub depth {
193 698     698 1 1256 my $self = shift;
194 698         898 my $depth = shift;
195            
196 698 100       1339 croak "Depth can not be negative" unless($depth >= 0);
197              
198 697 100       1492 if ($depth != $self->{depth} ) {
199 601         890 $self->{previousdepth} = $self->{depth};
200 601         738 $self->{depth} = $depth;
201            
202             # remember this depthchange on the time scale
203 601         1331 $self->{time}->{lastdepthchange} = $self->{time}->{current};
204             #print "Time of last depth change is: " . $self->{time}->{lastdepthchange} ."\n";
205              
206             # when depth changes we need to recalculate the Haldane formula
207 601         1094 $self->{haldane} = $self->_haldanePressure();
208             }
209              
210 697         2968 return $self->{depth};
211             }
212              
213             # set new timestamp in seconds
214              
215             sub time {
216 711     711 1 2143 my $self = shift;
217 711         832 my $time = shift;
218            
219 711 100       1851 croak "Time can not be negative" unless($time >= 0);
220 710 100       2195 if ($time != $self->{time}->{current} ) {
221 496         908 $self->{time}->{previous} = $self->{time}->{current};
222 496         662 $self->{time}->{current} = $time;
223            
224             # when time changes, we need to recalculate the internal pressure
225 496         1025 my $minutes = ($time - $self->{time}->{lastdepthchange}) / 60;
226             #print "Minutes passed since last depth change: $minutes\n";
227 496         514 $self->{n2}->{pressure} = &{ $self->{haldane} }( $minutes );
  496         1169  
228             #print "So internal N2 is now: " . $self->{n2}->{pressure} . "\n";
229             }
230              
231 710         1892 return $self->{time}->{current};
232             }
233              
234              
235             # set or get halftime
236             sub halftime {
237 3     3 1 5 my $self = shift;
238 3         6 my $newvalue = shift;
239            
240 3 100       8 if ($newvalue) {
241 2 50       9 croak "Halftimes need to be entered in positives minutes" unless($newvalue >= 1);
242 2         4 $self->{halftime} = $newvalue;
243             }
244            
245 3         18 return $self->{halftime};
246             }
247              
248             # set or get Respiratory Quotient (RQ)
249             sub rq {
250 1     1 1 3 my $self = shift;
251 1         2 my $newvalue = shift;
252            
253 1 50       3 if ($newvalue) {
254 1 50 33     9 croak "RQ (Respiratory Quotient) needs to be entered in the range 0.7 - 1.1" unless($newvalue >= 0.7 and $newvalue <= 1.1);
255 1         2 $self->{RQ} = $newvalue;
256             }
257            
258 1         3 return $self->{RQ};
259             }
260              
261             # get the K value = ln(2) / halftime
262             sub k {
263 1594     1594 1 1777 my $self = shift;
264 1594         4378 return log(2) / $self->{halftime};
265             }
266              
267             # give ambient total pressure
268             sub ambientpressure {
269 2613     2613 1 3009 my $self = shift;
270 2613         2897 my $depth = shift;
271              
272 2613 100       5004 $depth = $self->{depth} unless defined $depth;
273              
274 2613         5305 my $press = $self->{topsidepressure} + $self->_depth2pressure( $depth );
275 2613         3760 $self->{ambientpressure} = $press;
276              
277 2613         6886 return $press;
278             }
279              
280             # print info about tissue
281             sub info {
282 1     1 1 3 my $self = shift;
283 1         2 my $gaslist = '';
284 1         4 foreach my $gas (keys %GASES) {
285 3   100     13 my $fraction = $self->{$gas}->{fraction} || 0;
286 3         24 $gaslist .= " $gas at " . sprintf("%.1f", 100 * $fraction) . "% ";
287             }
288            
289 1         7 my $info = "============================================================
290             = TISSUE INFO
291             =
292             = Halftime (min) : " . $self->{halftime} ."
293             = k (min^-1) : " . $self->k ."
294             = RQ : " . $self->{RQ} ."
295             = M0 (bar) : " . $self->{m0} ."
296             = delta M (bar/m) : " . $self->{deltam} ."
297             = Ambient (bar) : " . $self->ambientpressure() ."
298             = Depth (m) : " . $self->{depth} ."
299             = Total time (s) : " . $self->{time}->{current} ."
300             = Tissue N2 (bar) : " . $self->{n2}->{pressure} . "
301             = Alveo. N2 (bar) : " . $self->_alveolarPressure( gas => 'n2', depth => $self->{depth} ) ."
302             = M (bar) : " . $self->M( depth => $self->{depth}) ."
303             = Safe depth (m) : " . $self->safe_depth( gas => 'n2') ."
304             = No deco time (min) : " . $self->nodeco_time( gas => 'n2') ."
305             = OTU's : " . $self->{otu} ."
306             = Gas list : $gaslist
307             ===========================================================\n";
308              
309 1         4 return $info;
310             }
311              
312             # get the current M-values
313             sub M {
314 3     3 1 5 my $self = shift;
315 3         12 my %opt = @_;
316            
317 3   100     23 my $depth = $opt{depth} || $self->{depth}; # meters
318 3         16 my $M = $self->{m0} + $self->{deltam} * $depth / ( 10 * $self->{waterfactor} ); # bar
319 3         10 $self->{M} = $M;
320 3         22 return $M;
321             }
322              
323             # calculate the minimal depth to which we can safely ascend
324             # without exceeding the M0 value for the tissue
325              
326             sub safe_depth {
327 550     550 1 659 my $self = shift;
328 550         777 my %opt = @_;
329            
330 550   100     30376 my $gas = lc($opt{gas}) || 'n2';
331 550 50       2091 croak "The Delta M value has not been set for this tissue" unless ($self->{deltam} > 0);
332 550         1449 my $safe_depth = ( $self->{$gas}->{pressure} - $self->{m0} ) / $self->{deltam};
333            
334             # negative values mean we can go to the surface
335 550 50       1150 if ($safe_depth < 0 ) {
336 550         664 $safe_depth = 0;
337             }
338            
339 550         1782 return $safe_depth;
340             }
341              
342             # calculate how long this tissue is allowed to stay at the current depth
343             # without getting into Deco
344             # note: time is returned in minutes
345             sub nodeco_time {
346 733     733 1 925 my $self = shift;
347 733         1087 my %opt = @_;
348              
349 733   100     39937 my $gas = lc($opt{gas}) || 'n2';
350              
351             # shortcut: take P_no_deco to be M0 (instant go to surface)
352             # unless a specific pressure was specified (for time_until function)
353 733         2451 my $p_end = $self->{m0};
354 733         1725 my $time = $self->time_until_pressure( pressure => $p_end, gas => $gas );
355            
356 733         2329 return $time;
357              
358             }
359              
360             # calculate how many minutes this tissue
361             # can stay at the present depth until the
362             # given pressure (in bar) will be reached
363             #
364             # a special case of this function is d
365             # this is practically the same as the no_deco_time function
366             # but there we take some surfacing pressure
367             sub time_until_pressure {
368 1095     1095 1 2790 my $self = shift;
369 1095         2906 my %opt = @_;
370            
371 1095   100     25491 my $gas = lc($opt{gas}) || 'n2';
372 1095         1685 my $pressure = $opt{pressure};
373            
374             # alveolar pressure
375 1095         2654 my $p_alv = $self->_alveolarPressure( gas => $gas , depth => $self->{depth} );
376            
377 1095         2123 my $k = $self->k();
378 1095         1387 my $time_until = '-';
379            
380 1095         1365 my $depth = $self->{'depth'};
381 1095         1614 my $current_pressure = $self->{$gas}->{pressure};
382            
383 1095 50       2240 if ($current_pressure >= $pressure) {
384             # already at or over the wanted pressure
385 0         0 $time_until = 0;
386             } else {
387 1095         1262 my $denominator = $current_pressure - $p_alv;
388            
389 1095 100       1976 if ( $denominator ) {
390 1077         1118 my $nominator = $pressure - $p_alv;
391 1077         1175 my $fraction = $nominator / $denominator;
392            
393 1077 100       2214 if ($fraction > 0 ) {
394 639         1309 $time_until = -1 / $k * log( $fraction );
395             # round it to whole minutes
396 639         1375 $time_until = sprintf('%.0f', $time_until);
397             }
398             }
399             }
400            
401 1095         3244 return $time_until;
402             }
403              
404              
405             ##########################################
406             # PRIVATE FUNCTIONS
407             ##########################################
408              
409             # convert meters to bar
410             # this does NOT include the starting pressure
411             # so 0 meters = 0 bar water pressure
412             sub _depth2pressure {
413 2615     2615   2703 my $self = shift;
414 2615         2756 my $depth = shift;
415 2615         4784 my $press = $depth * $self->{waterfactor} / 10;
416 2615         5949 return $press;
417             }
418              
419             # use haldanian formula to solve the current pressure in tissue
420             # as long as the depth remains constant, this formula is still valid
421             sub _haldanePressure {
422 781     781   995 my $self = shift;
423 781   50     45453 my $gas = lc(shift) || 'n2';
424 781 50       2668 croak "Asking for unsupported gas $gas" unless exists $GASES{$gas};
425              
426             # we need the current tissure pressure, at t=0 for the depth
427 781         1696 my $tissue_press0 = $self->{$gas}->{pressure};
428             #print "recalculating haldane formula. tissue pressure at t0 = $tissue_press0\n";
429              
430             # and the alveolar pressure
431 781         1885 my $alveolar = $self->_alveolarPressure( depth => $self->{depth} );
432              
433             # the time in minutes we have been at this depth, note that internal times are in seconds!
434             return sub {
435 497     497   586 my $t = shift;
436 497         1106 $alveolar + ($tissue_press0 - $alveolar ) * exp( -1 * $self->k() * $t );
437             }
438            
439 781         6387 }
440              
441             # return alvealor pressure for N2 (or other inert gas specified)
442             # see the Deco.pdf document for explanation on this calculation
443             sub _alveolarPressure {
444 2058     2058   2452 my $self = shift;
445 2058         6151 my %opt = @_;
446              
447 2058   100     5235 my $depth = $opt{depth} || 0;
448 2058   100     46440 my $gas = lc($opt{gas}) || 'n2';
449 2058 50       5521 croak "Asking for unsupported gas $gas" unless exists $GASES{$gas};
450            
451 2058         5305 my $press = $self->{$gas}->{fraction} * ( $self->ambientpressure( $depth ) - WATER_VAPOR_PRESSURE + ( ( 1 - $self->{RQ} ) / $self->{RQ} ) * CO2_PRESSURE );
452 2058         5062 return $press;
453             }
454              
455              
456             1;
457             __END__