File Coverage

blib/lib/SCUBA/Blender.pm
Criterion Covered Total %
statement 9 163 5.5
branch 0 24 0.0
condition n/a
subroutine 3 19 15.7
pod 7 10 70.0
total 19 216 8.8


line stmt bran cond sub pod time code
1             #
2             # Author: Jaap Voets - Xperience Automatisering
3             # A van der Waals Gas Blender
4             #
5             #
6             package SCUBA::Blender;
7              
8 1     1   29072 use strict;
  1         2  
  1         106  
9 1     1   7 use Carp;
  1         2  
  1         217  
10              
11             our $VERSION = '0.1';
12              
13             # vanderwaals constants
14             my %GASES;
15             $GASES{'AIR'}{'a'} = 1.3725;
16             $GASES{'AIR'}{'b'} = 0.0372;
17             $GASES{'AIR'}{'weight'} = 28.84; # gram /mole
18             $GASES{'O2'}{'a'} = 1.382;
19             $GASES{'O2'}{'b'} = 0.0318;
20             $GASES{'O2'}{'weight'} = 32; # gram /mole
21             $GASES{'N2'}{'a'} = 1.37;
22             $GASES{'N2'}{'b'} = 0.0387;
23             $GASES{'N2'}{'weight'} = 28; # gram /mole
24             $GASES{'HE'}{'a'} = 0.0346;
25             $GASES{'HE'}{'b'} = 0.0238;
26             $GASES{'HE'}{'weight'} = 4;
27              
28             # Universal Gas Constant when using Bar and Liters
29 1     1   6 use constant R => 0.08314472;
  1         6  
  1         1958  
