forked from bwaite/vpmb
-
Notifications
You must be signed in to change notification settings - Fork 0
/
vpmb.py
executable file
·2573 lines (1986 loc) · 123 KB
/
vpmb.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
# Copyright 2010, Bryan Waite, Erik C. Baker. All rights reserved.
# Redistribution and use in source and binary forms, with or without modification, are
# permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice, this list of
# conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice, this list
# of conditions and the following disclaimer in the documentation and/or other materials
# provided with the distribution.
# THIS SOFTWARE IS PROVIDED BY Bryan Waite, Erik C. Baker ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Bryan Waite, or Erik C. Baker OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# The views and conclusions contained in the software and documentation are those of the
# authors and should not be interpreted as representing official policies, either expressed
# or implied, of Bryan Waite, or Erik C. Baker.
# import pycallgraph
import json
from math import log, trunc, exp, sqrt
import datetime
import argparse
COMPARTMENTS = 16
class AltitudeException(Exception):
"""Thrown when altitude is invalid, or diver acclimatized is invalid."""
def __init__(self, value):
self.value = value
super(AltitudeException, self).__init__()
def __str__(self):
return repr(self.value)
class MaxIterationException(Exception):
"""Thrown when root finding fails."""
def __init__(self, value):
self.value = value
super(MaxIterationException, self).__init__()
def __str__(self):
return repr(self.value)
class InputFileException(Exception):
"""Thrown when there are errors with the input file values."""
def __init__(self, value):
self.value = value
super(InputFileException, self).__init__()
def __str__(self):
return repr(self.value)
class DecompressionStepException(Exception):
"""Thrown when the decompression step is too large."""
def __init__(self, value):
self.value = value
super(DecompressionStepException, self).__init__()
def __str__(self):
return repr(self.value)
class RootException(Exception):
"""Thrown when root calculated is not within brackets"""
def __init__(self, value):
self.value = value
super(RootException, self).__init__()
def __str__(self):
return repr(self.value)
class OffGassingException(Exception):
"""Thrown when Off Gassing gradient is too small"""
def __init__(self, value):
self.value = value
super(OffGassingException, self).__init__()
def __str__(self):
return repr(self.value)
class DiveState(object):
"""Contains the program state so that this isn't a huge mess"""
def __init__(self, input_file_name=None, json_input=None):
"""Take the input_file_name parsed by parse_settings or raw json data
and use it to initialize the program state"""
if input_file_name is None and json_input is None:
raise ValueError("""DiveState must be given a file name (to load the input from), or
raw json data""")
if json_input:
data = json_input
else:
# load the file data
with open(input_file_name) as input_file:
data = json.loads(input_file.read())
self.input_values = data["input"]
self.settings_values = data["settings"]
self.altitude_values = data["altitude"]
self.output_object = HtmlOutput(self)
# init the instance variables
# strings
# self.Word = ""
self.Units = ""
self.Units_Word1 = ""
self.Units_Word2 = ""
# integers
# self.Number_of_Mixes = 0
self.Number_of_Changes = 0
self.Segment_Number_Start_of_Ascent = 0
self.Repetitive_Dive_Flag = 0
# bools
self.Schedule_Converged = False
self.Critical_Volume_Algorithm_Off = False
self.Altitude_Dive_Algorithm_Off = False
# floats
self.Ascent_Ceiling_Depth = 0.0
self.Deco_Stop_Depth = 0.0
self.Step_Size = 0.0
self.Sum_Check = 0.0
self.Depth = 0.0
self.Ending_Depth = 0.0
self.Starting_Depth = 0.0
self.Rate = 0.0
self.Run_Time_End_of_Segment = 0.0
self.Last_Run_Time = 0.0
self.Stop_Time = 0.0
self.Depth_Start_of_Deco_Zone = 0.0
self.Deepest_Possible_Stop_Depth = 0.0
self.First_Stop_Depth = 0.0
self.Critical_Volume_Comparison = 0.0
self.Next_Stop = 0.0
self.Run_Time_Start_of_Deco_Zone = 0.0
self.Critical_Radius_N2_Microns = 0.0
self.Critical_Radius_He_Microns = 0.0
self.Run_Time_Start_of_Ascent = 0.0
self.Altitude_of_Dive = 0.0
self.Deco_Phase_Volume_Time = 0.0
self.Regenerated_Radius_He = [0.0] * COMPARTMENTS
self.Regenerated_Radius_N2 = [0.0] * COMPARTMENTS
# Global Arrays
self.Mix_Change = []
self.Depth_Change = []
self.Rate_Change = []
self.Step_Size_Change = []
self.He_Pressure_Start_of_Ascent = [0.0] * COMPARTMENTS
self.N2_Pressure_Start_of_Ascent = [0.0] * COMPARTMENTS
self.He_Pressure_Start_of_Deco_Zone = [0.0] * COMPARTMENTS
self.N2_Pressure_Start_of_Deco_Zone = [0.0] * COMPARTMENTS
self.Phase_Volume_Time = [0.0] * COMPARTMENTS
self.Last_Phase_Volume_Time = [0.0] * COMPARTMENTS
self.Allowable_Gradient_He = [0.0] * COMPARTMENTS
self.Allowable_Gradient_N2 = [0.0] * COMPARTMENTS
self.Adjusted_Crushing_Pressure_He = [0.0] * COMPARTMENTS
self.Adjusted_Crushing_Pressure_N2 = [0.0] * COMPARTMENTS
self.Initial_Allowable_Gradient_N2 = [0.0] * COMPARTMENTS
self.Initial_Allowable_Gradient_He = [0.0] * COMPARTMENTS
self.Deco_Gradient_He = [0.0] * COMPARTMENTS
self.Deco_Gradient_N2 = [0.0] * COMPARTMENTS
# GLOBAL CONSTANTS
self.Water_Vapor_Pressure = 0.0
self.Surface_Tension_Gamma = 0.0
self.Skin_Compression_GammaC = 0.0
self.Crit_Volume_Parameter_Lambda = 0.0
self.Minimum_Deco_Stop_Time = 0.0
self.Regeneration_Time_Constant = 0.0
self.Constant_Pressure_Other_Gases = 0.0
self.Gradient_Onset_of_Imperm_Atm = 0.0
self.ATM = 101325.0 # 1 atm of pressure
self.fraction_inert_gas = 0.79 # oxygen = 21% of air so this is what's left over
# GLOBAL VARIABLES
self.Segment_Number = 0
self.Run_Time = 0.0
self.Segment_Time = 0.0
self.Ending_Ambient_Pressure = 0.0
self.Mix_Number = 0
self.Barometric_Pressure = 0.0
self.units_fsw = False
self.Units_Factor = 0.0
# GLOBAL ARRAYS
# Float
self.Helium_Time_Constant = [0.0] * COMPARTMENTS
self.Nitrogen_Time_Constant = [0.0] * COMPARTMENTS
self.Helium_Pressure = [0.0] * COMPARTMENTS
self.Nitrogen_Pressure = [0.0] * COMPARTMENTS
self.Initial_Helium_Pressure = [0.0] * COMPARTMENTS
self.Initial_Nitrogen_Pressure = [0.0] * COMPARTMENTS
# self.Fraction_Oxygen = []
self.Fraction_Helium = []
self.Fraction_Nitrogen = []
self.Initial_Critical_Radius_He = [0.0] * COMPARTMENTS
self.Initial_Critical_Radius_N2 = [0.0] * COMPARTMENTS
self.Adjusted_Critical_Radius_He = [0.0] * COMPARTMENTS
self.Adjusted_Critical_Radius_N2 = [0.0] * COMPARTMENTS
self.Max_Crushing_Pressure_He = [0.0] * COMPARTMENTS
self.Max_Crushing_Pressure_N2 = [0.0] * COMPARTMENTS
self.Surface_Phase_Volume_Time = [0.0] * COMPARTMENTS
self.Max_Actual_Gradient = [0.0] * COMPARTMENTS
self.Amb_Pressure_Onset_of_Imperm = [0.0] * COMPARTMENTS
self.Gas_Tension_Onset_of_Imperm = [0.0] * COMPARTMENTS
self.Diver_Acclimatized = None
# ASSIGN HALF-TIME VALUES TO BUHLMANN COMPARTMENT ARRAYS
self.Helium_Half_Time = [1.88, 3.02, 4.72, 6.99, 10.21, 14.48, 20.53, 29.11, 41.20,
55.19, 70.69, 90.34, 115.29, 147.42, 188.24, 240.03]
self.Nitrogen_Half_Time = [5.0, 8.0, 12.5, 18.5, 27.0, 38.3, 54.3, 77.0, 109.0,
146.0, 187.0, 239.0, 305.0, 390.0, 498.0, 635.0]
def get_json(self):
return self.output_object.get_json()
def gas_loadings_surface_interval(self, surface_interval_time):
"""
Purpose: This subprogram calculates the gas loading (off-gassing) during
a surface interval.
Side Effects: Sets
`self.Helium_Pressure`,
`self.Nitrogen_Pressure`
Returns: None
"""
inspired_helium_pressure = 0.0
inspired_nitrogen_pressure = (self.Barometric_Pressure - self.Water_Vapor_Pressure) * self.fraction_inert_gas
for i in range(COMPARTMENTS):
temp_helium_pressure = self.Helium_Pressure[i]
temp_nitrogen_pressure = self.Nitrogen_Pressure[i]
self.Helium_Pressure[i] = haldane_equation(temp_helium_pressure, inspired_helium_pressure, self.Helium_Time_Constant[i], surface_interval_time)
self.Nitrogen_Pressure[i] = haldane_equation(temp_nitrogen_pressure, inspired_nitrogen_pressure, self.Nitrogen_Time_Constant[i], surface_interval_time)
def _new_critical_radius(self, max_actual_gradient_pascals, adj_crush_pressure_pascals):
"""Calculates the new radius for the `VPM_REPETITIVE_ALGORITHM`
Side Effects: None
Returns: A floating point value
"""
return ((2.0 * self.Surface_Tension_Gamma * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma))) / (max_actual_gradient_pascals * self.Skin_Compression_GammaC - self.Surface_Tension_Gamma * adj_crush_pressure_pascals)
def vpm_repetitive_algorithm(self, surface_interval_time):
"""
Purpose: This subprogram implements the VPM Repetitive Algorithm that was
envisioned by Professor David E. Yount only months before his passing.
Side Effects: Sets
`self.Adjusted_Critical_Radius_He`,
`self.Adjusted_Critical_Radius_N2`
Returns: None
"""
for i in range(COMPARTMENTS):
max_actual_gradient_pascals = (self.Max_Actual_Gradient[i] / self.Units_Factor) * self.ATM
adj_crush_pressure_he_pascals = (self.Adjusted_Crushing_Pressure_He[i] / self.Units_Factor) * self.ATM
adj_crush_pressure_n2_pascals = (self.Adjusted_Crushing_Pressure_N2[i] / self.Units_Factor) * self.ATM
if self.Max_Actual_Gradient[i] > self.Initial_Allowable_Gradient_N2[i]:
new_critical_radius_n2 = self._new_critical_radius(max_actual_gradient_pascals, adj_crush_pressure_n2_pascals)
self.Adjusted_Critical_Radius_N2[i] = self.Initial_Critical_Radius_N2[i] + (self.Initial_Critical_Radius_N2[i] - new_critical_radius_n2) * exp(-surface_interval_time / self.Regeneration_Time_Constant)
else:
self.Adjusted_Critical_Radius_N2[i] = self.Initial_Critical_Radius_N2[i]
if self.Max_Actual_Gradient[i] > self.Initial_Allowable_Gradient_He[i]:
new_critical_radius_he = self._new_critical_radius(max_actual_gradient_pascals, adj_crush_pressure_he_pascals)
self.Adjusted_Critical_Radius_He[i] = self.Initial_Critical_Radius_He[i] + (self.Initial_Critical_Radius_He[i] - new_critical_radius_he) * exp(-surface_interval_time / self.Regeneration_Time_Constant)
else:
self.Adjusted_Critical_Radius_He[i] = self.Initial_Critical_Radius_He[i]
def calc_max_actual_gradient(self, deco_stop_depth):
"""
Purpose: This subprogram calculates the actual supersaturation gradient
obtained in each compartment as a result of the ascent profile during
decompression. Similar to the concept with crushing pressure, the
supersaturation gradients are not cumulative over a multi-level, staged
ascent. Rather, it will be the maximum value obtained in any one discrete
step of the overall ascent. Thus, the program must compute and store the
maximum actual gradient for each compartment that was obtained across all
steps of the ascent profile. This subroutine is invoked on the last pass
through the deco stop loop block when the final deco schedule is being
generated.
The max actual gradients are later used by the VPM Repetitive Algorithm to
determine if adjustments to the critical radii are required. If the max
actual gradient did not exceed the initial allowable gradient, then no
adjustment will be made. However, if the max actual gradient did exceed
the initial allowable gradient, such as permitted by the Critical Volume
Algorithm, then the critical radius will be adjusted (made larger) on the
repetitive dive to compensate for the bubbling that was allowed on the
previous dive. The use of the max actual gradients is intended to prevent
the repetitive algorithm from being overly conservative.
Side Effects: Sets
`self.Max_Actual_Gradient`
Returns: None
"""
# Note: negative supersaturation gradients are meaningless for this
# application, so the values must be equal to or greater than zero.
for i in range(COMPARTMENTS):
compartment_gradient = (self.Helium_Pressure[i] + self.Nitrogen_Pressure[i] + self.Constant_Pressure_Other_Gases) - (deco_stop_depth + self.Barometric_Pressure)
if compartment_gradient <= 0.0:
compartment_gradient = 0.0
self.Max_Actual_Gradient[i] = max(self.Max_Actual_Gradient[i], compartment_gradient)
def vpm_altitude_dive_algorithm(self, altitude_settings):
"""
Purpose: This subprogram updates gas loadings and adjusts critical radii
(as required) based on whether or not diver is acclimatized at altitude or
makes an ascent to altitude before the dive.
Side Effects: Sets
`self.Adjusted_Critical_Radius_He`,
`self.Adjusted_Critical_Radius_N2`,
`self.Barometric_Pressure`,
`self.Helium_Pressure`,
`self.Initial_Critical_Radius_He`,
`self.Initial_Critical_Radius_N2`
`self.Nitrogen_Pressure`,
or
Raises an AltitudeException
Returns: None
"""
ascent_to_altitude_time = altitude_settings['Ascent_to_Altitude_Hours'] * 60.0
time_at_altitude_before_dive = altitude_settings['Hours_at_Altitude_Before_Dive'] * 60.0
if self.Diver_Acclimatized:
self.Barometric_Pressure = calc_barometric_pressure(altitude_settings['Altitude_of_Dive'], self.units_fsw)
for i in range(COMPARTMENTS):
self.Adjusted_Critical_Radius_N2[i] = self.Initial_Critical_Radius_N2[i]
self.Adjusted_Critical_Radius_He[i] = self.Initial_Critical_Radius_He[i]
self.Helium_Pressure[i] = 0.0
self.Nitrogen_Pressure[i] = (self.Barometric_Pressure - self.Water_Vapor_Pressure) * self.fraction_inert_gas
else:
if (altitude_settings['Starting_Acclimatized_Altitude'] >= altitude_settings['Altitude_of_Dive']) or (altitude_settings['Starting_Acclimatized_Altitude'] < 0.0):
raise AltitudeException("ERROR! STARTING ACCLIMATIZED ALTITUDE MUST BE LESS THAN ALTITUDE OF DIVE AND GREATER THAN OR EQUAL TO ZERO")
self.Barometric_Pressure = calc_barometric_pressure(altitude_settings['Starting_Acclimatized_Altitude'], self.units_fsw)
starting_ambient_pressure = self.Barometric_Pressure
for i in range(COMPARTMENTS):
self.Helium_Pressure[i] = 0.0
self.Nitrogen_Pressure[i] = (self.Barometric_Pressure - self.Water_Vapor_Pressure) * self.fraction_inert_gas
self.Barometric_Pressure = calc_barometric_pressure(altitude_settings['Altitude_of_Dive'], self.units_fsw)
ending_ambient_pressure = self.Barometric_Pressure
initial_inspired_n2_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.fraction_inert_gas
rate = (ending_ambient_pressure - starting_ambient_pressure) / ascent_to_altitude_time
nitrogen_rate = rate * self.fraction_inert_gas
for i in range(COMPARTMENTS):
initial_nitrogen_pressure = self.Nitrogen_Pressure[i]
self.Nitrogen_Pressure[i] = schreiner_equation(initial_inspired_n2_pressure, nitrogen_rate, ascent_to_altitude_time, self.Nitrogen_Time_Constant[i], initial_nitrogen_pressure)
compartment_gradient = (self.Nitrogen_Pressure[i] + self.Constant_Pressure_Other_Gases) - ending_ambient_pressure
compartment_gradient_pascals = (compartment_gradient / self.Units_Factor) * self.ATM
gradient_he_bubble_formation = ((2.0 * self.Surface_Tension_Gamma * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma)) / (self.Initial_Critical_Radius_He[i] * self.Skin_Compression_GammaC))
if compartment_gradient_pascals > gradient_he_bubble_formation:
new_critical_radius_he = ((2.0 * self.Surface_Tension_Gamma * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma))) / (compartment_gradient_pascals * self.Skin_Compression_GammaC)
self.Adjusted_Critical_Radius_He[i] = self.Initial_Critical_Radius_He[i] + (self.Initial_Critical_Radius_He[i] - new_critical_radius_he) * exp(-time_at_altitude_before_dive / self.Regeneration_Time_Constant)
self.Initial_Critical_Radius_He[i] = self.Adjusted_Critical_Radius_He[i]
else:
ending_radius_he = 1.0 / (compartment_gradient_pascals / (2.0 * (self.Surface_Tension_Gamma - self.Skin_Compression_GammaC)) + 1.0 / self.Initial_Critical_Radius_He[i])
regenerated_radius_he = self.Initial_Critical_Radius_He[i] + (ending_radius_he - self.Initial_Critical_Radius_He[i]) * exp(-time_at_altitude_before_dive / self.Regeneration_Time_Constant)
self.Initial_Critical_Radius_He[i] = regenerated_radius_he
self.Adjusted_Critical_Radius_He[i] = self.Initial_Critical_Radius_He[i]
gradient_n2_bubble_formation = ((2.0 * self.Surface_Tension_Gamma * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma)) / (self.Initial_Critical_Radius_N2[i] * self.Skin_Compression_GammaC))
if compartment_gradient_pascals > gradient_n2_bubble_formation:
new_critical_radius_n2 = ((2.0 * self.Surface_Tension_Gamma * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma))) / (compartment_gradient_pascals * self.Skin_Compression_GammaC)
self.Adjusted_Critical_Radius_N2[i] = self.Initial_Critical_Radius_N2[i] + (self.Initial_Critical_Radius_N2[i] - new_critical_radius_n2) * exp(-time_at_altitude_before_dive / self.Regeneration_Time_Constant)
self.Initial_Critical_Radius_N2[i] = self.Adjusted_Critical_Radius_N2[i]
else:
ending_radius_n2 = 1.0 / (compartment_gradient_pascals / (2.0 * (self.Surface_Tension_Gamma - self.Skin_Compression_GammaC)) + 1.0 / self.Initial_Critical_Radius_N2[i])
regenerated_radius_n2 = self.Initial_Critical_Radius_N2[i] + (ending_radius_n2 - self.Initial_Critical_Radius_N2[i]) * exp(-time_at_altitude_before_dive / self.Regeneration_Time_Constant)
self.Initial_Critical_Radius_N2[i] = regenerated_radius_n2
self.Adjusted_Critical_Radius_N2[i] = self.Initial_Critical_Radius_N2[i]
inspired_nitrogen_pressure = (self.Barometric_Pressure - self.Water_Vapor_Pressure) * self.fraction_inert_gas
for i in range(COMPARTMENTS):
initial_nitrogen_pressure = self.Nitrogen_Pressure[i]
self.Nitrogen_Pressure[i] = haldane_equation(initial_nitrogen_pressure, inspired_nitrogen_pressure, self.Nitrogen_Time_Constant[i], time_at_altitude_before_dive)
def gas_loadings_ascent_descent(self, starting_depth, ending_depth, rate):
"""
Purpose: This subprogram applies the Schreiner equation to update the
gas loadings (partial pressures of helium and nitrogen) in the half-time
compartments due to a linear ascent or descent segment at a constant rate.
Side Effects: Sets `self.Segment_Time`,
`self.Ending_Ambient_Pressure`,
`self.Helium_Pressure`,
`self.Initial_Helium_Pressure`,
`self.Initial_Nitrogen_Pressure`,
`self.Nitrogen_Pressure`
`self.Run_Time`,
`self.Segment_Number`,
Returns: None
"""
self.Segment_Time = float(ending_depth - starting_depth) / rate
self.Run_Time += self.Segment_Time
self.Segment_Number += 1
self.Ending_Ambient_Pressure = ending_depth + self.Barometric_Pressure
starting_ambient_pressure = starting_depth + self.Barometric_Pressure
initial_inspired_he_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Helium[self.Mix_Number - 1]
initial_inspired_n2_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Nitrogen[self.Mix_Number - 1]
helium_rate = rate * self.Fraction_Helium[self.Mix_Number - 1]
nitrogen_rate = rate * self.Fraction_Nitrogen[self.Mix_Number - 1]
for i in range(COMPARTMENTS):
self.Initial_Helium_Pressure[i] = self.Helium_Pressure[i]
self.Initial_Nitrogen_Pressure[i] = self.Nitrogen_Pressure[i]
self.Helium_Pressure[i] = schreiner_equation(initial_inspired_he_pressure, helium_rate, self.Segment_Time, self.Helium_Time_Constant[i], self.Initial_Helium_Pressure[i])
self.Nitrogen_Pressure[i] = schreiner_equation(initial_inspired_n2_pressure, nitrogen_rate, self.Segment_Time, self.Nitrogen_Time_Constant[i], self.Initial_Nitrogen_Pressure[i])
def _crushing_pressure_helper(self, radius_onset_of_imperm_molecule, ending_ambient_pressure_pa, amb_press_onset_of_imperm_pa, gas_tension_onset_of_imperm_pa, gradient_onset_of_imperm_pa):
"""Calculate the crushing pressure for a molecule(He or N2) (a helper for CALC_CRUSHING_PRESSURE)
Side Effects: None
Returns: A floating point value
"""
A = ending_ambient_pressure_pa - amb_press_onset_of_imperm_pa + gas_tension_onset_of_imperm_pa + (2.0 * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma)) / radius_onset_of_imperm_molecule
B = 2.0 * (self.Skin_Compression_GammaC - self.Surface_Tension_Gamma)
C = gas_tension_onset_of_imperm_pa * radius_onset_of_imperm_molecule ** 3
high_bound = radius_onset_of_imperm_molecule
low_bound = B / A
ending_radius = radius_root_finder(A, B, C, low_bound, high_bound)
crushing_pressure_pascals = gradient_onset_of_imperm_pa + ending_ambient_pressure_pa - amb_press_onset_of_imperm_pa + gas_tension_onset_of_imperm_pa * (1.0 - radius_onset_of_imperm_molecule ** 3 / ending_radius ** 3)
return (crushing_pressure_pascals / self.ATM) * self.Units_Factor
def calc_crushing_pressure(self, starting_depth, ending_depth, rate):
"""
Purpose: Compute the effective "crushing pressure" in each compartment as
a result of descent segment(s). The crushing pressure is the gradient
(difference in pressure) between the outside ambient pressure and the
gas tension inside a VPM nucleus (bubble seed). This gradient acts to
reduce (shrink) the radius smaller than its initial value at the surface.
This phenomenon has important ramifications because the smaller the radius
of a VPM nucleus, the greater the allowable supersaturation gradient upon
ascent. Gas loading (uptake) during descent, especially in the fast
compartments, will reduce the magnitude of the crushing pressure. The
crushing pressure is not cumulative over a multi-level descent. It will
be the maximum value obtained in any one discrete segment of the overall
descent. Thus, the program must compute and store the maximum crushing
pressure for each compartment that was obtained across all segments of
the descent profile.
The calculation of crushing pressure will be different depending on
whether or not the gradient is in the VPM permeable range (gas can diffuse
across skin of VPM nucleus) or the VPM impermeable range (molecules in
skin of nucleus are squeezed together so tight that gas can no longer
diffuse in or out of nucleus; the gas becomes trapped and further resists
the crushing pressure). The solution for crushing pressure in the VPM
permeable range is a simple linear equation. In the VPM impermeable
range, a cubic equation must be solved using a numerical method.
Separate crushing pressures are tracked for helium and nitrogen because
they can have different critical radii. The crushing pressures will be
the same for helium and nitrogen in the permeable range of the model, but
they will start to diverge in the impermeable range. This is due to
the differences between starting radius, radius at the onset of
impermeability, and radial compression in the impermeable range.
Side Effects: Sets
`self.Max_Crushing_Pressure_He`,
`self.Max_Crushing_Pressure_N2`
Returns: None
"""
# First, convert the Gradient for Onset of Impermeability from units of
# atmospheres to diving pressure units (either fsw or msw) and to Pascals
# (SI units). The reason that the Gradient for Onset of Impermeability is
# given in the program settings in units of atmospheres is because that is
# how it was reported in the original research papers by Yount and
# colleagues.
gradient_onset_of_imperm = self.settings_values['Gradient_Onset_of_Imperm_Atm'] * self.Units_Factor # convert to diving units
gradient_onset_of_imperm_pa = self.settings_values['Gradient_Onset_of_Imperm_Atm'] * self.ATM # convert to Pascals
# Assign values of starting and ending ambient pressures for descent segment
starting_ambient_pressure = starting_depth + self.Barometric_Pressure
ending_ambient_pressure = ending_depth + self.Barometric_Pressure
# MAIN LOOP WITH NESTED DECISION TREE
# For each compartment, the program computes the starting and ending
# gas tensions and gradients. The VPM is different than some dissolved gas
# algorithms, Buhlmann for example, in that it considers the pressure due to
# oxygen, carbon dioxide, and water vapor in each compartment in addition to
# the inert gases helium and nitrogen. These "other gases" are included in
# the calculation of gas tensions and gradients.
for i in range(COMPARTMENTS):
starting_gas_tension = self.Initial_Helium_Pressure[i] + self.Initial_Nitrogen_Pressure[i] + self.Constant_Pressure_Other_Gases
starting_gradient = starting_ambient_pressure - starting_gas_tension
ending_gas_tension = self.Helium_Pressure[i] + self.Nitrogen_Pressure[i] + self.Constant_Pressure_Other_Gases
ending_gradient = ending_ambient_pressure - ending_gas_tension
# Compute radius at onset of impermeability for helium and nitrogen
# critical radii
radius_onset_of_imperm_he = 1.0 / (gradient_onset_of_imperm_pa / (2.0 * (self.settings_values['Skin_Compression_GammaC'] - self.settings_values['Surface_Tension_Gamma'])) + 1.0 / self.Adjusted_Critical_Radius_He[i])
radius_onset_of_imperm_n2 = 1.0 / (gradient_onset_of_imperm_pa / (2.0 * (self.settings_values['Skin_Compression_GammaC'] - self.settings_values['Surface_Tension_Gamma'])) + 1.0 / self.Adjusted_Critical_Radius_N2[i])
# FIRST BRANCH OF DECISION TREE - PERMEABLE RANGE
# Crushing pressures will be the same for helium and nitrogen
if ending_gradient <= gradient_onset_of_imperm:
crushing_pressure_he = ending_ambient_pressure - ending_gas_tension
crushing_pressure_n2 = ending_ambient_pressure - ending_gas_tension
# SECOND BRANCH OF DECISION TREE - IMPERMEABLE RANGE
# Both the ambient pressure and the gas tension at the onset of
# impermeability must be computed in order to properly solve for the ending
# radius and resultant crushing pressure. The first decision block
# addresses the special case when the starting gradient just happens to be
# equal to the gradient for onset of impermeability (not very likely!).
# if ending_gradient > gradient_onset_of_imperm:
else:
if starting_gradient == gradient_onset_of_imperm:
self.Amb_Pressure_Onset_of_Imperm[i] = starting_ambient_pressure
self.Gas_Tension_Onset_of_Imperm[i] = starting_gas_tension
# In most cases, a subroutine will be called to find these values using a
# numerical method.
if starting_gradient < gradient_onset_of_imperm:
self.onset_of_impermeability(starting_ambient_pressure, ending_ambient_pressure, rate, i)
# Next, using the values for ambient pressure and gas tension at the onset
# of impermeability, the equations are set up to process the calculations
# through the radius root finder subroutine. This subprogram will find the
# root (solution) to the cubic equation using a numerical method. In order
# to do this efficiently, the equations are placed in the form
# Ar^3 - Br^2 - C = 0, where r is the ending radius after impermeable
# compression. The coefficients A, B, and C for helium and nitrogen are
# computed and passed to the subroutine as arguments. The high and low
# bounds to be used by the numerical method of the subroutine are also
# computed (see separate page posted on Deco List ftp site entitled
# "VPM: Solving for radius in the impermeable regime"). The subprogram
# will return the value of the ending radius and then the crushing
# pressures for helium and nitrogen can be calculated.
ending_ambient_pressure_pa = (ending_ambient_pressure / self.Units_Factor) * self.ATM
amb_press_onset_of_imperm_pa = (self.Amb_Pressure_Onset_of_Imperm[i] / self.Units_Factor) * self.ATM
gas_tension_onset_of_imperm_pa = (self.Gas_Tension_Onset_of_Imperm[i] / self.Units_Factor) * self.ATM
crushing_pressure_he = self._crushing_pressure_helper(radius_onset_of_imperm_he, ending_ambient_pressure_pa, amb_press_onset_of_imperm_pa, gas_tension_onset_of_imperm_pa, gradient_onset_of_imperm_pa)
crushing_pressure_n2 = self._crushing_pressure_helper(radius_onset_of_imperm_n2, ending_ambient_pressure_pa, amb_press_onset_of_imperm_pa, gas_tension_onset_of_imperm_pa, gradient_onset_of_imperm_pa)
# UPDATE VALUES OF MAX CRUSHING PRESSURE IN Object ARRAYS
self.Max_Crushing_Pressure_He[i] = max(self.Max_Crushing_Pressure_He[i], crushing_pressure_he)
self.Max_Crushing_Pressure_N2[i] = max(self.Max_Crushing_Pressure_N2[i], crushing_pressure_n2)
def onset_of_impermeability(self, starting_ambient_pressure, ending_ambient_pressure, rate, i):
"""
Purpose: This subroutine uses the Bisection Method to find the ambient
pressure and gas tension at the onset of impermeability for a given
compartment. Source: "Numerical Recipes in Fortran 77",
Cambridge University Press, 1992.
Side Effects: Sets
`self.Amb_Pressure_Onset_of_Imperm`,
`self.Gas_Tension_Onset_of_Imperm`
or
Raises a RootException
Returns: None
"""
# First convert the Gradient for Onset of Impermeability to the diving
# pressure units that are being used
gradient_onset_of_imperm = self.Gradient_Onset_of_Imperm_Atm * self.Units_Factor
# ESTABLISH THE BOUNDS FOR THE ROOT SEARCH USING THE BISECTION METHOD
# In this case, we are solving for time - the time when the ambient pressure
# minus the gas tension will be equal to the Gradient for Onset of
# Impermeability. The low bound for time is set at zero and the high
# bound is set at the elapsed time (segment time) it took to go from the
# starting ambient pressure to the ending ambient pressure. The desired
# ambient pressure and gas tension at the onset of impermeability will
# be found somewhere between these endpoints. The algorithm checks to
# make sure that the solution lies in between these bounds by first
# computing the low bound and high bound function values.
initial_inspired_he_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Helium[self.Mix_Number - 1]
initial_inspired_n2_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Nitrogen[self.Mix_Number - 1]
helium_rate = rate * self.Fraction_Helium[self.Mix_Number - 1]
nitrogen_rate = rate * self.Fraction_Nitrogen[self.Mix_Number - 1]
low_bound = 0.0
high_bound = (ending_ambient_pressure - starting_ambient_pressure) / rate
starting_gas_tension = self.Initial_Helium_Pressure[i] + self.Initial_Nitrogen_Pressure[i] + self.Constant_Pressure_Other_Gases
function_at_low_bound = starting_ambient_pressure - starting_gas_tension - gradient_onset_of_imperm
high_bound_helium_pressure = schreiner_equation(initial_inspired_he_pressure, helium_rate, high_bound, self.Helium_Time_Constant[i], self.Initial_Helium_Pressure[i])
high_bound_nitrogen_pressure = schreiner_equation(initial_inspired_n2_pressure, nitrogen_rate, high_bound, self.Nitrogen_Time_Constant[i], self.Initial_Nitrogen_Pressure[i])
ending_gas_tension = high_bound_helium_pressure + high_bound_nitrogen_pressure + self.Constant_Pressure_Other_Gases
function_at_high_bound = ending_ambient_pressure - ending_gas_tension - gradient_onset_of_imperm
if(function_at_high_bound * function_at_low_bound) >= 0.0:
raise RootException("ERROR! ROOT IS NOT WITHIN BRACKETS")
# APPLY THE BISECTION METHOD IN SEVERAL ITERATIONS UNTIL A SOLUTION WITH
# THE DESIRED ACCURACY IS FOUND
# Note: the program allows for up to 100 iterations. Normally an exit will
# be made from the loop well before that number. If, for some reason, the
# program exceeds 100 iterations, there will be a pause to alert the user.
if function_at_low_bound < 0.0:
time = low_bound
differential_change = high_bound - low_bound
else:
time = high_bound
differential_change = low_bound - high_bound
for j in range(100):
last_diff_change = differential_change
differential_change = last_diff_change * 0.5
mid_range_time = time + differential_change
mid_range_ambient_pressure = (starting_ambient_pressure + rate * mid_range_time)
mid_range_helium_pressure = schreiner_equation(initial_inspired_he_pressure, helium_rate, mid_range_time, self.Helium_Time_Constant[i], self.Initial_Helium_Pressure[i])
mid_range_nitrogen_pressure = schreiner_equation(initial_inspired_n2_pressure, nitrogen_rate, mid_range_time, self.Nitrogen_Time_Constant[i], self.Initial_Nitrogen_Pressure[i])
gas_tension_at_mid_range = mid_range_helium_pressure + mid_range_nitrogen_pressure + self.Constant_Pressure_Other_Gases
function_at_mid_range = mid_range_ambient_pressure - gas_tension_at_mid_range - gradient_onset_of_imperm
if function_at_mid_range <= 0.0:
time = mid_range_time
# When a solution with the desired accuracy is found, the program breaks
if (abs(differential_change) < 1.0E-3) or (function_at_mid_range == 0.0):
break
self.Amb_Pressure_Onset_of_Imperm[i] = mid_range_ambient_pressure
self.Gas_Tension_Onset_of_Imperm[i] = gas_tension_at_mid_range
def gas_loadings_constant_depth(self, depth, run_time_end_of_segment):
"""
Purpose: This subprogram applies the Haldane equation to update the
gas loadings (partial pressures of helium and nitrogen) in the half-time
compartments for a segment at constant depth.
Side Effects: Sets
`self.Ending_Ambient_Pressure`,
`self.Helium_Pressure`,
`self.Nitrogen_Pressure`
`self.Run_Time`,
`self.Segment_Number`,
`self.Segment_Time`,
Returns: None
"""
self.Segment_Time = run_time_end_of_segment - self.Run_Time
self.Run_Time = run_time_end_of_segment
self.Segment_Number += 1
ambient_pressure = depth + self.Barometric_Pressure
inspired_helium_pressure = (ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Helium[self.Mix_Number - 1]
inspired_nitrogen_pressure = (ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Nitrogen[self.Mix_Number - 1]
self.Ending_Ambient_Pressure = ambient_pressure
for i in range(COMPARTMENTS):
temp_helium_pressure = self.Helium_Pressure[i]
temp_nitrogen_pressure = self.Nitrogen_Pressure[i]
self.Helium_Pressure[i] = haldane_equation(temp_helium_pressure, inspired_helium_pressure, self.Helium_Time_Constant[i], self.Segment_Time)
self.Nitrogen_Pressure[i] = haldane_equation(temp_nitrogen_pressure, inspired_nitrogen_pressure, self.Nitrogen_Time_Constant[i], self.Segment_Time)
def nuclear_regeneration(self, dive_time):
"""
Purpose: This subprogram calculates the regeneration of VPM critical
radii that takes place over the dive time. The regeneration time constant
has a time scale of weeks so this will have very little impact on dives of
normal length, but will have a major impact for saturation dives.
Side Effects: Sets
`self.Adjusted_Crushing_Pressure_He`,
`self.Adjusted_Crushing_Pressure_N2`
`self.Regenerated_Radius_He`,
`self.Regenerated_Radius_N2`,
Returns: None
"""
# First convert the maximum crushing pressure obtained for each compartment
# to Pascals. Next, compute the ending radius for helium and nitrogen
# critical nuclei in each compartment.
for i in range(COMPARTMENTS):
crushing_pressure_pascals_he = (self.Max_Crushing_Pressure_He[i] / self.Units_Factor) * self.ATM
crushing_pressure_pascals_n2 = (self.Max_Crushing_Pressure_N2[i] / self.Units_Factor) * self.ATM
ending_radius_he = 1.0 / (crushing_pressure_pascals_he / (2.0 * (self.settings_values['Skin_Compression_GammaC'] - self.settings_values['Surface_Tension_Gamma'])) + 1.0 / self.Adjusted_Critical_Radius_He[i])
ending_radius_n2 = 1.0 / (crushing_pressure_pascals_n2 / (2.0 * (self.settings_values['Skin_Compression_GammaC'] - self.settings_values['Surface_Tension_Gamma'])) + 1.0 / self.Adjusted_Critical_Radius_N2[i])
# A "regenerated" radius for each nucleus is now calculated based on the
# regeneration time constant. This means that after application of
# crushing pressure and reduction in radius, a nucleus will slowly grow
# back to its original initial radius over a period of time. This
# phenomenon is probabilistic in nature and depends on absolute temperature.
# It is independent of crushing pressure.
self.Regenerated_Radius_He[i] = self.Adjusted_Critical_Radius_He[i] + (ending_radius_he - self.Adjusted_Critical_Radius_He[i]) * exp(-dive_time / self.settings_values['Regeneration_Time_Constant'])
self.Regenerated_Radius_N2[i] = self.Adjusted_Critical_Radius_N2[i] + (ending_radius_n2 - self.Adjusted_Critical_Radius_N2[i]) * exp(-dive_time / self.settings_values['Regeneration_Time_Constant'])
# In order to preserve reference back to the initial critical radii after
# regeneration, an "adjusted crushing pressure" for the nuclei in each
# compartment must be computed. In other words, this is the value of
# crushing pressure that would have reduced the original nucleus to the
# to the present radius had regeneration not taken place. The ratio
# for adjusting crushing pressure is obtained from algebraic manipulation
# of the standard VPM equations. The adjusted crushing pressure, in lieu
# of the original crushing pressure, is then applied in the VPM Critical
# Volume Algorithm and the VPM Repetitive Algorithm.
crush_pressure_adjust_ratio_he = (ending_radius_he * (self.Adjusted_Critical_Radius_He[i] - self.Regenerated_Radius_He[i])) / (self.Regenerated_Radius_He[i] * (self.Adjusted_Critical_Radius_He[i] - ending_radius_he))
crush_pressure_adjust_ratio_n2 = (ending_radius_n2 * (self.Adjusted_Critical_Radius_N2[i] - self.Regenerated_Radius_N2[i])) / (self.Regenerated_Radius_N2[i] * (self.Adjusted_Critical_Radius_N2[i] - ending_radius_n2))
adj_crush_pressure_he_pascals = crushing_pressure_pascals_he * crush_pressure_adjust_ratio_he
adj_crush_pressure_n2_pascals = crushing_pressure_pascals_n2 * crush_pressure_adjust_ratio_n2
self.Adjusted_Crushing_Pressure_He[i] = (adj_crush_pressure_he_pascals / self.ATM) * self.Units_Factor
self.Adjusted_Crushing_Pressure_N2[i] = (adj_crush_pressure_n2_pascals / self.ATM) * self.Units_Factor
def calc_initial_allowable_gradient(self):
"""
Purpose: This subprogram calculates the initial allowable gradients for
helium and nitrogen in each compartment. These are the gradients that
will be used to set the deco ceiling on the first pass through the deco
loop. If the Critical Volume Algorithm is set to "off", then these
gradients will determine the final deco schedule. Otherwise, if the
Critical Volume Algorithm is set to "on", these gradients will be further
"relaxed" by the Critical Volume Algorithm subroutine. The initial
allowable gradients are referred to as "PssMin" in the papers by Yount
and colleagues, i.e., the minimum supersaturation pressure gradients
that will probe bubble formation in the VPM nuclei that started with the
designated minimum initial radius (critical radius).
The initial allowable gradients are computed directly from the
"regenerated" radii after the Nuclear Regeneration subroutine. These
gradients are tracked separately for helium and nitrogen.
Side Effects: Sets
`self.Allowable_Gradient_He`,
`self.Allowable_Gradient_N2`
`self.Initial_Allowable_Gradient_He`,
`self.Initial_Allowable_Gradient_N2`,
Returns: None
"""
# The initial allowable gradients are computed in Pascals and then converted
# to the diving pressure units. Two different sets of arrays are used to
# save the calculations - Initial Allowable Gradients and Allowable
# Gradients. The Allowable Gradients are assigned the values from Initial
# Allowable Gradients however the Allowable Gradients can be changed later
# by the Critical Volume subroutine. The values for the Initial Allowable
# Gradients are saved in a global array for later use by both the Critical
# Volume subroutine and the VPM Repetitive Algorithm subroutine.
for i in range(COMPARTMENTS):
initial_allowable_grad_n2_pa = ((2.0 * self.settings_values['Surface_Tension_Gamma'] * (self.settings_values['Skin_Compression_GammaC'] - self.settings_values['Surface_Tension_Gamma'])) / (self.Regenerated_Radius_N2[i] * self.settings_values['Skin_Compression_GammaC']))
initial_allowable_grad_he_pa = ((2.0 * self.settings_values['Surface_Tension_Gamma'] * (self.settings_values['Skin_Compression_GammaC'] - self.settings_values['Surface_Tension_Gamma'])) / (self.Regenerated_Radius_He[i] * self.settings_values['Skin_Compression_GammaC']))
self.Initial_Allowable_Gradient_N2[i] = (initial_allowable_grad_n2_pa / self.ATM) * self.Units_Factor
self.Initial_Allowable_Gradient_He[i] = (initial_allowable_grad_he_pa / self.ATM) * self.Units_Factor
self.Allowable_Gradient_He[i] = self.Initial_Allowable_Gradient_He[i]
self.Allowable_Gradient_N2[i] = self.Initial_Allowable_Gradient_N2[i]
def calc_start_of_deco_zone(self, starting_depth, rate):
"""
Purpose: This subroutine uses the Bisection Method to find the depth at
which the leading compartment just enters the decompression zone.
Source: "Numerical Recipes in Fortran 77", Cambridge University Press,
1992.
Side Effects: Sets
`self.Depth_Start_of_Deco_Zone`
or
Raises a RootException
Returns: None
"""
# First initialize some variables
self.Depth_Start_of_Deco_Zone = 0.0
starting_ambient_pressure = starting_depth + self.Barometric_Pressure
initial_inspired_he_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Helium[self.Mix_Number - 1]
initial_inspired_n2_pressure = (starting_ambient_pressure - self.Water_Vapor_Pressure) * self.Fraction_Nitrogen[self.Mix_Number - 1]
helium_rate = rate * self.Fraction_Helium[self.Mix_Number - 1]
nitrogen_rate = rate * self.Fraction_Nitrogen[self.Mix_Number - 1]
# ESTABLISH THE BOUNDS FOR THE ROOT SEARCH USING THE BISECTION METHOD
# AND CHECK TO MAKE SURE THAT THE ROOT WILL BE WITHIN BOUNDS. PROCESS
# EACH COMPARTMENT INDIVIDUALLY AND FIND THE MAXIMUM DEPTH ACROSS ALL
# COMPARTMENTS (LEADING COMPARTMENT)
# In this case, we are solving for time - the time when the gas tension in
# the compartment will be equal to ambient pressure. The low bound for time
# is set at zero and the high bound is set at the time it would take to
# ascend to zero ambient pressure (absolute). Since the ascent rate is
# negative, a multiplier of -1.0 is used to make the time positive. The
# desired point when gas tension equals ambient pressure is found at a time
# somewhere between these endpoints. The algorithm checks to make sure that
# the solution lies in between these bounds by first computing the low bound
# and high bound function values.
low_bound = 0.0
high_bound = -1.0 * (starting_ambient_pressure / rate)
for i in range(COMPARTMENTS):
initial_helium_pressure = self.Helium_Pressure[i]
initial_nitrogen_pressure = self.Nitrogen_Pressure[i]
function_at_low_bound = initial_helium_pressure + initial_nitrogen_pressure + self.Constant_Pressure_Other_Gases - starting_ambient_pressure
high_bound_helium_pressure = schreiner_equation(initial_inspired_he_pressure, helium_rate, high_bound, self.Helium_Time_Constant[i], initial_helium_pressure)
high_bound_nitrogen_pressure = schreiner_equation(initial_inspired_n2_pressure, nitrogen_rate, high_bound, self.Nitrogen_Time_Constant[i], initial_nitrogen_pressure)
function_at_high_bound = high_bound_helium_pressure + high_bound_nitrogen_pressure + self.Constant_Pressure_Other_Gases
if (function_at_high_bound * function_at_low_bound) >= 0.0:
raise RootException("ERROR! ROOT IS NOT WITHIN BRACKETS")
# APPLY THE BISECTION METHOD IN SEVERAL ITERATIONS UNTIL A SOLUTION WITH
# THE DESIRED ACCURACY IS FOUND
# Note: the program allows for up to 100 iterations. Normally an exit will
# be made from the loop well before that number. If, for some reason, the
# program exceeds 100 iterations, there will be a pause to alert the user.
if function_at_low_bound < 0.0:
time_to_start_of_deco_zone = low_bound
differential_change = high_bound - low_bound
else:
time_to_start_of_deco_zone = high_bound
differential_change = low_bound - high_bound
for j in range(100):
last_diff_change = differential_change
differential_change = last_diff_change * 0.5
mid_range_time = time_to_start_of_deco_zone + differential_change
mid_range_helium_pressure = schreiner_equation(initial_inspired_he_pressure, helium_rate, mid_range_time, self.Helium_Time_Constant[i], initial_helium_pressure)
mid_range_nitrogen_pressure = schreiner_equation(initial_inspired_n2_pressure, nitrogen_rate, mid_range_time, self.Nitrogen_Time_Constant[i], initial_nitrogen_pressure)
function_at_mid_range = mid_range_helium_pressure + mid_range_nitrogen_pressure + self.Constant_Pressure_Other_Gases - (starting_ambient_pressure + rate * mid_range_time)
if function_at_mid_range <= 0.0:
time_to_start_of_deco_zone = mid_range_time
if (abs(differential_change) < 1.0E-3) or (function_at_mid_range == 0.0):
break
if j == 100:
raise MaxIterationException('ERROR! ROOT SEARCH EXCEEDED MAXIMUM ITERATIONS')
# When a solution with the desired accuracy is found, the program jumps out
# of the loop to Line 170 and assigns the solution value for the individual
# compartment.
cpt_depth_start_of_deco_zone = (starting_ambient_pressure + rate * time_to_start_of_deco_zone) - self.Barometric_Pressure
# The overall solution will be the compartment with the maximum depth where
# gas tension equals ambient pressure (leading compartment).
self.Depth_Start_of_Deco_Zone = max(self.Depth_Start_of_Deco_Zone, cpt_depth_start_of_deco_zone)
def calc_ascent_ceiling(self):
"""
Purpose: This subprogram calculates the ascent ceiling (the safe ascent
depth) in each compartment, based on the allowable gradients, and then
finds the deepest ascent ceiling across all compartments.
Side Effects: Sets