Deco

 view release on metacpan or  search on metacpan

lib/Deco/Tissue.pm  view on Meta::CPAN

    my $self = {};

    # defaults
    # O2 fraction
    $self->{o2}->{fraction} = 0.21;
    
    # N2 fraction
    $self->{n2}->{fraction} = 0.78;
    
    # pressure at start of dive, normally 1 bar sea level
    $self->{topsidepressure} = 1; #bar
  	
    # specific weight for the type of water, fresh = 1.000, salt = 1.030
    $self->{waterfactor} = 1.000;

    # offgassing might be slower than ongassing
    $self->{offgasfactor} = 1;
    
    # respiratory quotient RQ. This is the amount of CO2 produced per O2
    # schreiner uses 0.8 (more conservative), US Navy 0.9 , Buhlmann 1.0
    $self->{RQ} = 0.8;
    
    $self->{nr} = 0;
    
    # we need a few things for a valid tissue
    # half time (in minutes)
    # M0 and deltaM value (in bar)
    foreach my $arg (keys %args) {
	if ( grep {/$arg/i} @PROPERTIES) {
	    $self->{ lc($arg) } = $args{ $arg };
	} else {
	    carp "Argument $arg is not valid";
	}
    }
    
    bless $self, $class;


    # additional properties, these are for finetuning
    
    # current internal pressure for N2, assumes equilibrium at current level (sea or higher)
    $self->{n2}->{pressure} = $self->_alveolarPressure( depth=>0 , gas=>'n2');

    # defaults to sea level in bar
    $self->{ambientpressure} = $self->{topsidepressure};

    # current depth in meters
    $self->{depth} = 0;

    # previous depth
    $self->{previousdepth} = 0;

    # some timestamps in seconds
    $self->{time}->{current}  = 0;
    $self->{time}->{previous} = 0;
    $self->{time}->{lastdepthchange} = 0;

    # oxygen exposure tracking through OTU's
    $self->{otu}   = 0;

    # haldane formula for current parameters, returns a ref to a sub
    $self->{haldane} = $self->_haldanePressure();

    return $self;
}

# get the tissue nr
sub nr {
    my $self = shift;
    croak "Tissue numbers not set yet" unless ($self->{nr});
    return $self->{nr};
}

# return the current internal for requested gas pressure
sub internalpressure {
    my $self = shift;
    my %opt  = @_;
    my $gas  = lc($opt{gas}) || 'n2';
    croak "Asking for unsupported gas $gas" unless exists $GASES{$gas};
    return $self->{$gas}->{pressure};
}

# percentage of tissue pressure compared to allowed M0
sub percentage {
    my $self = shift;
    my %opt  = @_;
    my $gas  = lc($opt{gas}) || 'n2';
    return ( 100 * $self->internalpressure( gas => $gas) / $self->{m0}) ;
}

# return the current OTU (Oxygen Toxicity Unit)
sub otu {
    my $self = shift;
    return $self->{otu};
}

# calculate otu's, should be called after changing depth/time
# if pO2 > 0.5 then OTU = minutes x ( ( pO2 -0.5 ) / 0.5) ^ 0.83
sub calculate_otu {
    my $self = shift;
    my $minutes = ($self->{time}->{current} - $self->{time}->{previous} ) / 60;
    my $otu = 0;
    my $pO2 = $self->ambientpressure() * $self->{o2}->{fraction};
    if ($pO2 > 0.5) {
	$otu = $minutes * POSIX::pow( 2 * $pO2 - 1,  0.83);
    }
    $self->{otu} += $otu;
    return $self->{otu};
}

# set time, depth combination
# this way we control the order of time/depth changes
# during descent, we want to change depth first, the time
# during ascent we want to change the time first then the depth
# this way we calculate the 'outer' profile in rectangles of the real profile
# and thus we are more conservative
sub point {
    my $self = shift;
    my ($time, $depth) = @_;

    if ( $depth > $self->{previousdepth} ) {
	# descending, so depth first 
	$self->depth( $depth );
	$self->time( $time );

    } elsif ( $depth < $self->{previousdepth} ) {
	# ascending, so time first
	$self->time( $time );
	$self->depth( $depth );
    } else {
	# same depth, only time change
	$self->time( $time );
    }

    return 1;
}

# set gas fractions
sub gas {
    my $self = shift;
    my %gaslist = @_;
    foreach my $keygas (keys %gaslist) {
	my $gas = lc($keygas);
	if (! exists $GASES{$gas} ) {
	    croak "Can't use gas $gas";
	}
	my $fraction = $gaslist{$keygas};
	# if using percentage, convert to fractions
	if ($fraction > 1) {
	    $fraction = $fraction / 100;
	}

	$self->{$gas}->{fraction} = $fraction;
    }
    return 1;
}

# set new depth point
sub depth {
    my $self = shift;
    my $depth = shift;
    
    croak "Depth can not be negative" unless($depth >= 0);

    if ($depth != $self->{depth} ) {
	$self->{previousdepth} = $self->{depth};
        $self->{depth}         = $depth;
	
	# remember this depthchange on the time scale
	$self->{time}->{lastdepthchange} = $self->{time}->{current};
	#print "Time of last depth change is: " . $self->{time}->{lastdepthchange} ."\n";

	# when depth changes we need to recalculate the Haldane formula
	$self->{haldane} = $self->_haldanePressure();
    }

    return $self->{depth};
}

# set new timestamp in seconds