30              
31             sub new {
32 0     0 1   my $class = shift;
33 0           my $self = {};
34 0           bless($self, $class);
35            
36             # start with empty gas list
37 0           $self->{start}->{pressure} = 0; # bar
38 0           $self->{start}->{mix} = { '02' => 0.21, 'N2' => 0.79, 'HE' => 0, 'AIR' => 0}; # start with air
39 0           $self->{end}->{pressure} = 200; # bar
40 0           $self->{end}->{mix} = { '02' => 0.21, 'N2' => 0.79, 'HE' => 0, 'AIR' => 0}; # end with air by default
41 0           $self->{temperature} = 293; # Kelvin
42 0           $self->{volume} = 10; #liter
43 0           $self->{fillorder} = ['O2','HE','AIR'];
44            
45             # place holders for result of calculation
46 0           $self->{results} = {};
47              
48 0           return $self;
49             }
50              
51             # set temperature
52             sub temperature {
53 0     0 1   my $self = shift;
54 0           my $temp = shift;
55 0 0         if ($temp < 100) {
56             # sounds like celsius
57 0           $temp += 273; # make kelvin
58             }
59 0           $self->{temperature} = $temp;
60             }
61              
62             sub volume {
63 0     0 1   my $self = shift;
64 0           my $volume = shift;
65 0           $self->{volume} = $volume;
66             }
67              
68             # set starting values
69             # first param is pressure, rest is gas list with percentages
70             sub start_mix {
71 0     0 1   my $self = shift;
72 0           my $pressure = shift;
73 0           $self->{start}->{mix} = $self->_gasfractions( @_ );
74 0           $self->{start}->{pressure} = $pressure;
75             }
76              
77             # e.g.
78             # mixer->end_mix(210, 'O2' => 32, 'N2' => 68);
79             sub end_mix {
80 0     0 1   my $self = shift;
81 0           my $pressure = shift;
82 0           $self->{end}->{pressure} = $pressure;
83 0           $self->{end}->{mix} = $self->_gasfractions( @_ );
84             }
85              
86             # set the fill order of the gases
87             sub fill_order {
88 0     0 0   my $self = shift;
89 0           $self->{fillorder} = [];
90 0           my @gases = @_;
91 0           foreach my $gas (@gases) {
92 0           $gas = uc($gas);
93 0 0         if (exists $GASES{$gas}) {
94 0           push @{$self->{fillorder}}, $gas;
  0            
95             } else {
96 0           croak "Unknown gas $gas!";
97             }
98             }
99            
100             }
101              
102             # set fractions
103             sub _gasfractions {
104 0     0     my $self = shift;
105 0           my %gaslist = @_;
106            
107             # first make empty list based on %GASES
108 0           my %fractions;
109 0           foreach my $gas (keys %GASES) {
110 0           $fractions{$gas} = 0;
111             }
112            
113 0           my $totalfraction = 0;
114 0           foreach my $key (keys %gaslist) {
115 0           my $gas = uc($key);
116 0 0         if (exists $GASES{$gas} ) {
117 0           my $fraction = $gaslist{$key};
118 0 0         $fraction = $fraction / 100 if ($fraction > 1);
119 0           $fractions{$gas} = $fraction;
120 0           $totalfraction += $fraction;
121             } else {
122 0           croak "Unknown gas $key!";
123             }
124             }
125             # maybe they left out nitrogen, so complement the mix
126 0 0         if ($totalfraction < 1.0) {
127 0           $fractions{'N2'} = 1 - $fractions{'O2'} - $fractions{'HE'};
128             }
129            
130 0           return \%fractions;
131             }
132              
133             # determine new A & B depending on mix of gases
134             # note: this depends on the MOLAR fractions, not the partial pressure fractions
135             sub compositeAB {
136 0     0 0   my $self = shift;
137 0           my $gaslist_ref = shift;
138              
139 0           my $a = 0;
140 0           my $b = 0;
141 0           foreach my $gas1 ( keys %{ $gaslist_ref } ) {
  0            
142 0           foreach my $gas2 ( keys %{ $gaslist_ref } ) {
  0            
143 0           $a += sqrt( $GASES{$gas1}{a} * $GASES{$gas2}{a} ) * $gaslist_ref->{$gas1} * $gaslist_ref->{$gas2};
144 0           $b += sqrt( $GASES{$gas1}{b} * $GASES{$gas2}{b} ) * $gaslist_ref->{$gas1} * $gaslist_ref->{$gas2};
145             }
146             }
147 0           return ($a, $b);
148             }
149              
150              
151             # do the calculation
152             sub calc {
153 0     0 1   my $self = shift;
154            
155             # determine starting moles of each gas and the corresponding composite a and b values
156 0           my ($a_start, $b_start) = $self->compositeAB( $self->{start}->{mix} );
157             # and the initial molar amounts of each gas (hashref)
158 0           my $start_moles = _molesInMix( $self->{start}->{mix}, $self->{start}->{pressure}, $self->{temperature}, $self->{volume}, $a_start, $b_start);
159 0           _adjustforAir($start_moles);
160            
161             # same for the end mix
162 0           my ($a_end, $b_end) = $self->compositeAB( $self->{end}->{mix} );
163 0           my $end_moles = _molesInMix( $self->{end}->{mix}, $self->{end}->{pressure}, $self->{temperature}, $self->{volume}, $a_end, $b_end);
164 0           _adjustforAir($end_moles);
165              
166             # we now know how much to add for each gas
167 0           my %add_moles;
168 0           foreach my $gas (keys %GASES) {
169 0           $add_moles{$gas} = $end_moles->{$gas} - $start_moles->{$gas};
170             }
171              
172             # so let's see how we fill the tank in bars instead of moles
173 0           my $mix = $start_moles;
174            
175             # it's determined by the fillorder
176 0           my $previous_pressure = $self->{start}->{pressure};
177 0           foreach my $gas ( $self->_order_all() ) {
178            
179 0 0         if ($add_moles{$gas} != 0 ) {
180 0           $mix->{$gas} += $add_moles{$gas};
181            
182 0           $self->{results}->{$gas}->{mole_add} = $add_moles{$gas};
183 0           $self->{results}->{$gas}->{mole_total} = $mix->{$gas};
184 0           $self->{results}->{$gas}->{weight} = $mix->{$gas} * $GASES{$gas}{weight}; # in grams
185              
186             # get the new fractions of this fresh mix
187 0           my ($gas_fractions, $moles) = _recalc_fractions($mix);
188            
189             # then calculate the new a and b
190 0           my ($a, $b) = $self->compositeAB( $gas_fractions );
191            
192             # and get the pressure to fill to with these params
193             # (P + a*n^2/V^2)*(V - n*b) = n*R*T
194             # so P = (n*R*T) / (V - n*b) - a*n^2/V^2
195 0           my $P = ($moles * R * $self->{temperature}) / ( $self->{volume} - $moles * $b) - $a * _sqr($moles / $self->{volume});
196            
197 0           $self->{results}->{$gas}->{end_pressure} = $P;
198 0           $self->{results}->{$gas}->{pressure} = $P - $previous_pressure;
199            
200 0           $previous_pressure = $P;
201             }
202             }
203             }
204              
205             # make a human friendly string of the mix
206             sub mix_to_string {
207 0     0 0   my $self = shift;
208 0           my $mix_ref = shift;
209              
210 0           my $string = '';
211 0           $string .= int($mix_ref->{'O2'} * 100) . "% O2";
212 0 0         if ($mix_ref->{'HE'} > 0 ) {
213 0           $string .= ', ' . int($mix_ref->{'HE'} * 100) . "% He";
214             }
215            
216 0           return $string;
217             }
218              
219             # get all gases, but preserve the fill order
220             sub _order_all {
221 0     0     my $self = shift;
222 0           my @order = @{ $self->{fillorder} };
  0            
223 0           foreach my $gas ( keys %GASES ) {
224 0 0         if ( ! grep ( { /$gas/ } @order) ) {
  0            
225 0           push @order, $gas;
226             }
227             }
228 0           return @order;
229             }
230              
231             # make a nice report of the results
232             sub report {
233 0     0 1   my $self = shift;
234 0           my $report = '';
235              
236 0           my $desired_mix = $self->mix_to_string( $self->{end}->{mix} );
237 0           my $start_mix = $self->mix_to_string( $self->{start}->{mix} );
238 0           $report .= 'To fill a tank of ' . $self->{volume} . " liters with $desired_mix to a pressure of " . $self->{end}->{pressure} . " bar\n";
239 0           $report .= "starting with $start_mix at a pressure of " . $self->{start}->{pressure} . " bar\n\n";
240              
241 0           my $total_weight = 0;
242 0           foreach my $gas ( $self->_order_all() ) {
243 0 0         if (exists $self->{results}->{$gas} ) {
244 0           $report .= "add $gas to " . int($self->{results}->{$gas}->{end_pressure}) . " bar";
245 0           $report .= " (adding " . int($self->{results}->{$gas}->{pressure}) . " bars)\n";
246 0           $total_weight += $self->{results}->{$gas}->{weight};
247             }
248             }
249              
250 0           $report .= "\ntotal weight of mix is " . int($total_weight) . " grams\n";
251 0           return $report;
252             }
253              
254             # get a mix of moles
255             # and return the same mix as fractions
256             sub _recalc_fractions {
257 0     0     my $mix = shift;
258 0           my $frac = {};
259 0           my $total_moles = 0;
260              
261 0           foreach my $gas ( %{ $mix } ) {
  0            
262 0           $total_moles += $mix->{$gas};
263             }
264            
265 0 0         if ($total_moles > 0) {
266 0           foreach my $gas ( %{ $mix } ) {
  0            
267 0           $frac->{$gas} = $mix->{$gas} / $total_moles;
268             }
269             }
270 0           return ($frac, $total_moles);
271             }
272              
273             # solve the vanderwaals equation for n
274             # (P + a*n^2/V^2)*(V - n*b) = n*R*T
275             # for a mix with a composite a and b
276             # this will give the TOTAL number of moles of the gas
277             #
278             # we will be using a newton iteration
279             sub _molesInMix {
280 0     0     my ($mix_ref, $P, $T, $V, $a, $b) = @_;
281              
282 0           my $n = 1;
283 0           my $success = 0;
284              
285 0           my $max_iteration = 100;
286 0           my $epsilon = 0.0001;
287             # function to solve
288 0           for (my $i = 1; $i < $max_iteration; $i++) {
289 0           my $function_val = ( $P + $a * _sqr($n / $V)) * ( $V - $n*$b) - $n * R * $T;
290             # dF/dn = -b*P + 2a*n/V - 3ab * n^2/V^2 -RT
291 0           my $funcderiv_val = -1 * $P * $b + 2 * $a * $n/$V -3 * $a * $b * _sqr($n/$V) - R * $T;
292             # calculate new n
293 0           $n = $n - $function_val/$funcderiv_val;
294 0 0         if ( abs( $function_val ) < $epsilon ) {
295             # we are close enough to the root
296 0           $success = 1;
297 0           last;
298             }
299             }
300 0 0         if ($success) {
301 0           my $moles_mix = {};
302 0           foreach my $gas (keys %{ $mix_ref } ) {
  0            
303 0           $moles_mix->{$gas} = $mix_ref->{$gas} * $n;
304             }
305 0           return $moles_mix;
306             } else {
307 0           croak "Couldn't solve";
308             }
309            
310             }
311              
312             # we do not fill with pure nitrogen
313             # but with air
314             # so correct both the nitrogen and oxygen parts
315             sub _adjustforAir {
316 0     0     my $mix_ref = shift;
317             # the nitrogen comes from the air, so we know how much air we need
318 0           $mix_ref->{'AIR'} = $mix_ref->{'N2'} / 0.79;
319 0           $mix_ref->{'O2'} = $mix_ref->{'O2'} - 0.21 * $mix_ref->{'AIR'};
320             # now clear the pure nitrogen part
321 0           $mix_ref->{'N2'} = 0;
322             }
323              
324             # square a number
325             sub _sqr {
326 0     0     my $n = shift;
327 0           return ($n * $n);
328             }
329              
330             1;
331             __END__