1 """!Defines StormInfo and related functions for interacting with
4 This module handles reading and manipulating individual entries of
5 TCVitals files or CARQ entries of aid ("A deck") files. It provides
6 each line as a StormInfo object with all available information
7 contained within. This module does NOT supply much functionality for
8 manipulating entire tcvitals databases. That is provided in the
11 @note StormInfo is good at complex manipulations of a single vitals
12 time. However, it is inherently slow, and should not be used for
13 large-scale manipulation of many vitals times such as multiple years
14 of tcvitals or deck files. For example, model forecast verification
15 packages should not use StormInfo. It is better to use compiled
16 programs for such purposes. This slowness is inherent to Python,
17 which is quite slow at creating and modifying objects."""
21 __all__=[
'current_century',
'StormInfoError',
'InvalidBasinError',
22 'InvalidStormInfoLine',
'InvalidVitals',
'CenturyError',
23 'InvalidATCF',
'NoSuchVitals',
'name_number_okay',
24 'basin_center_okay',
'vit_cmp_by_storm',
'vitcmp',
'storm_key',
25 'clean_up_vitals',
'floatlatlon',
'quadrantinfo',
26 'parse_tcvitals',
'find_tcvitals_for',
'parse_carq',
'StormInfo',
29 import re, datetime, math, fractions, logging, copy
37 current_century=int(datetime.datetime.now().year)/100
38 """The first two digits of the year: the thousands and hundreds
39 digits. This is used to convert message files to tcvitals."""
42 """!This is the base class of all exceptions raised when errors are
43 found in the tcvitals, Best Track, Aid Deck or other storm
44 information databases."""
47 """!This exception is raised when an invalid Tropical Cyclone basin
48 is found. The invalid basin is available as self.basin, and the
49 subbasin is self.subbasin (which might be None)."""
51 """!InvalidBasinError constructor
52 @param basin the basin in question
53 @param subbasin the subbasin, if known. For example, the
54 North Indian Ocean (IO) is split into subbasins
55 Arabian Sea (AA) and Bay of Bengal (BB)"""
66 """!Return a human-readable string representation of this error"""
68 return 'Invalid basin identifier %s'%(repr(self.
basin),)
70 return 'Invalid basin identifier %s (subbasin %s)' \
73 """!Return a Pythonic representation of this error."""
74 return '%s(%s,%s)'%(type(self).__name__,repr(self.
basin),
78 """!This exception is raised when the StormInfo class receives an
79 invalid tcvitals line or ATCF line that it cannot parse. The line
80 is available as self.badline."""
82 """!InvalidStormInfoLine constructor
84 @param message the error message
85 @param badline line at which the problem happened"""
87 super(InvalidStormInfoLine,self).
__init__(message)
93 """!Raised when a syntax error is found in the tcvitals, and the
94 code cannot guess what the operator intended."""
95 class CenturyError(InvalidStormInfoLine):
96 """!Raised when an implausible century is found."""
98 """!Raised when invalid ATCF data is found."""
100 """!This should be raised when the user requests a specific storm
101 or cycle of a storm and no such vitals exists. This module never
102 raises this exception: this is meant to be raised by calling
106 """!Given an array of StormInfo objects, iterate over those that
107 have valid names and numbers. Discards TEST, UNKNOWN, and numbers
109 @param vl An iterable of StormInfo objects to consider."""
111 if vital.stormname==
'TEST' or vital.stormname==
'UNKNOWN' or \
112 (vital.stnum>50
and vital.stnum<90):
117 """!Given a list of StormInfo objects, iterates over those that
118 have the right basins for the right centers. A, B, W, S and P are
119 allowed for JTWC, and E, L, C and Q are allowed for NHC. Other
120 entries are ignored. Also discards North hemispheric basin storms
121 that are in the south hemisphere and vice-versa
123 @param vl An iterable of StormInfo objects to consider."""
124 okay={
'JTWC':
'ABWSPECQ',
'NHC':
'ELCQ' }
127 if not (center
in okay
and vital.basin1
in okay[center]):
continue
128 if vital.basin1
in 'SPQU':
129 if vital.lat>0:
continue
131 if vital.lat<0:
continue
135 """!A cmp comparison for StormInfo objects intended to be used with
136 sorted(). This is intended to be used on cleaned vitals returned
137 by clean_up_vitals. For other purposes, use vitcmp.
139 Uses the following method:
140 1. Sort numerically by when.year
141 2. Break ties by sorting lexically by stormid3
142 3. Break ties by sorting by date/time
143 4. Break ties by retaining original order ("stable sort").
145 @param a,b StormInfo objects to order"""
146 c=cmp(a.when.year,b.when.year)
147 if c==0: c=cmp(a.longstormid,b.longstormid)
148 if c==0: c=cmp(a.when,b.when)
152 """!A cmp comparison for StormInfo objects intended to be used with
155 @param a,b StormInfo objects to order.
156 Uses the following method:
158 1. Sort numerically by date/time.
159 2. Break ties by a reverse sort by stormid. This places Invest
161 3. Break ties by ASCII lexical sort by center (ie.: JTWC first,
163 4. Break ties by placing vitals WITH 34kt wind radii after those
165 5. Break ties by placing vitals with a full line (through 64kt
167 6. Break ties by retaining original order ("stable sort")."""
169 if c==0: c=-cmp(a.stormid3,b.stormid3)
170 if c==0: c=cmp(a.center,b.center)
171 if c==0: c=cmp(a.have34kt,b.have34kt)
175 """!Generates a hashable key for hashing StormInfo objects
177 Lines are considered to be for the same database entry if they're
178 from the same forecasting center, have the same basin, date/time
179 and storm number. Note that the public two-letter basin (AL, EP,
180 CP, WP, IO, SH, SL) is used here (stormid4) so that a/b and s/p
181 conflicts are handled by keeping the last entry in the file.
183 @param vit the StormInfo of interest.
184 @returns a tuple (center,stormid4,when) from the corresponding members of vit"""
185 return (vit.center, vit.stormid4, vit.when)
187 def clean_up_vitals(vitals,name_number_checker=None,basin_center_checker=None,vitals_cmp=None):
188 """!Given a list of StormInfo, sorts using the vitcmp comparison,
189 discards suspect storm names and numbers as per name_number_okay,
190 and discards invalid basin/center combinations as per
191 basin_center_okay. Lastly, loops over all lines keeping only the
192 last line for each storm and analysis time. The optional
193 name_number_checker is a function or callable object that takes a
194 StormInfo as an argument, and returns True if the name and number
195 match some internal requirements. By default, the
196 name_number_okay function is used.
197 @param vitals A list of StormInfo
198 @param name_number_checker A function that looks like name_number_okay()
199 for determining which storm names and numbers are acceptable
200 @param basin_center_checker A function that looks like basin_center_okay()
201 for determining which basins and RSMCs are okay.
202 @param vitals_cmp a cmp-like function for ordering two StormInfo objects"""
204 if name_number_checker
is None:
205 name_number_checker=name_number_okay
206 if basin_center_checker
is None:
207 basin_center_checker=basin_center_okay
208 if vitals_cmp
is None:
212 sortvitals=sorted(vitals,cmp=vitals_cmp)
216 nn_ok_vitals=[v
for v
in name_number_checker(vitals)]
220 keepvitals=[ v
for v
in basin_center_checker(vitals) ]
226 for vital
in reversed(vitals):
228 if key
in seen:
continue
230 revuniqvitals.append(vital)
231 uniqvitals=[x
for x
in reversed(revuniqvitals)]
235 """!Converts a string like "551N" to 55.1, correctly handling the sign of
239 floatlatlon(string="311N",fact=10.0) = float(31.1) # degrees North
241 @returns degrees North or degrees East
242 @param string Latitude or longitude in sum multiple of a degree,
243 followed by N, S, E or W to specify the hemisphere. Must be
245 @param fact The strung value is divided by this number. The
246 default is to convert from tenths of a degree in string to
247 a degree return value.
248 @note This function does not accept negative numbers. That means
249 the tcvitals "badval" -999 or -99 or -9999 will result in a None
251 m=re.search(
'\A(?P<num>0*\d+)(?:(?P<positive>[NnEe ])|(?P<negative>[SsWw]))\Z',string)
254 latlon=float(mdict[
'num'])/fact
255 if 'negative' in mdict
and mdict[
'negative']
is not None: latlon=-latlon
260 """!Internal function that parses wind or sea quadrant information.
262 This is part of the internal implementation of StormInfo: it deals
263 with parsing wind or sea quadrant information.
264 @param[out] data Output quadrant information, a dict mapping
265 from the quadrant name in qset.
266 @param[out] qset output set of quadrants that were seen
267 @param irad Integer radius. For example, 34 may indicate the 34 kt
269 @param qcode Special code used for circular (all quadrants) data
270 @param qdata String quadrant data information for each quadrant
271 @param what What type of data is this? Wind radii? Sea height radii?
272 This should be an alphanumeric string.
273 @param conversion Unit conversion. """
275 assert(len(qdata)==4)
276 quadrant=[
'NNQ',
'NEQ',
'EEQ',
'SEQ',
'SSQ',
'SWQ',
'WWQ',
'NWQ']
281 if qdata[i]
is not None and qdata[i]!=
'':
282 fqdata[i]=float(qdata[i])
286 fqdata[i]*=conversion
287 maxdata=max(maxdata,fqdata[i])
290 var=
'%sCIRCLE%d'%(what,irad)
296 iquad=quadrant.index(qcode)
298 var=
'%s%s%d'%(what,quadrant[(iquad+2*i)%len(quadrant)][0:2],irad)
303 """!Reads data from a tcvitals file.
305 This reads line by line from the given file object fd, parsing
308 @returns A list of StormInfo objects, one per line.
309 @param fd An opened file to read.
310 @param raise_all If raise_all=True, exceptions will be raised
311 immediately. Otherwise, any StormInfoError or ValueError will be
312 logged or ignored, and parsing will continue.
313 @param logger The logger is a logging.Logger object in which to
314 log messages, or None (the default) to disable logging."""
318 out.append(
StormInfo(
'tcvitals',line.rstrip(
'\n')))
319 except (StormInfoError,ValueError)
as e:
320 if logger
is not None:
321 logger.warning(str(e))
326 stnum=
None,basin1=
None):
327 """!Faster way of finding tcvitals data for a specific case.
329 A fast method of finding tcvitals in a file: instead of parsing
330 each line into a StormInfo, it simply scans the characters of the
331 line trying to find the right storm and time. Returns a list of
332 matching vitals as StormInfo objects.
334 * fd - the stream-like object to read from
335 * logger - the logging.Logger to log to, or None
336 * raise_all - if True, exceptions are raised immediately instead
337 of just being logged.
338 * when - the date to look for, or None
339 * stnum - the storm number (ie.: 09 in 09L)
340 * basin1 - the basin letter (ie.: L in 09L)
342 @warning This function cannot handle errors in the formatting
343 of the tcvitals lines. It will only work if the data in fd
344 strictly follows the tcvitals format."""
345 if(isinstance(stnum,basestring)): stnum=int(stnum)
346 assert(
not isinstance(stnum,basestring))
350 assert(strwhen
is not None)
351 if(logger
is not None):
352 logger.debug(
'VITALS: search for when=%s'%(strwhen,))
353 if stnum
is not None:
354 strstnum=
"%02d" % (stnum,)
355 if(logger
is not None):
356 logger.debug(
'VITALS: search for stnum=%s'%(strstnum,))
357 if basin1
is not None:
358 strbasin1=str(basin1)[0].upper()
359 if(logger
is not None):
360 logger.debug(
'VITALS: search for basin1=%s'%(basin1,))
370 if stnum
is not None and line[5:7]!=strstnum:
continue
371 if when
is not None and line[19:32]!=strwhen:
continue
372 if basin1
is not None and line[7]!=strbasin1:
continue
374 yield StormInfo(
'tcvitals',line.rstrip(
'\n'))
375 if(logger
is not None):
376 logger.debug(
'VITALS: yielded %d matches'%(count,))
379 """!Scans an A deck file connected to stream-like object fd,
380 reading it into a list of StormInfo objects. Returns the list.
382 @param logger A logging.Logger to log to.
383 @param raise_all If False, log and ignore errors instead of raising them.
384 @param fd the file object to read
385 @returns a list of StormInfo objects """
391 if logger
is not None:
392 logger.warning(
'Ignoring short line (<40 chars): %s'%(line,))
395 when2=line[0:40].split(
',')[2].strip()
396 if when
is None or when==when2:
403 out.append(
StormInfo(linetype=
'carq',inputs=store,logger=logger,
404 raise_all=raise_all))
407 except(StormInfoError,ValueError)
as e:
412 """!Storm vitals information from ATCF, B-deck, tcvitals or message files.
414 Represents all information about a one storm at one time as a
415 Python object. It can read a single line of a tcvitals file, one
416 time from a Best Track file, or CARQ entries from an Aid Deck file
417 at a single time. It will scan multiple lines from a Best Track
418 or CARQ group to get the last forecast hour (up to a maximum of
419 72hrs) and all possible radii for one time.
421 This class is meant for complex manipulations of a small amount of
422 data, not for manipulations of whole databases. It can be used
423 for manipulating the entire TCVitals database, or multiple ATCF
424 deck files, but will generally be slower than other libraries --
425 operations will take on the order of ten times as long.
427 @todo Write a separate class for simple manipulations of a whole
428 ATCF database. This could be done efficiently using an in-memory
441 def __init__(self,linetype,inputs,carq='CARQ',logger=None,raise_all=True):
442 """!StormInfo constructor
444 Constructor for the StormInfo class. You should not call this directly.
445 Instead, use the other parsing functions in this module to generate
446 tcvitals from file objects.
448 @param linetype type of vitals: tcvitals, message, carq (ATCF
449 CARQ entries), old, or copy. See below
450 @param inputs inputs, converted to a string before processing
451 @param carq additional CARQ data to fill in more information
452 @param logger a logging.Logger for log messages
453 @param raise_all if True, exceptions will be raised if parser
456 The constructor can create StormInfo objects in several ways, specified
457 by the @c type argument:
458 * @c tcvitals --- Parse a line of a tcvitals file.
459 * @c message --- Parse a tropical cyclone message file.
460 * @c carq --- Parse A deck CARQ entries.
461 * @c old --- Take another StormInfo whose storm id/name has been
462 replaced with another id/name through invest renumbering.
463 Swap the old invest id/name with the current non-invest
465 * @c copy --- Do a deep copy of the supplied StormInfo"""
466 if logger
is not None and not isinstance(logger,logging.Logger):
468 'In StormInfo constructor, logger must be a '
469 'logging.Logger, but instead it is a %s'
470 %(type(logger).__name__))
476 if linetype==
'tcvitals':
479 elif linetype==
'message':
480 self.
line=str(inputs)
482 elif linetype==
'carq':
484 self.
_parse_carq(lines=inputs,tech=str(carq),logger=logger,
485 raise_all=bool(raise_all))
486 elif linetype==
'old' or linetype==
'copy':
489 for t
in ( basestring, int, float, datetime.datetime,
490 datetime.timedelta ):
491 if isinstance(var,t):
return True
493 if not isinstance(inputs,StormInfo):
495 'In StormInfo constructor, when linetype=="old", '
496 'inputs must be a StormInfo object, not a %s.'
497 %(type(inputs).__name__))
498 for k,v
in inputs.__dict__.iteritems():
499 if k[0]==
'_':
continue
500 if k[0:4]==
'old_' and old:
continue
501 if not checktype(v):
continue
504 for k,v
in inputs.__dict__.iteritems():
505 if not checktype(v):
continue
506 if k[0:4]==
'old_': self.__dict__[k[4:]]=v
508 raise InvalidStormInfoFormat(
509 'Unknown storm info format %s: only know "tcvitals" '
510 'and "message".'%(repr(linetype),))
512 """!Returns a copy of this StormInfo, but with the last
513 renumbering or renaming of the vitals undone."""
516 """!Returns a copy if this object."""
519 """!Same as self + (-amount)
520 @param amount The amount of time to extrapolate backwards."""
521 return self+ (-amount)
523 """!Returns a copy of this object, with the vitals extrapolated
524 forward "amount" hours. Only the location is changed.
525 @param amount the amount of time to extrapolate forward"""
528 vmag=max(0,copy.stormspeed)*10.0
530 Rearth=hwrf.constants.Rearth
532 dx=vmag*math.sin(copy.stormdir*pi180)
533 dy=vmag*math.cos(copy.stormdir*pi180)
534 dlat=float(dy*dt/Rearth)*2*math.pi
535 dlon=float(dx*dt*math.cos(copy.lat*pi180)/Rearth)*2*math.pi
536 copy.lat=round( float(copy.lat+dlat)*10 )/10.0
537 copy.lon=round( float(copy.lon+dlon)*10 )/10.0
539 copy.YMDH=copy.when.strftime(
'%Y%m%d%H')
540 for v
in (
'flat',
'flon',
'fhr'):
541 if v
in copy.__dict__: del(copy.__dict__[v])
542 copy.havefcstloc=
False
547 """!Uses the 2013 operational HWRF method of deciding the
548 domain center based on the storm location, basin, and, if
549 available, the 72hr forecast location. Returns a tuple
550 containing a pair of floats (cenlo, cenla) which are the
551 domain center longitude and latitude, respectively. Results
552 are cached internally so future calls will not have to
553 recompute the center location.
554 @param logger a logging.Logger for log messages"""
568 assert(storm_lon
is not None)
571 assert(self.
flon is not None)
574 avglon=storm_lon-20.0
575 assert(avglon
is not None)
579 if storm_lat<0: cenla=-cenla
580 ilat=math.floor(cenla)
581 if ilat < 15: cenla=15.0
582 if ilat > 25: cenla=25.0
583 if ilat >= 35: cenla=30.0
584 if ilat >= 40: cenla=35.0
585 if ilat >= 44: cenla=40.0
586 if ilat >= 50: cenla=45.0
587 if ilat >= 55: cenla=50.0
588 if storm_lat<0: cenla=-cenla
591 if logger
is not None:
592 logger.info(
'Averaging storm_lon=%f and avglon=%f'%(storm_lon,avglon))
593 diff=storm_lon-avglon
594 if(diff> 360.): storm_lon -= 360.0
595 if(diff<-360.): avglon -= 360.0
596 result=int((10.0*storm_lon + 10.0*avglon)/2.0)/10.0
597 if(result > 180.0): result-=360.0
598 if(result < -180.0): result+=360.0
600 if logger
is not None:
601 logger.info(
'Decided cenlo=%f cenla=%f'%(cenlo,cenla))
602 logger.info(
'Storm is at lon=%f lat=%f'%(storm_lon,storm_lat))
606 if(int(cenlo)>int(storm_lon)+5):
608 if logger
is not None:
610 'Center is too far east of storm. Moving it to %f'
613 if(int(cenlo)<int(storm_lon)-5):
615 if logger
is not None:
617 'Center is too far west of storm. Moving it to %f'
620 if logger
is not None and not moved:
621 logger.info(
'Center is within +/- 5 degrees longitude of storm.')
622 logger.info(
'Final outer domain center is lon=%f lat=%f'
626 return ( cenlo, cenla )
628 def _parse_carq(self,lines,tech="CARQ",logger=None,raise_all=True):
629 """!Given an array of lines from a CARQ entry in an ATCF Aid
630 Deck file, parses the data and adds it to this StormInfo
631 object most of the work is done in other subroutines.
632 @param lines list of lines of CARQ data
633 @param tech technique name to grep for, usually CARQ, though BEST also
634 works when using B deck files
635 @param logger a logging.Logger for log messages
636 @param raise_all raise all exceptions instead of ignoring some of them"""
643 lines,tech,logger,raise_all)
645 raise InvalidATCF(
'ATCF CARQ data must contain at least '
646 'one line with forecast hour 0 data.',
652 logger=logger,raise_all=raise_all)
655 logger=logger,raise_all=raise_all)
660 d[
'havefcstloc']=
True
662 except(StormInfoError,KeyError,ValueError,TypeError,
663 AttributeError)
as e:
664 if logger
is not None:
665 logger.warning(
'could not location: %s line: %s'%
666 (str(e),lines[ibig]),exc_info=
True)
669 nameless=set((
'NAMELESS',
'UNKNOWN',
'NONAME',
''))
670 if 'stormname' not in d
or str(d[
'stormname']).upper()
in nameless:
671 d[
'stormname']=
'NAMELESS'
674 require=set((
'basin1',
'stormname',
'lat',
'lon',
'stnum'))
676 if not var
in self.__dict__:
678 'Could not get mandatory field %s from input. '
679 'First line: %s'%(var,lines[0]),lines[0])
680 d[
'stormnamelc']=d[
'stormname'].lower()
682 def _split_carq(self,lines,tech,logger=None,raise_all=True):
683 """!Internal function for parsing CARQ data
685 Do not call this: it is an internal implementation
686 function. It parses an array of CARQ lines from an Aid Deck
687 file. Returns a four-element tuple containing:
688 1. A list of lists (one list per line). The inner lists
689 contain one string per comma separated entry in the line.
690 2. A list of indices of lines that are for forecast hour 0
691 3. The index of the last line that has the latest forecast
692 hour that is not later than 72hrs (needed for generating
693 tcvitals forecast locations).
694 4. The forecast hour for that line.
696 @param lines list of lines of CARQ data
697 @param tech technique name to grep for, usually CARQ, though BEST also
698 works when using B deck files
699 @param logger a logging.Logger for log messages
700 @param raise_all raise all exceptions instead of ignoring some of them"""
701 split=[ line.split(
',')
for line
in lines ]
705 for i
in xrange(len(split)):
706 split[i]=[ x.strip()
for x
in split[i] ]
709 'CARQ entries in deck files must have at least '
710 'eight fields (everything through lat & lon). '
711 'Cannot parse this: %s'%(lines[i],),lines[i])
712 if any([ split[i][j]!=split[0][j]
for j
in [0,1,2,4] ]):
714 'Basin, storm number, YMDH and technique must '
715 'match for ALL LINES when parsing CARQ data in '
716 '_parse_carq.',lines[i])
717 myfhr=int(split[i][5])
721 logger=logger,raise_all=raise_all)
723 if (ibig
is None or myfhr>fhrbig)
and myfhr<=72
and myfhr>0:
727 return (split,izeros,ibig,fhrbig)
729 def _parse_atcf_time(self,data,tech='CARQ',logger=None,raise_all=True):
730 """!Internal function for getting the time out of ATCF data.
732 Do not call this. It is an internal implementation
733 routine. Adds to this StormInfo object the "when" parameter
734 that contains the analysis time. If available, will also add
735 the "technum" technique sort number. The instr is a line of
736 original input text for error messages, the "data" is an
737 output from _split_carq, and the other parameters are inputs
738 to the original constructor.
739 @param data Four element array where the last two are the forecast
741 @param tech technique name to grep for, usually CARQ, though BEST also
742 works when using B deck files
743 @param logger a logging.Logger for log messages
744 @param raise_all raise all exceptions instead of ignoring some of them"""
750 self.__dict__[
'technum']=int(data[3])
752 when=datetime.datetime(
753 year=iwhen/1000000,month=(iwhen/10000)%100,day=(iwhen/100)%100,
754 hour=iwhen%100,minute=imin,second=0,microsecond=0,tzinfo=
None)
755 self.__dict__[
'when']=when
756 self.__dict__[
'YMDH']=when.strftime(
'%Y%m%d%H')
758 def _parse_atcf_radii_seas(self,instr,data,logger=None,raise_all=True):
759 """!Internal function for parsing radii and sea information in ATCF data
761 Do not call this. It is an internal implementation
762 routine. Adds to this StormInfo object radii and sea height
763 data from the given input. The instr is a line of original
764 input text for error messages, the "data" is an output from
765 _split_carq, and the other parameters are inputs to the
766 original constructor.
767 @param instr string to parse
768 @param tech technique name to grep for, usually CARQ, though BEST also
769 works when using B deck files
770 @param logger a logging.Logger for log messages
771 @param raise_all raise all exceptions instead of ignoring some of them"""
786 quadrantinfo(d,qset,irad,windcode,data[13:17],
'',nmi2km)
787 d[
'windcode%02d'%(irad,)]=windcode
788 except (KeyError,ValueError,TypeError,AttributeError)
as e:
789 if logger
is not None:
790 logger.warning(
'could not parse wind radii: %s line: %s'%
791 (str(e),instr),exc_info=
True)
798 quadrantinfo(d,qset,iseas,seascode,data[30:34],
'seas',nmi2km)
799 d[
'seascode%02d'%(iseas,)]=seascode
800 except (KeyError,ValueError,TypeError,AttributeError):
801 if logger
is not None:
803 'could not parse wave height info: %s line: %s'%
804 (str(e),instr),exc_info=
True)
807 def _parse_atcf_meat(self,instr,data,logger=None,raise_all=True):
808 """!Internal function that parses most of a line of ATCF data.
810 Do not call this. It is an internal implementation routine.
811 Parses just about everything except the time, radii and sea
812 height from the input ATCF data. The instr is a line of
813 original input text for error messages, the "data" is an
814 output from _split_carq, and the other parameters are inputs
815 to the original constructor.
816 @param instr string to parse
817 @param data split-up elements of a A deck line
818 @param tech technique name to grep for, usually CARQ, though BEST also
819 works when using B deck files
820 @param logger a logging.Logger for log messages
821 @param raise_all raise all exceptions instead of ignoring some of them"""
822 if logger
is not None and not isinstance(logger,logging.Logger):
824 'in _parse_atcf_meat, logger must be a logging.Logger, '
825 'but instead it is a %s'%(type(logger).__name__))
832 d[
'technique']=data[4]
833 d[
'tau']=int(data[5])
848 except (KeyError,ValueError,TypeError,AttributeError)
as e:
849 if logger
is not None:
850 logger.warning(
'could not parse %s: %s line: %s'%
851 (repr(data[i]),str(e),instr),exc_info=
True)
853 raise InvalidATCF(
'%s: %s: line %s'%(s,str(e),instr),instr)
861 except(KeyError,ValueError,TypeError,AttributeError)
as e:
862 if logger
is not None:
863 logger.warning(
'could not parse %s: %s line: %s'%
864 (repr(data[i]),str(e),instr),exc_info=
True)
866 raise InvalidATCF(
'%s: %s: line %s'%(s,str(e),instr),instr)
867 if n>=9
and data[8]!=
'': fic(
'wmax',8,kts2mps)
868 if n>=10
and data[9]!=
'': fa(
'pmin',9)
869 if n>=11
and data[10]!=
'': d[
'stormtype']=data[10]
870 if n>=18
and data[17]!=
'': fa(
'poci',17)
871 if n>=19
and data[18]!=
'': fic(
'roci',18,nmi2km)
872 if n>=20
and data[19]!=
'': fic(
'rmw',19,nmi2km)
873 if n>=21
and data[20]!=
'': fic(
'gusts',20,kts2mps)
874 if n>=22
and data[21]!=
'': fic(
'eyediam',21,nmi2km)
875 if n>=23
and data[22]!=
'': subregion=data[22]
876 if n>=24
and data[23]!=
'' and data[23]!=
'L': fic(
'maxseas',23,ft2m)
877 if n>=25
and data[24]!=
'': d[
'initials']=data[24]
878 if n>=26
and data[25]!=
'':
883 if n>=27
and data[26]!=
'': fic(
'stormspeed',26,kts2mps)
884 if n>=28
and data[27]!=
'':
885 d[
'stormname']=str(data[27]).upper()
886 if n>=29
and data[28]!=
'': d[
'depth']=str(data[28]).upper()[0]
889 d[
'stormnamelc']=d[
'stormname'].lower()
891 def _parse_message_line(self,instr):
892 """!Do not call this routine directly. Call
893 StormInfo("message",instr) instead.
895 This subroutine parses one line of a hurricane message text
896 that is assumed to be for the current century. The format of
897 a hurricane message is the same as for a tcvitals file, except
898 that the century is omitted and the file is always exactly one
901 int(datetime.datetime.utcnow().year)/100)
902 def _parse_tcvitals_line(self,instr,century=None):
903 """!Parses one line of tcvitals data
905 Do not call this routine directly. Call
906 StormInfo("tcvitals",instr) instead.
908 This subroutine parses one line of a tcvitals file of a format
911 http://www.emc.ncep.noaa.gov/mmb/data_processing/tcvitals_description.htm
913 Here is an example line with only some of the possible data:
915 JTWC 31W HAIYAN 20131104 1200 061N 1483E 270 077 0989 1008 0352 23 064 0084 0074 0074 0084 M ... more stuff ...
918 The resulting data is put in self._data. Note that, at this
919 time, there is one new field not present in the above
920 mentioned webpage. The "storm type parameter" is a two
921 letter description of the type of the storm: LO=low,
922 WV=wave, etc. (there are many possibilities). That field is
923 at the end of the line described in the above link, after
926 The "century" argument is the first two digits of the year,
927 so 19 for the 1900s, 20 for the 2000s and so on. If century
928 is missing or None, and the tcvitals does not specify the
929 century either, then InvalidVitals will be raised. If both
930 are available, the tcvitals century is used."""
932 (?P<center>\S+) \s+ (?P<stnum>\d\d)(?P<rawbasin>[A-Za-z])
933 \s+ (?P<rawstormname>[A-Za-z_ -]+)
934 \s+ (?P<rawcentury>\d\d)? (?P<rawYYMMDD>\d\d\d\d\d\d)
935 \s+ (?P<rawHHMM>\d\d\d\d)
936 \s+ (?P<strlat>-?0*\d+[NS ]) \s+ (?P<strlon>-?0*\d+[EW ])
937 \s+ (?P<stormdir>-?0*\d+)
938 \s+ (?P<stormspeed>-?0*\d+)
939 \s+ (?P<pmin>-?0*\d+)
940 \s+ (?P<poci>-?0*\d+) \s+ (?P<roci>-?0*\d+)
941 \s+ (?P<wmax>-?0*\d+)
943 \s+ (?P<NE34>-?0*\d+) \s+ (?P<SE34>-?0*\d+) \s+ (?P<SW34>-?0*\d+) \s+ (?P<NW34>-?0*\d+)
944 (?: \s+ (?P<depth>\S)
946 \s+ (?P<NE50>-?0*\d+) \s+ (?P<SE50>-?0*\d+) \s+ (?P<SW50>-?0*\d+) \s+ (?P<NW50>-?0*\d+)
949 \s+ (?P<fstrlat>-?0*\d+[NS ])
950 \s+ (?P<fstrlon>-?0*\d+[EW ])
952 \s+ (?P<NE64>-?0*\d+) \s+ (?P<SE64>-?0*\d+) \s+ (?P<SW64>-?0*\d+) \s+ (?P<NW64>-?0*\d+)
953 (?: \s+ (?P<stormtype>\S\S?) )?
959 raise InvalidVitals(
'Cannot parse vitals: %s'%(repr(instr),),instr)
962 noneok=set((
'NE50',
'SE50',
'SW50',
'NW50',
'fhr',
'fstrlat',
'fstrlon',
963 'NE64',
'SE64',
'SW64',
'NW64',
'stormtype',
'rawcentury',
967 raws=set((
'rawbasin',
'rawstormname',
'rawcentury',
'rawYYMMDD',
'rawHHMM',
971 latlons=set((
'strlat',
'fstrlat',
'strlon',
'fstrlon'))
974 stripme=set((
'center',
'stormtype'))
980 float1=set((
'stormdir',
'pmin',
'poci',
'roci',
'wmax',
'rmw',
'fhr'))
981 float1radii=set((
'NE34',
'SE34',
'SW34',
'NW34',
982 'NE50',
'SE50',
'SW50',
'NW50',
983 'NE64',
'SE64',
'SW64',
'NW64'))
986 float10=set((
'stormspeed',))
994 mdict=dict(m.groupdict())
996 for k,v
in mdict.iteritems():
998 if k
in noneok:
continue
1000 'Mandatory variable %s had None value in line: %s'%
1001 (str(k),repr(instr)), instr )
1003 if k
in raws: d[k] = v
1004 elif k
in stripme: d[k] = str(v).strip()
1005 elif k
in float1: d[k] = float(v.strip())
1006 elif k
in float10: d[k] = float(v.strip())/10.0
1007 elif k
in int1: d[k] = int(v.strip())
1008 elif k
in float1radii:
1009 val=float(v.strip())
1010 d[k]=float(v.strip())
1011 if val>0: qset.add(k)
1014 if repl
is None and not k
in noneok:
1016 'Mandatory variable %s had invalid value %s '
1018 (str(k),str(v),repr(instr)), instr )
1019 d[k.replace(
'str',
'')]=repl
1020 except ValueError
as e:
1022 'Cannot parse vitals key %s value %s: %s from line %s'%
1023 ( str(k),repr(v),str(e),repr(instr) ), instr )
1028 'NE34' in d
and 'SE34' in d
and 'SW34' in d
and 'NW34' in d
and \
1029 ( d[
'NE34']>0
or d[
'SE34']>0
or d[
'SW34']>0
or d[
'NW34']>0 ) )
1032 d[
'havefcstloc'] =
'flat' in d
and 'flon' in d
and \
1033 d[
'flat']
is not None and d[
'flon']
is not None
1036 if 'rawstormname' in d:
1037 d[
'stormname']=d[
'rawstormname'].strip().upper()
1040 'No storm name detected in this line: %s'%(instr,),instr)
1044 if 'rawYYMMDD' in d
and 'rawHHMM' in d:
1045 if 'rawcentury' in d:
1046 icentury=int(d[
'rawcentury'])
1048 icentury=int(current_century)
1049 if(icentury<16
or icentury>20):
1052 'Implausable tcvitals century %d. Require '
1053 '16 through 20.'%(icentury,))
1054 sdate=
'%02d%06d%02d'%(icentury,int(d[
'rawYYMMDD']),
1055 int(d[
'rawHHMM'])/100)
1060 'from vitals: %s'%(repr(instr),),instr)
1074 d[
'stormnamelc']=d[
'stormname'].lower()
1078 """!Sets the two letter storm type self.stormtype.
1080 @param discardold If discardold=False (the default), then the
1081 old value, if any, is moved to self.old_stormtype.
1082 @param stormtype the storm type information"""
1083 if 'stormtype' in self.__dict__
and not discardold:
1084 self.__dict__[
'old_stormtype']=self.
stormtype
1085 if isinstance(stormtype,basestring):
1088 self.
stormtype=getattr(stormtype,
'stormtype',
'XX')
1095 """!Sets the name of the storm.
1097 @param newname the new storm name
1098 @param discardold If discardold=False (the default) then the
1099 old storm name is moved to self.old_stormname."""
1100 if 'stormname' in self.__dict__
and not discardold:
1101 self.__dict__[
'old_stormname']=self.
stormname
1104 self.
line=
'%s%-9s%s' % (self.
line[0:9],
1106 self.__dict__[
'stormnamelc']=self.stormname.lower()
1109 """!Changes the storm number.
1111 Changes the storm number: the 09 in 09L. That changes
1112 self.stnum, stormid3, stormid3lc, stormid4 and longstormid.
1114 @param newnumber the new storm number
1115 @param discardold If discardold=False (the default), then the old values
1116 are moved to the old_stnum, old_stormid3, etc."""
1117 if 'stnum' in self.__dict__
and not discardold:
1118 self.__dict__[
'old_stnum']=self.
stnum
1119 self.__dict__[
'old_stormid3']=self.
stormid3
1120 self.__dict__[
'old_stormid3lc']=self.
stormid3lc
1121 self.__dict__[
'old_stormid4']=self.
stormid4
1128 if self.
lat<0
and self.when.month<7:
1141 """!Swaps the new and old stormid variables. The stnum and
1142 old_stnum are swapped, the stormid3 and old_stormid3 are
1143 swapped, and so on."""
1145 if o
in self.__dict__
and n
in self.__dict__:
1146 keep=self.__dict__[o]
1147 self.__dict__[o]=self.__dict__[n]
1148 self.__dict__[n]=keep
1149 swapname(
'old_stnum',
'stnum')
1150 swapname(
'old_stormid3',
'stormid3')
1151 swapname(
'old_stormid3lc',
'stormid3lc')
1152 swapname(
'old_stormid4',
'stormid4')
1153 swapname(
'old_longstormid',
'longstormid')
1159 """!Returns a tcvitals version of this data. This is not
1160 cached, and will be recalculated every time it is called."""
1164 """!Returns a message line version of this data. This is not
1165 cached, and will be recalculated every time it is called."""
1169 """!Internal function that underlies as_tcvitals() and as_message()
1171 Returns a tcvitals or message version of this data. This is
1172 not cached, and will be recalculated every time it is called.
1173 @param no_century If no_century=True, then only two digits of the year are
1174 written, and the line will be a message."""
1177 if s
not in d:
return True
1179 if val
is None:
return True
1182 if s
not in d:
return True
1184 if val
is None:
return True
1186 def cint(i):
return int(abs(round(i)))
1188 datestring=
'%y%m%d %H%M'
1190 datestring=
'%Y%m%d %H%M'
1191 result=
'%-4s %02d%s %-9s %s %03d%s %04d%s %03d %03d %04d ' \
1192 '%04d %04d %02d %03d %04d %04d %04d %04d' % (
1194 str(self.basin1[0]),
1195 str(self.
stormname)[0:9], self.when.strftime(datestring),
1196 min(900,cint(self.
lat*10.0)), (
'N' if(self.
lat>0)
else 'S' ),
1197 min(3600,cint(self.
lon*10.0)), (
'E' if(self.
lon>0)
else 'W' ),
1198 -99
if (bad(
'stormdir'))
else min(360,cint(self.
stormdir)),
1199 -99
if (bad(
'stormspeed'))
else \
1201 -999 if(bad0(
'pmin'))
else min(1100,cint(self.
pmin)),
1202 -999 if(bad0(
'poci'))
else min(1100,cint(self.
poci)),
1203 -99 if(bad(
'roci'))
else min(9999,cint(self.
roci)),
1204 -9 if(bad(
'wmax'))
else min(99,cint(self.
wmax)),
1205 -99 if(bad(
'rmw'))
else min(999,cint(self.
rmw)),
1206 -999 if(bad0(
'NE34'))
else min(9999,cint(self.
NE34)),
1207 -999 if(bad0(
'SE34'))
else min(9999,cint(self.
SE34)),
1208 -999 if(bad0(
'SW34'))
else min(9999,cint(self.
SW34)),
1209 -999 if(bad0(
'NW34'))
else min(9999,cint(self.
NW34)))
1211 gotfcst=
'fhr' in d
and 'flon' in d
and \
1212 'flat' in d
and self.
fhr is not None and\
1213 self.
flat is not None and \
1214 self.
flon is not None and self.
fhr>0
1215 gotst=(
'stormtype' in d )
1217 if 'depth' not in d
and not gotst
and not gotfcst:
1219 result=
'%s %s'%(result,str(getattr(self,
'depth',
'X'))[0])
1221 if 'NW50' not in d
and not gotst
and not gotfcst:
1223 result=
'%s %04d %04d %04d %04d'%(
1225 -999 if(bad0(
'NE50'))
else min(9999,cint(self.
NE50)),
1226 -999 if(bad0(
'SE50'))
else min(9999,cint(self.
SE50)),
1227 -999 if(bad0(
'SW50'))
else min(9999,cint(self.
SW50)),
1228 -999 if(bad0(
'NW50'))
else min(9999,cint(self.
NW50)))
1231 result=
'%s %02d %03d%s %04d%s'%(
1233 -9 if(self.
fhr<0)
else min(99,int(self.
fhr)),
1234 min(900,cint(self.
flat*10.0)),
1235 (
'N' if(self.
flat>0)
else 'S' ),
1236 min(3600,cint(self.
flon*10.0)),
1237 (
'E' if(self.
flon>0)
else 'W' ))
1239 if 'NW64' not in d
and not gotst:
return result
1241 if not gotfcst: result+=
' -9 -99N -999W'
1243 result=
'%s %04d %04d %04d %04d'%(
1245 -999 if(bad0(
'NE64'))
else min(9999,cint(self.
NE64)),
1246 -999 if(bad0(
'SE64'))
else min(9999,cint(self.
SE64)),
1247 -999 if(bad0(
'SW64'))
else min(9999,cint(self.
SW64)),
1248 -999 if(bad0(
'NW64'))
else min(9999,cint(self.
NW64)))
1250 if not gotst:
return result
1252 return '%s % 2s'%(result,str(self.
stormtype)[0:2])
1255 """!Changes the basin of this StormInfo
1256 @param basin the primary basin (IO, L, etc.)
1257 @param subbasin the subbasin. For example, IO has the subbasins AA and BB.
1258 @param discardold If discardold=False (the default), then the old values
1259 are moved to the old_stnum, old_stormid3, etc. """
1261 self.__dict__[
'old_hwrfbasin2']=self.hwrfbasin2
1262 self.__dict__[
'old_pubbasin2']=self.pubbasin2
1263 self.__dict__[
'old_basin1']=self.basin1
1264 self.__dict__[
'old_basin1lc']=self.basin1lc
1265 self.__dict__[
'old_basinname']=self.basinname
1267 if self.
format==
'tcvitals':
1268 self.
line=
'%s%s%s'%(self.
line[0:7],self.basin1[0],self.
line[8:])
1271 def _set_basin(self,basin,subbasin=None,discardold=False):
1272 """!This is a utility function that creates the one and two
1273 letter basins from a raw one and/or two letter basin. If the
1274 input basin is invalid, InvalidBasinError is raised.
1276 @param basin the primary basin (IO, L, etc.)
1277 @param subbasin the subbasin. For example, IO has the subbasins AA and BB.
1278 @param discardold If discardold=False (the default), then the old values
1279 are moved to the old_stnum, old_stormid3, etc. """
1281 self.__dict__[
'hwrfbasin2']=bb[0]
1282 self.__dict__[
'pubbasin2']=bb[1]
1283 self.__dict__[
'basin1']=bb[2].upper()
1284 self.__dict__[
'basin1lc']=bb[2].lower()
1285 self.__dict__[
'basinname']=bb[3]
1287 def __doxygen(self):
1288 """!Ensure that self.varname exists for all member variables,
1289 so that Doxygen detects them"""
1516 """!Converts basin identifiers
1518 Given a one-letter or two-letter tropical basin identifier, and
1519 possibly another one-letter tropical basin identifier (subbasin),
1520 attempts to determine more information about the basin. Some
1521 information may be ambiguous if a two letter basin is specified.
1522 Returns a four-element tuple:
1524 1. The internal (HWRF/GFDL) two-letter basin identifier. These
1525 have an unambiguous mapping to the one-letter basin.
1527 2. The public, standard two-letter basin identifier used by JTWC
1528 and others. These are ambiguous: IO can be A or B, and SH can
1531 3. The one-letter basin identifier.
1533 4. A description of the meaning of the basin.
1535 @param basin the primary basin
1536 @param subbasin Optional: the subbasin, if known"""
1537 b=str(basin).upper()
1538 s=
'' if(subbasin
is None)
else str(subbasin).upper()
1539 if b==
'AL' or b==
'L': bb=(
'AL',
'AL',
'L',
1540 'North Atlantic (L/AL)' )
1541 elif b==
'SL' or b==
'Q': bb=(
'SL',
'SL',
'Q',
1542 'South Atlantic (Q/SL/LS)' )
1543 elif b==
'LS' : bb=(
'LS',
'LS',
'Q',
1544 'South Atlantic (Q/SL/LS)' )
1545 elif b==
'EP' or b==
'E': bb=(
'EP',
'EP',
'E',
1546 'North East Pacific (E/EP)' )
1547 elif b==
'CP' or b==
'C': bb=(
'CP',
'CP',
'C',
1548 'North Central Pacific (C/CP)' )
1549 elif b==
'SS' or b==
'S': bb=(
'SS',
'SH',
'S',
1550 'South Pacific (S/SH)' )
1551 elif b==
'PP' or b==
'P': bb=(
'PP',
'SH',
'P',
1552 'South Indian Ocean (P/SH/PP)' )
1553 elif b==
'AA' or b==
'A': bb=(
'AA',
'IO',
'A',
1554 'Indian Ocean: Arabian Sea (A/IO/AA)' )
1555 elif b==
'NA' or b==
'A': bb=(
'NA',
'IO',
'A',
1556 'Indian Ocean: Arabian Sea (A/IO/NA)' )
1557 elif b==
'BB' or b==
'B': bb=(
'BB',
'IO',
'B',
1558 'Indian Ocean: Bay of Bengal (B/IO/BB)' )
1560 if s==
'O': bb=(
'OO',
'WP',
'O',
1561 'North West Pacific: South China Sea Basin (O/W/WP)' )
1562 elif s==
'T': bb=(
'TT',
'WP',
'T',
1563 'North West Pacific: East China Sea (T/W/WP)' )
1565 else: bb=(
'WP',
'WP',
'W',
1566 'North West Pacific (W/WP)' )
1567 elif b==
'W': bb=(
'WP',
'WP',
'W',
1568 'North West Pacific (W/WP)' )
1570 if s==
'S': bb=(
'SS',
'SH',
'S',
1571 'South Pacific (S/SH)' )
1572 elif s==
'P': bb=(
'PP',
'SH',
'P',
1573 'South Indian Ocean (P/SH)' )
1574 elif s==
'U': bb=( 'UU', 'SH', '
U',
1575 'South Pacific: Australian Basin (U/P/S/SH)' )
1576 else: bb=(
'SH',
'SH',
'S',
1577 'South Pacific or South Indian Ocean (SH)' )
1579 if s==
'A': bb=(
'AA',
'IO',
'A',
1580 'Indian Ocean: Arabian Sea (A/IO)' )
1581 elif s==
'B': bb=(
'BB',
'IO',
'B',
1582 'Indian Ocean: Bay of Bengal (B/IO)' )
1583 else: bb=(
'IO',
'IO',
'B',
1584 'Unspecified North Indian Ocean (IO)' )
1587 elif b==
'U': bb=( 'UU', 'SH', '
U',
1588 'South Pacific: Australian Basin (U/P/S/SH)' )
1589 elif b==
'O': bb=(
'OO',
'WP',
'O',
1590 'North West Pacific: South China Sea Basin (O/W/WP)' )
1591 elif b==
'T': bb=(
'TT',
'WP',
'T',
1592 'North West Pacific: East China Sea (T/W/WP)' )
def clean_up_vitals
Given a list of StormInfo, sorts using the vitcmp comparison, discards suspect storm names and number...
def name_number_okay(vl)
Given an array of StormInfo objects, iterate over those that have valid names and numbers...
NW50
NW quadrant 50kt wind radius.
fstrlat
Original flat string.
def swap_numbers(self)
Swaps the new and old stormid variables.
def hwrf_domain_center
Uses the 2013 operational HWRF method of deciding the domain center based on the storm location...
wmax
Maximum wind as a float.
flat
Forecast latitude in degrees North.
def expand_basin
Converts basin identifiers.
def to_timedelta
Converts an object to a datetime.timedelta.
old_stormid3lc
stormid3lc before Invest renumbering
old_stormid4
stormid4 before Invest renumbering
def __init__
StormInfo constructor.
def basin_center_okay(vl)
Given a list of StormInfo objects, iterates over those that have the right basins for the right cente...
def _set_basin
This is a utility function that creates the one and two letter basins from a raw one and/or two lette...
old_stormtype
stormtype before Invest renumbering
longstormid
Storm basin, number and year: AL092012.
Constants used throughout the hwrf package.
roci
Radius of the outermost closed isobar.
def to_fraction
Converts an object or two to a fraction.
def to_datetime_rel(d, rel)
Converts objects to a datetime relative to another datetime.
def _parse_atcf_time
Internal function for getting the time out of ATCF data.
rawbasin
Raw parser data for the basin.
center
The forecast center (RSMC) whose forecaster provided this information.
lon
Storm center longitude in degrees East, a float.
rawYYMMDD
Raw parser data for the date.
qset
Set of quadrant information keys.
windcode34
Code sent for 34kt wind radii.
NE50
NE quadrant 50kt wind radius.
def as_tcvitals_or_message
Internal function that underlies as_tcvitals() and as_message()
def old(self)
Returns a copy of this StormInfo, but with the last renumbering or renaming of the vitals undone...
def __str__(self)
Return a human-readable string representation of this error.
SW64
SW quadrant 64kt wind radius.
def __add__(self, amount)
Returns a copy of this object, with the vitals extrapolated forward "amount" hours.
def floatlatlon
Converts a string like "551N" to 55.1, correctly handling the sign of each hemisphere.
Raised when a syntax error is found in the tcvitals, and the code cannot guess what the operator inte...
NW34
NW quadrant 34kt wind radius.
stormname
Upper-case storm name.
def parse_tcvitals
Reads data from a tcvitals file.
when
The datetime.datetime for the valid time.
fstrlon
Original flon string.
eyediam
Eye diameter from the tcvitals.
format
The linetype argument to the constructor.
flon
Forecast longitude in degrees East.
rawstormname
Raw parser data for the storm name.
subbasin
The problematic subbasin, or None if no subbasin was given.
rawcentury
Raw parser data for the century.
tau
Tau value from the ATCF.
old_longstormid
longstormid before Invest renumbering
rawHHMM
Raw parser data for the time.
def rename_storm
Sets the name of the storm.
NW64
NW quadrant 64kt wind radius.
technique
Technique field for ATCF.
stnum
Storm number: 1-49 for real storms, 0 for fake basin-scale, 50-79 for RSMC internal usage...
YMDH
The ten digit date and time of the valid time.
depth
Storm depth: S, M or D; or X for missing.
has_old_stnum
If True, the various old_* variables are present.
NE64
NE quadrant 64kt wind radius.
havefcstloc
If True, the fhr, flat and flon are provided.
maxseas
Maximum sea height as a float.
def _parse_message_line(self, instr)
Do not call this routine directly.
def to_datetime(d)
Converts the argument to a datetime.
This exception is raised when the StormInfo class receives an invalid tcvitals line or ATCF line that...
Time manipulation and other numerical routines.
stormspeed
Storm movement speed from tcvitals, m/s float.
Base class of all exceptions in this module.
def __init__
InvalidBasinError constructor.
lat
Storm center latitude in degrees North, a float.
windcode64
Code sent for 64kt wind radii.
stormid3
Three character storm ID from tcvitals: 09L, 91S, 18P, etc.
def renumber_storm
Changes the storm number.
def vitcmp(a, b)
A cmp comparison for StormInfo objects intended to be used with sorted().
pmin
Minimum pressure as a float.
This exception is raised when an invalid Tropical Cyclone basin is found.
stormid4
Four character storm ID: AL09, SH91, IO18, etc.
def storm_key(vit)
Generates a hashable key for hashing StormInfo objects.
stormtype
Two character best track storm type.
stormid3lc
Three character storm ID from tcvitals: 09l, 91s, 18p, etc.
stormdir
Storm movement direction from tcvitals, degrees.
def set_stormtype
Sets the two letter storm type self.stormtype.
lines
Multi-line input to the constructor.
This is the base class of all exceptions raised when errors are found in the tcvitals, Best Track, Aid Deck or other storm information databases.
technum
Technique number for ATCF.
poci
Pressure of the outermost closed isobar.
old_stormname
stormname before Invest renumbering
SW50
SW quadrant 50kt wind radius.
def change_basin
Changes the basin of this StormInfo.
SE34
SE quadrant 34kt wind radius.
SE64
SE quadrant 64kt wind radius.
NE34
NE quadrant 34kt wind radius.
def parse_carq
Scans an A deck file connected to stream-like object fd, reading it into a list of StormInfo objects...
Raised when an implausible century is found.
def vit_cmp_by_storm(a, b)
A cmp comparison for StormInfo objects intended to be used with sorted().
basin
The problematic basin.
Exceptions raised by the hwrf package.
strlon
Original string version of lon.
SW34
SW quadrant 34kt wind radius.
def copy(self)
Returns a copy if this object.
This should be raised when the user requests a specific storm or cycle of a storm and no such vitals ...
strlat
Original string version of lat.
line
Contents of the line of text sent to init
def _parse_atcf_meat
Internal function that parses most of a line of ATCF data.
SE50
SE quadrant 50kt wind radius.
def _parse_tcvitals_line
Parses one line of tcvitals data.
def __repr__(self)
Return a Pythonic representation of this error.
def find_tcvitals_for
Faster way of finding tcvitals data for a specific case.
def as_message(self)
Returns a message line version of this data.
windcode50
Code sent for 50kt wind radii.
def _parse_atcf_radii_seas
Internal function for parsing radii and sea information in ATCF data.
def __sub__(self, amount)
Same as self + (-amount)
def _split_carq
Internal function for parsing CARQ data.
stormnamelc
Lower-case storm name.
def _parse_carq
Given an array of lines from a CARQ entry in an ATCF Aid Deck file, parses the data and adds it to th...
def quadrantinfo
Internal function that parses wind or sea quadrant information.
old_stnum
stnum before Invest renumbering
Raised when invalid ATCF data is found.
def __init__(self, message, badline)
InvalidStormInfoLine constructor.
Storm vitals information from ATCF, B-deck, tcvitals or message files.
old_stormid3
stormid3 before Invest renumbering
rmw
Radius of the maximum wind.
def as_tcvitals(self)
Returns a tcvitals version of this data.
badline
The line at which the problem happened.