Package kenozooid :: Package plan :: Module deco

Source Code for Module kenozooid.plan.deco

  1  # 
  2  # Kenozooid - dive planning and analysis toolbox. 
  3  # 
  4  # Copyright (C) 2009-2019 by Artur Wroblewski <wrobell@riseup.net> 
  5  # 
  6  # This program is free software: you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation, either version 3 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 18  # 
 19   
 20  """ 
 21  Decompression dive planning. 
 22  """ 
 23   
 24  from collections import namedtuple 
 25  import enum 
 26  import itertools 
 27  import math 
 28  import operator 
 29  import re 
 30  import logging 
 31   
 32  from kenozooid.data import gas 
 33  from kenozooid.calc import mod, pp_o2 
 34   
 35  logger = logging.getLogger(__name__) 
 36   
 37   
 38  RE_GAS = re.compile(""" 
 39      ^(?P<name> 
 40          (?P<type> O2 | AIR | EAN | TX) 
 41          ((?<=TX|AN)(?P<o2>[0-9]{2}))? 
 42          ((?<=TX..)/(?P<he>[0-9]{2}))? 
 43      ) 
 44      (@(?P<depth>[0-9]+))? 
 45      (\|(?P<tank>([2-9]x[1-9]{1,2})))? 
 46      $ 
 47  """, re.VERBOSE) 
 48   
 49   