sub time {
    my $self = shift;
    my $time = shift;
    
    croak "Time can not be negative" unless($time >= 0);
    if ($time != $self->{time}->{current} ) {
	$self->{time}->{previous} = $self->{time}->{current};
	$self->{time}->{current}  = $time;
	
	# when time changes, we need to recalculate the internal pressure
	my $minutes = ($time - $self->{time}->{lastdepthchange}) / 60;
	#print "Minutes passed since last depth change: $minutes\n";
	$self->{n2}->{pressure} = &{ $self->{haldane} }( $minutes );
	#print "So internal N2 is now: " . $self->{n2}->{pressure} . "\n";
    }

    return $self->{time}->{current};
}


# set or get halftime
sub halftime {
    my $self = shift;
    my $newvalue = shift;
    
    if ($newvalue) {
	croak "Halftimes need to be entered in positives minutes" unless($newvalue >= 1);
	$self->{halftime} = $newvalue;
    }
    
    return $self->{halftime};
}

# set or get Respiratory Quotient (RQ)
sub rq {
    my $self = shift;
    my $newvalue = shift;
    
    if ($newvalue) {
	croak "RQ (Respiratory Quotient) needs to be entered in the range 0.7 - 1.1" unless($newvalue >= 0.7 and $newvalue <= 1.1);
	$self->{RQ} = $newvalue;
    }
    
    return $self->{RQ};
}

# get the K value = ln(2) / halftime
sub k {
    my $self = shift;
    return log(2) / $self->{halftime};	
}

lib/Deco/Tissue.pm  view on Meta::CPAN


# calculate how many minutes this tissue
# can stay at the present depth until the
# given pressure (in bar) will be reached
#  
# a special case of this function is d
# this is practically the same as the no_deco_time function
# but there we take some surfacing pressure 
sub time_until_pressure {
    my $self = shift;
    my %opt = @_;
    
    my $gas = lc($opt{gas}) || 'n2';
    my $pressure = $opt{pressure};
	
    # alveolar pressure
    my $p_alv = $self->_alveolarPressure( gas => $gas , depth => $self->{depth} );
	
    my $k = $self->k();
    my $time_until = '-';
    
    my $depth = $self->{'depth'};
    my $current_pressure = $self->{$gas}->{pressure};
    
    if ($current_pressure >= $pressure) {
		# already at or over the wanted pressure
		$time_until = 0;
    } else {
		my $denominator = $current_pressure - $p_alv;
	    
		if ( $denominator ) {
			my $nominator = $pressure - $p_alv;
			my $fraction = $nominator / $denominator;
	
			if ($fraction > 0 ) {
			    $time_until = -1 / $k * log( $fraction );
			    # round it to whole minutes
			    $time_until = sprintf('%.0f', $time_until);
			}
	    } 
	}
    
    return $time_until;
}


##########################################
# PRIVATE FUNCTIONS
##########################################

# convert meters to bar
# this does NOT include the starting pressure
# so 0 meters = 0 bar water pressure
sub _depth2pressure {
    my $self = shift;
    my $depth = shift;
    my $press =  $depth  * $self->{waterfactor} / 10;
    return $press;
}

# use haldanian formula to solve the current pressure in tissue
# as long as the depth remains constant, this formula is still valid
sub _haldanePressure {
    my $self = shift;
    my $gas  = lc(shift) || 'n2';
    croak "Asking for unsupported gas $gas" unless exists $GASES{$gas};

    # we need the current tissure pressure, at t=0 for the depth
    my $tissue_press0 = $self->{$gas}->{pressure};
    #print "recalculating haldane formula. tissue pressure at t0 = $tissue_press0\n";

    # and the alveolar pressure
    my $alveolar = $self->_alveolarPressure( depth => $self->{depth} );

    # the time in minutes we have been at this depth, note that internal times are in seconds!
    return sub {
		my $t = shift;
		$alveolar + ($tissue_press0 - $alveolar ) * exp( -1 * $self->k() * $t );
    }
	
}

# return alvealor pressure for N2 (or other inert gas specified)
# see the Deco.pdf document for explanation on this calculation
sub _alveolarPressure {
    my $self = shift;
    my %opt  = @_;

    my $depth = $opt{depth} || 0;
    my $gas  = lc($opt{gas}) || 'n2';
    croak "Asking for unsupported gas $gas" unless exists $GASES{$gas};
    
    my $press = $self->{$gas}->{fraction} * ( $self->ambientpressure( $depth ) - WATER_VAPOR_PRESSURE +  ( ( 1 - $self->{RQ} ) / $self->{RQ} ) * CO2_PRESSURE );
    return $press;	
}


1;
__END__

=head1 NAME

Tissue - Models a Tissue for decompression calculations

=head1 SYNOPSIS

  use Deco::Tissue;
my $tissue = new Deco::Tissue( halftime => 5, m0 => 1.52);

=head1 DESCRIPTION

This module can be used to mimick the behaviour of a theoretical body tissue when Scuba Diving with air or nitrox. It will model a hypothetical body tissue in a Haldanian fashion. The 2 parameters that determine the tissue behaviour are the B<halftim...

=head2 METHODS

=over 4

=item new( halftime => 10, m0 => 1.234, .... )

Constructor of the class. You can create a tissue with specific parameters
Allowed parameters are:
halftime
m0
deltam
o2fraction
waterfactor
topsidepressure
offgasfactor
RQ
nr



( run in 0.617 second using v1.01-cache-2.11-cpan-df04353d9ac )