50 -class DivePlanError(Exception):
51 """ 52 Dive planner exception. 53 """
54 55 56
57 -class GasList(object):
58 """ 59 List of gas mixes. 60 61 :var travel_gas: List of travel gas mixes. 62 :var bottom_gas: Bottom gas mix. 63 :var deco_gas: List of decompression gas mixes. 64 """
65 - def __init__(self, gas):
66 """ 67 Create list of gas mixes. 68 69 :param gas: Bottom gas mix. 70 """ 71 self.bottom_gas = gas 72 self.travel_gas = [] 73 self.deco_gas = []
74 75 76
77 -class DivePlan(object):
78 """ 79 Dive plan information. 80 81 Dive plan information contains list of dive profiles, gas volume 82 information and dive decompression parameters. 83 84 :var profiles: List of dive profiles. 85 :var min_gas_vol: Minimal volume of gas mixes required for the plan. 86 :var last_stop_6m: True if last stop is at 6m. 87 :var gf_low: Gradient factor low value (decompression model specific). 88 :var gf_high: Gradient factor high value (decompression model specific). 89 :var descent_rate: Descent rate. 90 :var rmv: Respiratory Minute Volume. 91 :var ext_profile: Tuple depth and time to add for an extended dive 92 profile. 93 """
94 - def __init__(self):
95 self.profiles = [] 96 self.min_gas_vol = {} 97 self.last_stop_6m = False 98 self.gf_low = 30 99 self.gf_high = 85 100 101 self.descent_rate = 20 102 self.rmv = 20 103 self.ext_profile = 5, 3 104 105 self.gas_mix_ppo2 = 1.4 106 self.deco_gas_mix_ppo2 = 1.6
107 108 109
110 -class DiveProfile(object):
111 """ 112 Dive profile information. 113 114 :var type: Dive profile type. 115 :var gas_list: Gas list for the dive profile. 116 :var depth: Maximum dive depth. 117 :var time: Dive bottom time. 118 :var descent_time: Time required to descent to dive bottom depth. 119 :var deco_time: Total decompression time. 120 :var dive_time: Total dive time. 121 :var pp_o2: The O2 partial pressure of bottom gas mix at maximum dive 122 depth. 123 :var mod: Maximum operating depth for bottom gas mix and maximum dive 124 depth. 125 :var slate: Dive slate. 126 :var gas_vol: Dictionary of gas mix and gas volume required for the 127 dive. 128 :var gas_info: Gas mix requirements information. 129 """
130 - def __init__(self, type, gas_list, depth, time):
131 self.type = type 132 self.gas_list = gas_list 133 self.depth = depth 134 self.time = time 135 self.descent_time = None 136 self.deco_time = None 137 self.dive_time = None 138 self.pp_o2 = None 139 self.mod = None 140 self.slate = [] 141 self.gas_vol = {} 142 self.gas_info = []
143 144 145
146 -class ProfileType(enum.Enum):
147 """ 148 Dive profile type. 149 150 The dive profile types are 151 152 PLANNED 153 Dive profile planned by a diver. 154 LOST_GAS 155 Dive profile as planned dive but for lost decompression gas. 156 EXTENDED 157 Extended dive profile compared to planned dive profile. 158 EXTENDED_LOST_GAS 159 Combination of `EXTENDED` and `LOST_GAS` dive profiles. 160 """ 161 PLANNED = 'planned' 162 LOST_GAS = 'lost gas' 163 EXTENDED = 'extended' 164 EXTENDED_LOST_GAS = 'extended + lost gas'
165 166 167
168 -def plan_deco_dive(plan, gas_list, depth, time):
169 """ 170 Plan decompression dive. 171 172 The dive plan information is calculated and stored in the dive plan 173 object. 174 175 Any dive plan configuration should be set in the dive plan object 176 before calling this function. 177 178 :param plan: Dive plan object to be filled with dive plan information. 179 :param gas_list: Gas mix configuration list. 180 :param depth: Maximum dive depth. 181 :param time: Dive bottom time. 182 """ 183 ext_depth = depth + plan.ext_profile[0] 184 ext_time = time + plan.ext_profile[1] 185 186 gas_list = gas_mix_depth_update( 187 gas_list, plan.gas_mix_ppo2, plan.deco_gas_mix_ppo2 188 ) 189 190 lost_gas_list = GasList(gas_list.bottom_gas) 191 lost_gas_list.travel_gas.extend(gas_list.travel_gas) 192 193 pt = ProfileType 194 plan.profiles = [ 195 DiveProfile(pt.PLANNED, gas_list, depth, time), 196 DiveProfile(pt.LOST_GAS, lost_gas_list, depth, time), 197 DiveProfile(pt.EXTENDED, gas_list, ext_depth, ext_time), 198 DiveProfile(pt.EXTENDED_LOST_GAS, lost_gas_list, ext_depth, ext_time), 199 ] 200 201 for p in plan.profiles: 202 stops = deco_stops(plan, p) 203 if not stops: 204 raise DivePlanError('NDL dive, no plan calculated') 205 206 legs = dive_legs(p, stops, plan.descent_rate) 207 if p.type == ProfileType.PLANNED: 208 plan.min_gas_vol = min_gas_volume(p.gas_list, legs, rmv=plan.rmv) 209 210 p.deco_time = sum_deco_time(legs) 211 p.dive_time = sum_dive_time(legs) 212 p.pp_o2 = pp_o2(p.depth, p.gas_list.bottom_gas.o2) 213 p.mod = mod(p.gas_list.bottom_gas.o2, plan.gas_mix_ppo2) 214 p.slate = dive_slate(p, stops, legs, plan.descent_rate) 215 216 p.descent_time = depth_to_time(0, p.depth, plan.descent_rate) 217 p.gas_vol = gas_volume(p.gas_list, legs, rmv=plan.rmv) 218 219 # after ver. 0.15 220 # if p.type != ProfileType.PLANNED: 221 # p.gas_info = gas_vol_info(p.gas_vol, plan.min_gas_vol) 222 223 assert plan.min_gas_vol
224 225
226 -def deco_stops(plan, profile):
227 """ 228 Calculate decompression stops for a dive profile. 229 230 The dive plan information is used to configure decompression engine. 231 232 :param plan: Dive plan information. 233 :param profile: Dive profile information. 234 """ 235 import decotengu # configurable in the future, do not import globally 236 engine = decotengu.create() 237 engine.last_stop_6m = plan.last_stop_6m 238 engine.model.gf_low = plan.gf_low / 100 239 engine.model.gf_high = plan.gf_high / 100 240 241 gas_list = profile.gas_list 242 243 # add gas mix information to decompression engine 244 for m in gas_list.travel_gas: 245 engine.add_gas(m.depth, m.o2, m.he, travel=True) 246 logger.debug('added travel gas {}'.format(m)) 247 248 m = gas_list.bottom_gas 249 engine.add_gas(m.depth, m.o2, m.he) 250 logger.debug('added bottom gas {}'.format(m)) 251 252 for m in gas_list.deco_gas: 253 engine.add_gas(m.depth, m.o2, m.he) 254 logger.debug('added deco gas {}'.format(m)) 255 256 list(engine.calculate(profile.depth, profile.time)) 257 258 return engine.deco_table
259 260
261 -def dive_legs(profile, stops, descent_rate):
262 """ 263 Calculate dive legs information. 264 265 The dive legs information is used for other calculations, i.e. dive gas 266 consumption, dive slate. 267 268 Dive profile is split into legs using 269 270 - gas mix switch depths 271 - dive maximum depth and bottom time 272 - descent rate 273 - list of decompression stops 274 275 The ascent rate is assumed to be 10m/min. 276 277 Each dive leg consists of the following information 278 279 - start depth 280 - end depth 281 - time 282 - gas mix used during a dive leg 283 - deco zone indicator (true or false) 284 """ 285 gas_list = profile.gas_list 286 max_depth = profile.depth 287 time = profile.time 288 289 legs = [] 290 291 # start with descent 292 if gas_list.travel_gas: 293 # add dive legs when travel gas mixes are used 294 mixes = gas_list.travel_gas + [gas_list.bottom_gas] 295 depths = [m.depth for m in mixes[1:]] 296 times = ( 297 depth_to_time(d, m.depth, descent_rate) 298 for m, d in zip(mixes, depths) 299 ) 300 legs.extend( 301 (m.depth, d, t, m, False) 302 for m, d, t in zip(mixes, depths, times) 303 ) 304 305 # descent leg to max depth, it is always from bottom gas mix switch 306 # depth (with or without travel gas mixes), skip it if bottom gas mix 307 # to be switched at max depth 308 m = gas_list.bottom_gas 309 if max_depth > m.depth: 310 t = depth_to_time(m.depth, max_depth, descent_rate) 311 legs.append((m.depth, max_depth, t, m, False)) 312 313 assert abs(sum(l[2] for l in legs) - max_depth / descent_rate) < 0.00001 314 315 # max depth leg, exclude descent time 316 t = time - max_depth / descent_rate 317 legs.append((max_depth, max_depth, t, gas_list.bottom_gas, False)) 318 319 first_stop = stops[0] 320 321 # ascent without decompression stops 322 mixes = [m for m in gas_list.deco_gas if m.depth > first_stop.depth] 323 depths = [max_depth] + [m.depth for m in mixes] + [first_stop.depth] 324 rd = zip(depths[:-1], depths[1:]) 325 mixes.insert(0, gas_list.bottom_gas) 326 legs.extend( 327 (d1, d2, (d1 - d2) / 10, m, False) 328 for (d1, d2), m in zip(rd, mixes) 329 ) 330 331 # ascent with decompression stops till the surface 332 depths = [s.depth for s in stops[1:]] + [0] 333 mixes = { 334 (m.depth // 3) * 3: m for m in gas_list.deco_gas 335 if m.depth <= first_stop.depth 336 } 337 cm = legs[-1][3] # current gas mix 338 for s, d in zip(stops, depths): 339 cm = mixes.get(s.depth, cm) # use current gas mix until gas mix switch 340 legs.append((s.depth, s.depth, s.time, cm, True)) 341 t = (s.depth - d) / 10 342 legs.append((s.depth, d, t, cm, True)) 343 344 return legs
345 346
347 -def dive_legs_overhead(gas_list, legs):
348 """ 349 Determine the overhead part of a decompression dive. 350 351 The overhead part of a dive is the descent, bottom and ascent parts of 352 a dive up to first decompression stop or first decompression gas mix 353 switch. 354 355 The overhead part of a dive is used to calculate gas mix consumption 356 using rule of thirds. 357 358 :param gas_list: Gas list information. 359 :param legs: List of dive legs. 360 361 ..seealso:: :py:func:`dive_legs` 362 """ 363 mix = gas_list.deco_gas[0] if gas_list.deco_gas else None 364 nr = range(len(legs)) 365 k = next(k for k in nr if legs[k][3] == mix or legs[k][-1]) 366 return legs[:k]
367 368
369 -def dive_slate(profile, stops, legs, descent_rate):
370 """ 371 Calculate dive slate for a dive profile. 372 373 The dive decompression stops is a collection of items implementing the 374 following interface 375 376 depth 377 Depth of dive stop [m]. 378 time 379 Time of dive stop [min]. 380 381 Dive slate is list of items consisting of 382 383 - dive depth 384 - decompression stop information, null if no decompression 385 - run time in minutes 386 - gas mix on gas switch, null otherwise 387 388 :param profile: Dive profile information. 389 :param stops: Dive decompression stops. 390 :param legs: Dive legs. 391 :param descent_rate: Dive descent rate. 392 """ 393 slate = [] 394 395 depth = profile.depth 396 time = profile.time 397 gas_list = profile.gas_list 398 rt = 0 399 400 # travel gas switches 401 k = len(gas_list.travel_gas) 402 if k: 403 for i in range(k + 1): 404 leg = legs[i] 405 406 d = leg[0] 407 m = leg[3] 408 slate.append((d, None, round(rt), m)) 409 rt += leg[2] 410 411 legs = legs[k:] 412 413 # bottom time, no descent row on slate 414 rt += legs[0][2] + legs[1][2] # reset run-time 415 d = legs[1][0] 416 m = None if gas_list.travel_gas else legs[1][3] 417 slate.append((d, None, round(rt), m)) 418 419 # no deco gas switches 420 no_deco = [l for l in legs if not l[4]] 421 no_deco = no_deco[2:] 422 for i in range(1, len(no_deco)): 423 prev = no_deco[i - 1] 424 leg = no_deco[i] 425 426 d = leg[0] 427 rt += prev[2] 428 m = leg[3] 429 slate.append((d, None, round(rt), m)) 430 431 # decompression stops 432 deco = [l for l in legs if l[4]] 433 if no_deco: 434 deco.insert(0, no_deco[-1]) 435 for i in range(1, len(deco), 2): 436 prev = deco[i - 1] 437 leg = deco[i] 438 439 d = leg[1] 440 dt = leg[2] 441 rt += dt + prev[2] 442 m = None if prev[3] == leg[3] else leg[3] # indicate gas switch only 443 slate.append((d, dt, round(rt), m)) 444 445 # surface 446 leg = deco[-1] 447 d = leg[1] 448 rt += leg[2] 449 slate.append((d, None, round(rt), None)) 450 451 return slate
452 453
454 -def depth_to_time(start, end, rate):
455 """ 456 Calculate time required to descent or ascent from start to end depth. 457 458 :param start: Starting depth. 459 :param end: Ending depth. 460 :param rate: Ascent or descent rate. 461 """ 462 return abs(start - end) / rate
463 464
465 -def sum_deco_time(legs):
466 """ 467 Calculate total decompression time using dive legs. 468 469 :param legs: List of dive legs. 470 471 ..seealso:: :py:func:`dive_legs` 472 """ 473 return sum(l[2] for l in legs if l[-1])
474 475
476 -def sum_dive_time(legs):
477 """ 478 Calculate total dive time using dive legs. 479 480 :param legs: List of dive legs. 481 482 ..seealso:: :py:func:`dive_legs` 483 """ 484 return sum(l[2] for l in legs)
485 486
487 -def gas_vol_info(gas_vol, min_gas_vol):
488 """ 489 Analyze gas volume requirements using gas mix volume calculations. 490 491 The list of messages is returned, which confirm required gas mix volume 492 or warn about gas logistics problems. 493 494 :param gas_vol: Gas volume requirements per gas mix. 495 :param min_gas_vol: Minimal gas mixes volume for the plan. 496 497 .. seealso:: 498 499 :py:func:`min_gas_volume` 500 :py:func:`gas_volume` 501 502 """ 503 info = [] 504 for mix, vol in gas_vol.items(): 505 assert mix in min_gas_vol 506 507 fmt = '{}Gas mix {} volume {}.' 508 if vol <= min_gas_vol[mix]: 509 msg = fmt.format('', mix.name, 'OK') 510 else: 511 msg = fmt.format('WARN: ', mix.name, 'NOT OK') 512 info.append(msg) 513 514 # TODO: msg = 'No diving cylinders specified to verify its configuration.' 515 return info
516 517
518 -def gas_volume(gas_list, legs, rmv=20):
519 """ 520 Calculate dive gas mix volume information. 521 522 Gas mix volume is calculated for each gas mix on the gas list. The 523 volume information is returned as dictionary `gas mix name -> usage`, 524 where gas usage is volume of gas in liters. 525 526 The key of the gas mix volume dictionary is gas mix name to merge all 527 travel and decompression gas mixes information regardless their depth 528 switch. 529 530 FIXME: apply separate RMV for decompression gas 531 532 :param gas_list: Gas list information. 533 :param legs: List of dive legs. 534 :param rmv: Respiratory minute volume (RMV) [min/l]. 535 536 ..seealso:: :py:func:`dive_legs` 537 """ 538 mixes = gas_list.travel_gas + [gas_list.bottom_gas] + gas_list.deco_gas 539 gas_vol = {m.name: 0 for m in mixes} 540 541 items = ( 542 (leg[3].name, ((leg[0] + leg[1]) / 2 / 10 + 1) * leg[2] * rmv) 543 for leg in legs 544 ) 545 key = operator.itemgetter(0) 546 items = sorted(items, key=key) 547 items = itertools.groupby(items, key) 548 gas_vol.update({m: sum(v[1] for v in vols) for m, vols in items}) 549 return gas_vol
550 551
552 -def min_gas_volume(gas_list, legs, rmv=20):
553 """ 554 Calculate minimal volume of gas mixes required for a dive using rule of 555 thirds. 556 557 The volume information is returned as dictionary `gas mix -> usage`, 558 where gas usage is volume of gas in liters. 559 560 :param gas_list: Gas list information. 561 :param legs: List of dive legs. 562 """ 563 # simply take gas volume requirements for the dive 564 gas_vol = gas_volume(gas_list, legs, rmv=rmv) 565 566 # but recalculate required volume of bottom gas for overhead part of 567 # dive 568 oh_legs = dive_legs_overhead(gas_list, legs) 569 cons = gas_volume(gas_list, oh_legs, rmv=rmv) 570 gas_vol[gas_list.bottom_gas] = cons[gas_list.bottom_gas.name] 571 572 # use rule of thirds 573 for mix in gas_vol: 574 gas_vol[mix] *= 1.5 575 return gas_vol
576 577
578 -def gas_mix_depth_update(gas_list, ppo2, deco_ppo2):
579 """ 580 Update gas mix list, so every gas mix has depth specified. 581 582 The following rules are used 583 584 - gas mixes with non-null depth are _not_ changed 585 - first travel gas mix switch depth is 0m 586 - if no travel gas mixes specified, then bottom gas mix switch depth is 587 set to 0m 588 - travel and bottom gas mixes are updated with MOD calculated using 589 ppO2 and O2 value of previous gas mix on the list 590 - decompression gas mixes are updated with MOD calculated using 591 decompression ppO2 and O2 value of changed gas mix 592 593 :param gas_list: Gas mix list to modify. 594 :param ppo2: ppO2 value used to calculate MOD of travel and bottom gas 595 mixes. 596 :param deco_ppo2: ppO2 value used to calculate MOD of decompression gas 597 mixes. 598 """ 599 def change_gas_mix(gas_mix, o2, ppo2, condition): 600 if gas_mix.depth is None: 601 v = mod(o2, ppo2) if condition else 0 602 return gas_mix._replace(depth=math.floor(v)) 603 else: 604 return gas_mix
605 606 m = gas_list.travel_gas[-1] if gas_list.travel_gas else gas_list.bottom_gas 607 bottom_gas = change_gas_mix( 608 gas_list.bottom_gas, m.o2, ppo2, gas_list.travel_gas 609 ) 610 new_list = GasList(bottom_gas) 611 612 if gas_list.travel_gas: 613 # change first travel gas mix 614 m = gas_list.travel_gas[0] 615 m = m._replace(depth=0) if m.depth is None else m 616 assert m.depth is not None 617 new_list.travel_gas = [m] 618 619 o2_list = [m.o2 for m in gas_list.travel_gas] 620 new_list.travel_gas.extend( 621 change_gas_mix(m, o2, ppo2, True) for m, o2 in 622 zip(gas_list.travel_gas[1:], o2_list[:-1]) 623 ) 624 625 new_list.deco_gas = [ 626 change_gas_mix(m, m.o2, deco_ppo2, True) for m in gas_list.deco_gas 627 ] 628 629 return new_list 630 631
632 -def plan_to_text(plan):
633 """ 634 Convert decompression dive plan to text. 635 """ 636 txt = [] 637 638 # dive profiles summary 639 txt.append('') 640 t = 'Dive Profile Summary' 641 txt.append(t) 642 txt.append('-' * len(t)) 643 644 titles = ( 645 'Depth [m]', 'Bottom Time [min]', 'Descent Time [min]', 646 'Total Decompression Time [min]', 'Total Dive Time [min]', 647 'O2 Pressure of Bottom Gas Mix', 'MOD for Bottom Gas Mix' 648 ) 649 attrs = 'depth', 'time', 'descent_time', 'deco_time', 'dive_time', \ 650 'pp_o2', 'mod' 651 fmts = '{:>6d}', '{:>6d}', '{:>6.1f}', '{:>6.0f}', '{:>6.0f}', \ 652 '{:>6.2f}', '{:>6.0f}' 653 assert len(titles) == len(fmts) == len(attrs) 654 655 # create dive profiles summary table 656 th = '=' * 30 + ' ' + ' '.join(['=' * 6, ] * 4) 657 txt.append(th) 658 txt.append(' {:32}'.format('Name') + 'P LG E E+LG') 659 txt.append(th) 660 for title, attr, fmt in zip(titles, attrs, fmts): 661 t = '{:30s} '.format(title) + ' '.join([fmt] * 4) 662 values = [getattr(p, attr) for p in plan.profiles] 663 txt.append(t.format(*values)) 664 txt.append(th) 665 txt.append('') 666 667 txt.append('') 668 t = 'Gas Logistics' 669 txt.append(t) 670 txt.append('-' * len(t)) 671 672 # required gas volume information as a table 673 th = '=' * 30 + ' ' + ' '.join(['=' * 6, ] * 4) 674 txt.append(th) 675 txt.append('{:33s}'.format('Gas Mix') + 'P LG E E+LG') 676 txt.append(th) 677 gas_list = plan.profiles[0].gas_list # all other plans, do not use more 678 # gas mixes 679 gas_list = gas_list.travel_gas + [gas_list.bottom_gas] + gas_list.deco_gas 680 gas_mix_names = sorted(set(m.name for m in gas_list)) 681 for m in gas_mix_names: 682 n = 'Gas Mix {} [liter]'.format(m) 683 vol = [p.gas_vol.get(m, 0) for p in plan.profiles] 684 # the main profile gas volume reported using rule of thirds 685 vol[0] = plan.min_gas_vol[m] 686 na = ' xx ' 687 s = ('{:6.0f}'.format(v) if v > 0 else na for v in vol) 688 t = '{:30s}'.format(n) + ' ' + ' '.join(s) 689 txt.append(t) 690 691 txt.append(th) 692 txt.append('') 693 694 # after ver. 0.15 695 # gas volume analysis information 696 # txt.append('') 697 # for p in plan.profiles: 698 # if p.type != ProfileType.PLANNED: 699 # txt.append('Dive profile: {}'.format(p.type)) 700 # txt.extend(' ' + s for s in p.gas_info) 701 # txt.append('') 702 703 # dive slates 704 t = 'Dive Slates' 705 txt.append(t) 706 txt.append('-' * len(t)) 707 for p in plan.profiles: 708 txt.append('Profile *{}*::'.format(p.type.value)) 709 txt.append('') 710 slate = p.slate 711 t = ' {:>3} {:>3} {:>4} {:7}'.format('D', 'DT', 'RT', 'GAS') 712 txt.append(t) 713 txt.append(' ' + '-' * (len(t) - 1)) 714 for item in slate: 715 st = int(item[1]) if item[1] else '' 716 717 m = item[3] 718 star = '*' if m else ' ' 719 m = m.name if m else '' 720 721 t = ' {}{:>3} {:>3} {:>4} {}'.format( 722 star, int(item[0]), st, int(item[2]), m 723 ) 724 txt.append(t) 725 txt.append('') 726 727 # dive plan parameters 728 t = 'Parameters' 729 txt.append(t) 730 txt.append('-' * len(t)) 731 732 titles = ( 733 'RMV [l/min]', 'Last stop at 6m', 'GF Low', 'GF High', #'RMV [l/min]', 'Extended Profile', 734 #'Decompression Model', 'Decompression Library' 735 ) 736 attrs = ('rmv', 'last_stop_6m', 'gf_low', 'gf_high')#, 'deco_time', 'dive_time') 737 fmts = ('{:>6}', ' {}', '{:>6}%', '{:>6}%')#, '{:>6.0f}')#, '{:>6.0f}') 738 assert len(titles) == len(fmts) == len(attrs) 739 740 th = '=' * 30 + ' ' + '=' * 7 741 txt.append(th) 742 txt.append('Parameter' + ' ' * 23 + 'Value') 743 txt.append(th) 744 for title, attr, fmt in zip(titles, attrs, fmts): 745 t = '{:30s} '.format(title) + fmt 746 txt.append(t.format(getattr(plan, attr))) 747 txt.append(th) 748 txt.append('') 749 750 751 return '\n'.join(txt)
752 753
754 -def parse_gas(t, travel=False):
755 """ 756 Parse gas mix. 757 758 :param t: Gas mix string. 759 :param travel: True if travel gas mix. 760 """ 761 t = t.upper() 762 v = RE_GAS.search(t) 763 m = None 764 765 if v: 766 n = v.group('name') 767 768 p = v.group('o2') 769 if p is None: 770 if n == 'AIR': 771 o2 = 21 772 elif n == 'O2': 773 o2 = 100 774 else: 775 return None 776 else: 777 o2 = int(p) 778 779 p = v.group('he') 780 he = 0 if p is None else int(p) 781 782 p = v.group('depth') 783 depth = None if p is None else int(p) 784 #tank = v.group('tank') 785 m = gas(o2, he, depth=depth) 786 787 return m
788 789
790 -def parse_gas_list(*args):
791 """ 792 Parse gas mix list. 793 794 :param *args: List of gas mix strings. 795 """ 796 travel_gas = [parse_gas(a[1:], True) for a in args if a[0] == '+'] 797 deco_gas = [parse_gas(a) for a in args if a[0] != '+'] 798 bottom_gas = deco_gas[0] 799 del deco_gas[0] 800 801 gl = GasList(bottom_gas) 802 gl.travel_gas.extend(travel_gas) 803 gl.deco_gas.extend(deco_gas) 804 return gl
805 806 807 # vim: sw=4:et:ai 808