HWRF  trunk@4391
storminfo.py
1 """!Defines StormInfo and related functions for interacting with
2 vitals ATCF data.
3 
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
9 hwrf.revital module.
10 
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."""
18 
19 ##@var __all__
20 # List of symbols exported by "from hwrf.storminfo import *"
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',
27  'expand_basin' ]
28 
29 import re, datetime, math, fractions, logging, copy
31 from hwrf.exceptions import HWRFError
32 import pdb
33 
34 ##@var current_century
35 # The first two digits of the year: the thousands and hundreds
36 # digits. This is used to convert message files to tcvitals.
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."""
40 
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."""
45 
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)."""
50  def __init__(self,basin,subbasin=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)"""
56  self.basin=basin
57  self.subbasin=subbasin
58  ##@var basin
59  # The problematic basin
60 
61  ##@var subbasin
62  # The problematic subbasin, or None if no subbasin was given. For
63  # example, the North Indian Ocean (IO) is split into subbasins
64  # Arabian Sea (AA) and Bay of Bengal (BB)
65  def __str__(self):
66  """!Return a human-readable string representation of this error"""
67  if self.subbasin is None:
68  return 'Invalid basin identifier %s'%(repr(self.basin),)
69  else:
70  return 'Invalid basin identifier %s (subbasin %s)' \
71  %(repr(self.basin),repr(self.subbasin))
72  def __repr__(self):
73  """!Return a Pythonic representation of this error."""
74  return '%s(%s,%s)'%(type(self).__name__,repr(self.basin),
75  repr(self.subbasin))
76 
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."""
81  def __init__(self,message,badline):
82  """!InvalidStormInfoLine constructor
83 
84  @param message the error message
85  @param badline line at which the problem happened"""
86  self.badline=badline
87  super(InvalidStormInfoLine,self).__init__(message)
88 
89  ##@var badline
90  # The line at which the problem happened.
91 
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
103  modules."""
104 
106  """!Given an array of StormInfo objects, iterate over those that
107  have valid names and numbers. Discards TEST, UNKNOWN, and numbers
108  50-89
109  @param vl An iterable of StormInfo objects to consider."""
110  for vital in vl:
111  if vital.stormname=='TEST' or vital.stormname=='UNKNOWN' or \
112  (vital.stnum>50 and vital.stnum<90):
113  continue
114  yield vital
115 
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
122 
123  @param vl An iterable of StormInfo objects to consider."""
124  okay={ 'JTWC': 'ABWSPECQ', 'NHC': 'ELCQ' }
125  for vital in vl:
126  center=vital.center
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 # should be in S hemisphere but is not
130  else:
131  if vital.lat<0: continue # should be in N hemisphere but is not
132  yield vital
133 
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.
138 
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").
144 
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)
149  return c
150 
151 def vitcmp(a,b):
152  """!A cmp comparison for StormInfo objects intended to be used with
153  sorted().
154 
155  @param a,b StormInfo objects to order.
156  Uses the following method:
157 
158  1. Sort numerically by date/time.
159  2. Break ties by a reverse sort by stormid. This places Invest
160  (90s) first.
161  3. Break ties by ASCII lexical sort by center (ie.: JTWC first,
162  NHC second)
163  4. Break ties by placing vitals WITH 34kt wind radii after those
164  without.
165  5. Break ties by placing vitals with a full line (through 64kt
166  radii) last
167  6. Break ties by retaining original order ("stable sort")."""
168  c=cmp(a.when,b.when)
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)
172  return c
173 
174 def storm_key(vit):
175  """!Generates a hashable key for hashing StormInfo objects
176 
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.
182 
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)
186 
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"""
203 
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:
209  vitals_cmp=vitcmp
210 
211  # Sort vitals using above described method:
212  sortvitals=sorted(vitals,cmp=vitals_cmp)
213  vitals=sortvitals
214 
215  # Discard suspect storm names and numbers:
216  nn_ok_vitals=[v for v in name_number_checker(vitals)]
217  vitals=nn_ok_vitals
218 
219  # Discard wrong center/basin lines (ie.: NHC W basin, JTWC L basin)
220  keepvitals=[ v for v in basin_center_checker(vitals) ]
221  vitals=keepvitals
222 
223  # Remove duplicate entries keeping the last:
224  revuniqvitals=list()
225  seen=set()
226  for vital in reversed(vitals):
227  key=storm_key(vital)
228  if key in seen: continue
229  seen.add(key)
230  revuniqvitals.append(vital)
231  uniqvitals=[x for x in reversed(revuniqvitals)]
232  return uniqvitals
233 
234 def floatlatlon(string,fact=10.0):
235  """!Converts a string like "551N" to 55.1, correctly handling the sign of
236  each hemisphere.
237 
238  @code
239  floatlatlon(string="311N",fact=10.0) = float(31.1) # degrees North
240  @endcode
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
244  a positive number
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
250  return value. """
251  m=re.search('\A(?P<num>0*\d+)(?:(?P<positive>[NnEe ])|(?P<negative>[SsWw]))\Z',string)
252  if m:
253  mdict=m.groupdict()
254  latlon=float(mdict['num'])/fact
255  if 'negative' in mdict and mdict['negative'] is not None: latlon=-latlon
256  return latlon
257  return None
258 
259 def quadrantinfo(data,qset,irad,qcode,qdata,what='',conversion=1.0):
260  """!Internal function that parses wind or sea quadrant information.
261 
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
268  wind radius.
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. """
274  #print(repr(qdata))
275  assert(len(qdata)==4)
276  quadrant=['NNQ', 'NEQ', 'EEQ', 'SEQ', 'SSQ', 'SWQ', 'WWQ', 'NWQ']
277  irad=int(irad)
278  fqdata=[None]*4
279  maxdata=0.
280  for i in xrange(4):
281  if qdata[i] is not None and qdata[i]!='':
282  fqdata[i]=float(qdata[i])
283  if fqdata[i]<0:
284  fqdata[i]=-999
285  else:
286  fqdata[i]*=conversion
287  maxdata=max(maxdata,fqdata[i])
288  if qcode=='AAA':
289  if count>0:
290  var='%sCIRCLE%d'%(what,irad)
291  data[var]=maxdata
292  qset.add(var)
293  elif qcode=='':
294  return
295  else:
296  iquad=quadrant.index(qcode)
297  for i in xrange(4):
298  var='%s%s%d'%(what,quadrant[(iquad+2*i)%len(quadrant)][0:2],irad)
299  data[var]=fqdata[i]
300  qset.add(var)
301 
302 def parse_tcvitals(fd,logger=None,raise_all=False):
303  """!Reads data from a tcvitals file.
304 
305  This reads line by line from the given file object fd, parsing
306  tcvitals.
307 
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."""
315  out=list()
316  for line in fd:
317  try:
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))
322  if raise_all: raise
323  return out
324 
325 def find_tcvitals_for(fd,logger=None,raise_all=False,when=None,
326  stnum=None,basin1=None):
327  """!Faster way of finding tcvitals data for a specific case.
328 
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.
333 
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)
341 
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))
347  if when is not None:
348  strwhen=hwrf.numerics.to_datetime(when).strftime('%Y%m%d %H%M')
349  abcd='abcd'
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,))
361  #---------------------------------
362  # JTWC 05P FREDA 20130101 0000
363  # NHC 99L ABCDEFGHI 20991234 1200
364  #---------------------------------
365  # 00000000001111111111222222222233
366  # 01234567890123456789012345678901
367  #---------------------------------
368  count=0
369  for line in fd:
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
373  count+=1
374  yield StormInfo('tcvitals',line.rstrip('\n'))
375  if(logger is not None):
376  logger.debug('VITALS: yielded %d matches'%(count,))
377 
378 def parse_carq(fd,logger=None,raise_all=True):
379  """!Scans an A deck file connected to stream-like object fd,
380  reading it into a list of StormInfo objects. Returns the list.
381 
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 """
386  out=list()
387  when=None
388  store=list()
389  for line in fd:
390  if len(line)<40: # blank or error line
391  if logger is not None:
392  logger.warning('Ignoring short line (<40 chars): %s'%(line,))
393  continue
394  elif 'CARQ' in line:
395  when2=line[0:40].split(',')[2].strip()
396  if when is None or when==when2:
397  when=when2
398  store.append(line)
399  continue
400  elif when is None:
401  continue
402  try:
403  out.append(StormInfo(linetype='carq',inputs=store,logger=logger,
404  raise_all=raise_all))
405  store=list()
406  when=None
407  except(StormInfoError,ValueError) as e:
408  if raise_all: raise
409  return out
410 
411 class StormInfo(object):
412  """!Storm vitals information from ATCF, B-deck, tcvitals or message files.
413 
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.
420 
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.
426 
427  @todo Write a separate class for simple manipulations of a whole
428  ATCF database. This could be done efficiently using an in-memory
429  sqlite3 database."""
430 
431  ##@var format
432  # The linetype argument to the constructor. This is the format
433  # of the input data: tcvitals, message or carq (atcf).
434 
435  ##@var line
436  # Contents of the line of text sent to __init__
437 
438  ##@var lines
439  # Multi-line input to the constructor
440 
441  def __init__(self,linetype,inputs,carq='CARQ',logger=None,raise_all=True):
442  """!StormInfo constructor
443 
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.
447 
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
454  errors happen
455 
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
464  id/name.
465  * @c copy --- Do a deep copy of the supplied StormInfo"""
466  if logger is not None and not isinstance(logger,logging.Logger):
467  raise TypeError(
468  'In StormInfo constructor, logger must be a '
469  'logging.Logger, but instead it is a %s'
470  %(type(logger).__name__))
471  super(StormInfo,self).__init__()
472  self._cenlo=None
473  self._cenla=None
474  self.format=linetype
475  self.has_old_stnum=False
476  if linetype=='tcvitals':
477  self.line=str(inputs)
478  self._parse_tcvitals_line(self.line)
479  elif linetype=='message':
480  self.line=str(inputs)
481  self._parse_message_line(self.line)
482  elif linetype=='carq':
483  self.lines=copy.copy(inputs)
484  self._parse_carq(lines=inputs,tech=str(carq),logger=logger,
485  raise_all=bool(raise_all))
486  elif linetype=='old' or linetype=='copy':
487  old=linetype=='old'
488  def checktype(var):
489  for t in ( basestring, int, float, datetime.datetime,
490  datetime.timedelta ):
491  if isinstance(var,t): return True
492  return False
493  if not isinstance(inputs,StormInfo):
494  raise TypeError(
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
502  self.__dict__[k]=v
503  if old:
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
507  else:
508  raise InvalidStormInfoFormat(
509  'Unknown storm info format %s: only know "tcvitals" '
510  'and "message".'%(repr(linetype),))
511  def old(self):
512  """!Returns a copy of this StormInfo, but with the last
513  renumbering or renaming of the vitals undone."""
514  return StormInfo('old',self)
515  def copy(self):
516  """!Returns a copy if this object."""
517  return StormInfo('copy',self)
518  def __sub__(self,amount):
519  """!Same as self + (-amount)
520  @param amount The amount of time to extrapolate backwards."""
521  return self+ (-amount)
522  def __add__(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"""
526  copy=self.copy()
527  dtamount=hwrf.numerics.to_timedelta(amount*3600)
528  vmag=max(0,copy.stormspeed)*10.0
529  pi180=math.pi/180.
530  Rearth=hwrf.constants.Rearth
531  dt=hwrf.numerics.to_fraction(dtamount,negok=True)
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
538  copy.when=hwrf.numerics.to_datetime_rel(amount*3600,copy.when)
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
543  #logging.debug('vmag=%s dt=%s dx=%s dy=%s dlat=%s dlon=%s'%(
544  # repr(vmag),repr(dt),repr(dx),repr(dy),repr(dlat),repr(dlon)))
545  return copy
546  def hwrf_domain_center(self,logger=None):
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"""
555 
556  if self._cenlo is not None and self._cenla is not None:
557  return (self._cenlo,self._cenla)
558 
559  # This strange-looking sequence of comparisons and coercion to
560  # irrelevant types mimics the weird combination of awk, bc and
561  # ksh calculations used in the 2013 HWRF. The latitude and
562  # longitude cutoffs prevent crashes of the HWRF relocation
563  # system when the storm approaches lateral boundaries, and
564  # beyond that, they have also had a positive impact on track
565  # forecasting skill.
566 
567  storm_lon=self.lon
568  assert(storm_lon is not None)
569  storm_lat=self.lat
570  if self.havefcstloc:
571  assert(self.flon is not None)
572  avglon=self.flon
573  else:
574  avglon=storm_lon-20.0
575  assert(avglon is not None)
576 
577  # Decide center latitude.
578  cenla=storm_lat
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
589 
590  # Decide the center longitude.
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
599  cenlo=result
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))
603  # Lastly, some sanity checks to avoid outer domain centers too
604  # far from storm centers:
605  moved=False
606  if(int(cenlo)>int(storm_lon)+5):
607  cenlo=storm_lon+5.0
608  if logger is not None:
609  logger.info(
610  'Center is too far east of storm. Moving it to %f'
611  %(cenlo,))
612  moved=True
613  if(int(cenlo)<int(storm_lon)-5):
614  cenlo=storm_lon-5.0
615  if logger is not None:
616  logger.info(
617  'Center is too far west of storm. Moving it to %f'
618  %(cenlo,))
619  moved=True
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'
623  %(cenlo,cenla))
624  # Return results as a tuple:
625  ( self._cenlo, self._cenla ) = ( cenlo, cenla )
626  return ( cenlo, cenla )
627 
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"""
637  d=self.__dict__
638  d['center']=tech
639  # STEP 1: Split all lines at commas, trim white space from
640  # each entry, and make sure everything is for the same storm
641  # and time.
642  (split,izeros,ibig,fhrbig) = self._split_carq(
643  lines,tech,logger,raise_all)
644  if not izeros:
645  raise InvalidATCF('ATCF CARQ data must contain at least '
646  'one line with forecast hour 0 data.',
647  lines[0])
648  first=True
649  for izero in izeros:
650  if first:
651  self._parse_atcf_meat(lines[izero],split[izero],
652  logger=logger,raise_all=raise_all)
653  first=False
654  self._parse_atcf_radii_seas(lines[izero],split[izero],
655  logger=logger,raise_all=raise_all)
656  if ibig is not None:
657  try:
658  d['flat']=floatlatlon(split[ibig][6])
659  d['flon']=floatlatlon(split[ibig][7])
660  d['havefcstloc']=True
661  d['fhr']=fhrbig
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)
667  if raise_all: raise
668 
669  nameless=set(('NAMELESS','UNKNOWN','NONAME',''))
670  if 'stormname' not in d or str(d['stormname']).upper() in nameless:
671  d['stormname']='NAMELESS'
672 
673  # Check for mandatory variables:
674  require=set(('basin1','stormname','lat','lon','stnum'))
675  for var in require:
676  if not var in self.__dict__:
677  raise InvalidVitals(
678  'Could not get mandatory field %s from input. '
679  'First line: %s'%(var,lines[0]),lines[0])
680  d['stormnamelc']=d['stormname'].lower()
681 
682  def _split_carq(self,lines,tech,logger=None,raise_all=True):
683  """!Internal function for parsing CARQ data
684 
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.
695 
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 ]
702  izeros=list()
703  ibig=None
704  fhrbig=None
705  for i in xrange(len(split)):
706  split[i]=[ x.strip() for x in split[i] ]
707  if len(split[i])<8:
708  raise InvalidATCF(
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] ]):
713  raise InvalidATCF(
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])
718  if myfhr==0:
719  if not izeros:
720  self._parse_atcf_time(split[i],tech,
721  logger=logger,raise_all=raise_all)
722  izeros.append(i)
723  if (ibig is None or myfhr>fhrbig) and myfhr<=72 and myfhr>0:
724  ibig=i
725  fhrbig=myfhr
726  assert(fhrbig>0)
727  return (split,izeros,ibig,fhrbig)
728 
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.
731 
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
740  hour and minute.
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"""
745  imin=0
746  if data[3]!='':
747  if tech=='BEST':
748  imin=int(data[3])
749  else:
750  self.__dict__['technum']=int(data[3])
751  iwhen=int(data[2])
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')
757 
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
760 
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"""
772  from hwrf.constants import ft2m,nmi2km,kts2mps
773  d=self.__dict__
774  if 'qset' in d:
775  qset=self.qset
776  else:
777  qset=set()
778  d['qset']=qset
779  n=len(data)
780  if n>=17:
781  try:
782  #print 'repr data',repr(data)
783  #print 'repr data 13 4=',repr(data[13:17])
784  irad=int(data[11])
785  windcode=data[12]
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)
792  if raise_all: raise
793  if n>=34:
794  try:
795  iseas=int(data[28])
796  seascode=data[29]
797  #print 'repr qdata 30 4=',repr(data[30:34])
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:
802  logger.warning(
803  'could not parse wave height info: %s line: %s'%
804  (str(e),instr),exc_info=True)
805  if raise_all: raise
806 
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.
809 
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):
823  raise TypeError(
824  'in _parse_atcf_meat, logger must be a logging.Logger, '
825  'but instead it is a %s'%(type(logger).__name__))
826  from hwrf.constants import ft2m,nmi2km,kts2mps
827  n=len(data)
828  d=self.__dict__
829  basin=data[0]
830  subbasin=None
831  #print 'line %s'%(', '.join(data),)
832  d['technique']=data[4]
833  d['tau']=int(data[5])
834  d['lat']=floatlatlon(data[6])
835  d['lon']=floatlatlon(data[7])
836  # Convenience functions to reduce code complexity. These
837  # attempt an operation, log a failure and move on.
838  def fic(s,i,c):
839  # unit conversion of an integer value data[i], producing a
840  # float, and assigning to d[s] if the input is >=0.
841  try:
842  ix=int(data[i])
843  if ix>=0:
844  d[s]=float(ix*c)
845  #print 'd[%s]=%f from %d'%(s,d[s],i)
846  #else:
847  #print 'd[%s] skipped due to %d (from %d)'%(s,ix,i)
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)
852  if raise_all:
853  raise InvalidATCF('%s: %s: line %s'%(s,str(e),instr),instr)
854  def fa(s,i):
855  # Simply convert a data value data[i] to a float and
856  # assign to d[s] if the result is >=0
857  try:
858  fx=float(data[i])
859  if fx>=0: d[s]=fx
860  #print 'd[%s]=%f from %d'%(s,fx,i)
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)
865  if raise_all:
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] # retain case
878  if n>=26 and data[25]!='':
879  if data[25]=='X':
880  d['stormdir']=-99
881  else:
882  fa('stormdir',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]
887  self._set_basin(basin,subbasin)
888  self.renumber_storm(int(data[1]),True)
889  d['stormnamelc']=d['stormname'].lower()
890 
891  def _parse_message_line(self,instr):
892  """!Do not call this routine directly. Call
893  StormInfo("message",instr) instead.
894 
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
899  line."""
900  return self._parse_tcvitals_line(instr,century=
901  int(datetime.datetime.utcnow().year)/100)
902  def _parse_tcvitals_line(self,instr,century=None):
903  """!Parses one line of tcvitals data
904 
905  Do not call this routine directly. Call
906  StormInfo("tcvitals",instr) instead.
907 
908  This subroutine parses one line of a tcvitals file of a format
909  described here:
910 
911  http://www.emc.ncep.noaa.gov/mmb/data_processing/tcvitals_description.htm
912 
913  Here is an example line with only some of the possible data:
914  @code{.tcvitals}
915  JTWC 31W HAIYAN 20131104 1200 061N 1483E 270 077 0989 1008 0352 23 064 0084 0074 0074 0084 M ... more stuff ...
916  @endcode
917 
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
924  one space.
925 
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."""
931  m=re.search('''(?xi)
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+)
942  \s+ (?P<rmw>-?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)
945  (?:
946  \s+ (?P<NE50>-?0*\d+) \s+ (?P<SE50>-?0*\d+) \s+ (?P<SW50>-?0*\d+) \s+ (?P<NW50>-?0*\d+)
947  (?:
948  \s+ (?P<fhr>-?0*\d+)
949  \s+ (?P<fstrlat>-?0*\d+[NS ])
950  \s+ (?P<fstrlon>-?0*\d+[EW ])
951  (?:
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?) )?
954  )?
955  )?
956  )?
957  )?''',instr)
958  if not m:
959  raise InvalidVitals('Cannot parse vitals: %s'%(repr(instr),),instr)
960 
961  # Variables that are allowed to have None values:
962  noneok=set(('NE50','SE50','SW50','NW50','fhr','fstrlat','fstrlon',
963  'NE64','SE64','SW64','NW64','stormtype','rawcentury',
964  'depth'))
965 
966  # Variables copied without type conversion:
967  raws=set(('rawbasin','rawstormname','rawcentury','rawYYMMDD','rawHHMM',
968  'depth'))
969 
970  # Convert lats and lons to signed floats, divided by 10.0:
971  latlons=set(('strlat','fstrlat','strlon','fstrlon'))
972 
973  # Call .strip on these variables:
974  stripme=set(('center','stormtype'))
975 
976  # Converted to int:
977  int1=set(('stnum',))
978 
979  # These variables are converted to floats:
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'))
984 
985  # Convert to float, divide by 10:
986  float10=set(('stormspeed',))
987 
988  d=self.__dict__
989  if 'qset' in d:
990  qset=d['qset']
991  else:
992  qset=set()
993  d['qset']=qset
994  mdict=dict(m.groupdict()) # input dict
995 
996  for k,v in mdict.iteritems():
997  if v is None:
998  if k in noneok: continue
999  raise InvalidVitals(
1000  'Mandatory variable %s had None value in line: %s'%
1001  (str(k),repr(instr)), instr )
1002  try:
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)
1012  elif k in latlons:
1013  repl=floatlatlon(v,10.0)
1014  if repl is None and not k in noneok:
1015  raise InvalidVitals(
1016  'Mandatory variable %s had invalid value %s '
1017  'in line: %s'%\
1018  (str(k),str(v),repr(instr)), instr )
1019  d[k.replace('str','')]=repl
1020  except ValueError as e:
1021  raise InvalidVitals(
1022  'Cannot parse vitals key %s value %s: %s from line %s'%
1023  ( str(k),repr(v),str(e),repr(instr) ), instr )
1024 
1025  # Boolean flag have34kt: do we have at least one valid 34kt
1026  # wind radius:
1027  d['have34kt'] = ( \
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 ) )
1030 
1031  # Boolean flag havefhr: do we have a forecast location?
1032  d['havefcstloc'] = 'flat' in d and 'flon' in d and \
1033  d['flat'] is not None and d['flon'] is not None
1034 
1035  # Generate upper-case storm name:
1036  if 'rawstormname' in d:
1037  d['stormname']=d['rawstormname'].strip().upper()
1038  else:
1039  raise InvalidVitals(
1040  'No storm name detected in this line: %s'%(instr,),instr)
1041 
1042  # Store a datetime.datetime object in the "when" variable, and
1043  # the ten-digit YYYYMMDDHH date in the YMDH variable as a str:
1044  if 'rawYYMMDD' in d and 'rawHHMM' in d:
1045  if 'rawcentury' in d:
1046  icentury=int(d['rawcentury'])
1047  else:
1048  icentury=int(current_century)
1049  if(icentury<16 or icentury>20):
1050  # icentury = first two digits of the year
1051  raise CenturyError(
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)
1056  d['YMDH']=sdate
1057  d['when']=hwrf.numerics.to_datetime(sdate)
1058  else:
1059  raise InvalidVitals('Cannot determine date and time '
1060  'from vitals: %s'%(repr(instr),),instr)
1061 
1062  if self.rawbasin=='L' and self.lat<0:
1063  self.rawbasin='Q'
1064 
1065  # Generate the basin information:
1066  if 'rawbasin' in d:
1067  self._set_basin(self.rawbasin)
1068  else:
1069  raise InvalidVitals('Cannot find a basin in this line: %s'
1070  %(instr,),instr)
1071 
1072  # Generate the auxiliary storm ID information:
1073  self.renumber_storm(self.stnum,discardold=True)
1074  d['stormnamelc']=d['stormname'].lower()
1075 
1076  return self
1077  def set_stormtype(self,stormtype,discardold=False):
1078  """!Sets the two letter storm type self.stormtype.
1079 
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):
1086  self.stormtype=str(stormtype)[0:2]
1087  else:
1088  self.stormtype=getattr(stormtype,'stormtype','XX')
1089  if self.stormtype is None or self.stormtype=='':
1090  self.stormtype='XX'
1091  else:
1092  self.stormtype=self.stormtype[0:2]
1093  assert(self.stormtype is not None)
1094  def rename_storm(self,newname,discardold=False):
1095  """!Sets the name of the storm.
1096 
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
1102  self.stormname=str(newname)[0:9]
1103  if self.format=='tcvitals' or self.format=='message':
1104  self.line='%s%-9s%s' % (self.line[0:9],
1105  self.stormname[0:9], self.line[18:])
1106  self.__dict__['stormnamelc']=self.stormname.lower()
1107 
1108  def renumber_storm(self,newnumber,discardold=False):
1109  """!Changes the storm number.
1110 
1111  Changes the storm number: the 09 in 09L. That changes
1112  self.stnum, stormid3, stormid3lc, stormid4 and longstormid.
1113 
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
1122  self.__dict__['old_longstormid']=self.longstormid
1123  self.has_old_stnum=True
1124  self.stnum=int(newnumber)
1125  self.stormid3='%02d%s' % (self.stnum,self.basin1)
1126  self.stormid4='%s%02d' % (self.pubbasin2,self.stnum)
1127  self.stormid3lc='%02d%s' % (self.stnum,self.basin1lc)
1128  if self.lat<0 and self.when.month<7:
1129  # South hemispheric season year starts in July. Storms
1130  # before that are for the prior year.
1131  self.longstormid='%s%02d%04d' % (self.pubbasin2,self.stnum,
1132  self.when.year-1)
1133  else:
1134  self.longstormid='%s%02d%04d' % (self.pubbasin2,self.stnum,
1135  self.when.year)
1136  if self.format=='tcvitals' or self.format=='message':
1137  self.line='%s%02d%s' % (self.line[0:5], self.stnum,
1138  self.line[7:])
1139 
1140  def swap_numbers(self):
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."""
1144  def swapname(o,n):
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')
1154  if self.format=='tcvitals' or self.format=='message':
1155  self.line='%s%02d%s' % (self.line[0:5], self.stnum,
1156  self.line[7:])
1157 
1158  def as_tcvitals(self):
1159  """!Returns a tcvitals version of this data. This is not
1160  cached, and will be recalculated every time it is called."""
1161  return self.as_tcvitals_or_message(no_century=False)
1162 
1163  def as_message(self):
1164  """!Returns a message line version of this data. This is not
1165  cached, and will be recalculated every time it is called."""
1166  return self.as_tcvitals_or_message(no_century=True)
1167 
1168  def as_tcvitals_or_message(self,no_century=False):
1169  """!Internal function that underlies as_tcvitals() and as_message()
1170 
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."""
1175  d=self.__dict__
1176  def bad(s):
1177  if s not in d: return True
1178  val=d[s]
1179  if val is None: return True
1180  return val<0
1181  def bad0(s):
1182  if s not in d: return True
1183  val=d[s]
1184  if val is None: return True
1185  return val<=1e-5
1186  def cint(i): return int(abs(round(i)))
1187  if no_century:
1188  datestring='%y%m%d %H%M'
1189  else:
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' % (
1193  str(self.center)[0:4], int(abs(self.stnum)%100),
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 \
1200  min(999,cint(self.stormspeed*10.0)),
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)))
1210 
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 )
1216 
1217  if 'depth' not in d and not gotst and not gotfcst:
1218  return result
1219  result='%s %s'%(result,str(getattr(self,'depth','X'))[0])
1220 
1221  if 'NW50' not in d and not gotst and not gotfcst:
1222  return result
1223  result='%s %04d %04d %04d %04d'%(
1224  result,
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)))
1229 
1230  if gotfcst:
1231  result='%s %02d %03d%s %04d%s'%(
1232  result,
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' ))
1238 
1239  if 'NW64' not in d and not gotst: return result
1240 
1241  if not gotfcst: result+=' -9 -99N -999W'
1242 
1243  result='%s %04d %04d %04d %04d'%(
1244  result,
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)))
1249 
1250  if not gotst: return result
1251 
1252  return '%s % 2s'%(result,str(self.stormtype)[0:2])
1253 
1254  def change_basin(self,basin,subbasin=None,discardold=False):
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. """
1260  if not discardold:
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
1266  self._set_basin(basin,subbasin)
1267  if self.format=='tcvitals':
1268  self.line='%s%s%s'%(self.line[0:7],self.basin1[0],self.line[8:])
1269  #self.renumber_storm(self.stnum,True)
1270 
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.
1275 
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. """
1280  bb=expand_basin(basin,subbasin)
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]
1286 
1287  def __doxygen(self):
1288  """!Ensure that self.varname exists for all member variables,
1289  so that Doxygen detects them"""
1290  self.center='' ; self.flat=1 ; self.flon=1
1291  self.havefcstloc=1 ; self.fhr=1 ; self.stormname=''
1292  self.stormnamelc='' ; self.technum='' ; self.when=1
1293  self.YMDH='' ; self.qset=1 ; self.windcode34=''
1294  self.windcode50='' ; self.windcode64='' ; self.technique=''
1295  self.tau='' ; self.lat='' ; self.lon=''
1296  self.wmax=1 ; self.pmin=1 ; self.poci=1
1297  self.roci=1 ; self.rmw=1 ; self.gusts=1
1298  self.eyediam=1 ; self.maxseas=1 ; self.stormdir=1
1299  self.stormspeed=1 ; self.depth=1 ; self.NE50=1
1300  self.SE50=1 ; self.SW50=1 ; self.NW50=1
1301  self.fstrlat='' ; self.fstrlon='' ; self.NE64=1
1302  self.SE64=1 ; self.SW64=1 ; self.NW64=1
1303  self.stormtype='' ; self.rawcentury='' ; self.rawstormname=''
1304  self.rawbasin='' ; self.rawYYMMDD='' ; self.rawHHMM=''
1305  self.strlat='' ; self.fstrlat='' ; self.strlon=''
1306  self.fstrlon='' ; self.center='' ; self.stnum=''
1307  self.NE34=1 ; self.SE34=1 ; self.SW34=1 ; self.NW34=1
1308  self.old_stormtype='' ; self.old_stormname='' ; self.stnum=''
1309  self.old_stnum='' ; self.stormid3='' ; self.old_stormid3=''
1310  self.stormid3lc='' ; self.old_stormid3lc='' ; self.stormid4=''
1311  self.old_stormid4='' ; self.longstormid='' ; self.old_longstormid=''
1312  self.line=''
1313 
1314  return
1315 
1316  ##@var center
1317  # The forecast center (RSMC) whose forecaster provided this information
1318 
1319  ##@var flat
1320  # Forecast latitude in degrees North
1321 
1322  ##@var flon
1323  # Forecast longitude in degrees East
1324 
1325  ##@var fhr
1326  # Forecast hour
1327 
1328  ##@var havefcstloc
1329  # If True, the fhr, flat and flon are provided
1330 
1331  ##@var stormname
1332  # Upper-case storm name
1333 
1334  ##@var stormnamelc
1335  # Lower-case storm name
1336 
1337  ##@var technum
1338  # Technique number for ATCF
1339 
1340  ##@var technique
1341  # Technique field for ATCF
1342 
1343  ##@var when
1344  # The datetime.datetime for the valid time
1345 
1346  ##@var YMDH
1347  # The ten digit date and time of the valid time
1348 
1349  ##@var qset
1350  # Set of quadrant information keys
1351 
1352  ##@var windcode34
1353  # Code sent for 34kt wind radii
1354 
1355  ##@var windcode50
1356  # Code sent for 50kt wind radii
1357 
1358  ##@var windcode64
1359  # Code sent for 64kt wind radii
1360 
1361  ##@var tau
1362  # Tau value from the ATCF
1363 
1364  ##@var lat
1365  # Storm center latitude in degrees North, a float
1366 
1367  ##@var lon
1368  # Storm center longitude in degrees East, a float
1369 
1370  ##@var wmax
1371  # Maximum wind as a float
1372 
1373  ##@var pmin
1374  # Minimum pressure as a float
1375 
1376  ##@var poci
1377  # Pressure of the outermost closed isobar
1378 
1379  ##@var roci
1380  # Radius of the outermost closed isobar
1381 
1382  ##@var rmw
1383  # Radius of the maximum wind
1384 
1385  ##@var gusts
1386  # Maximum gust
1387 
1388  ##@var eyediam
1389  # Eye diameter from the tcvitals
1390 
1391  ##@var maxseas
1392  # Maximum sea height as a float
1393 
1394  ##@var stormdir
1395  # Storm movement direction from tcvitals, degrees
1396 
1397  ##@var stormspeed
1398  # Storm movement speed from tcvitals, m/s float
1399 
1400  ##@var depth
1401  # Storm depth: S, M or D; or X for missing
1402 
1403  ##@var NW64
1404  # NW quadrant 64kt wind radius
1405 
1406  ##@var NE50
1407  # NE quadrant 50kt wind radius
1408 
1409  ##@var SE50
1410  # SE quadrant 50kt wind radius
1411 
1412  ##@var SW50
1413  # SW quadrant 50kt wind radius
1414 
1415  ##@var NW50
1416  # NW quadrant 50kt wind radius
1417 
1418  ##@var NE34
1419  # NE quadrant 34kt wind radius
1420 
1421  ##@var SE34
1422  # SE quadrant 34kt wind radius
1423 
1424  ##@var SW34
1425  # SW quadrant 34kt wind radius
1426 
1427  ##@var NW34
1428  # NW quadrant 34kt wind radius
1429 
1430  ##@var NE64
1431  # NE quadrant 64kt wind radius
1432 
1433  ##@var SE64
1434  # SE quadrant 64kt wind radius
1435 
1436  ##@var SW64
1437  # SW quadrant 64kt wind radius
1438 
1439  ##@var fstrlat
1440  # Original flat string.
1441 
1442  ##@var fstrlon
1443  # Original flon string.
1444 
1445  ##@var stormtype
1446  # Two character best track storm type
1447 
1448  ##@var rawcentury
1449  # Raw parser data for the century
1450 
1451  ##@var rawstormname
1452  # Raw parser data for the storm name
1453 
1454  ##@var rawbasin
1455  # Raw parser data for the basin
1456 
1457  ##@var rawYYMMDD
1458  # Raw parser data for the date
1459 
1460  ##@var rawHHMM
1461  # Raw parser data for the time
1462 
1463  ##@var strlat
1464  # Original string version of lat
1465 
1466  ##@var strlon
1467  # Original string version of lon
1468 
1469  ##@var stnum
1470  # Storm number: 1-49 for real storms, 0 for fake basin-scale,
1471  # 50-79 for RSMC internal usage, 80-89 for test storms and 90-99
1472  # for genesis cases.
1473 
1474  ##@var has_old_stnum
1475  # If True, the various old_* variables are present.
1476  # If False, they are not even in self.__dict__
1477 
1478  ##@var old_stormtype
1479  # stormtype before Invest renumbering
1480 
1481  ##@var old_stormname
1482  # stormname before Invest renumbering
1483 
1484  ##@var old_stnum
1485  # stnum before Invest renumbering
1486 
1487  ##@var old_stormid3
1488  # stormid3 before Invest renumbering
1489 
1490  ##@var old_stormid3lc
1491  # stormid3lc before Invest renumbering
1492 
1493  ##@var old_stormid4
1494  # stormid4 before Invest renumbering
1495 
1496  ##@var old_longstormid
1497  # longstormid before Invest renumbering
1498 
1499  ##@var stormid3
1500  # Three character storm ID from tcvitals: 09L, 91S, 18P, etc.
1501  # Upper-case
1502 
1503  ##@var stormid3lc
1504  # Three character storm ID from tcvitals: 09l, 91s, 18p, etc.
1505  # Lower-case.
1506 
1507  ##@var stormid4
1508  # Four character storm ID: AL09, SH91, IO18, etc.
1509 
1510  ##@var longstormid
1511  # Storm basin, number and year: AL092012. Note that southern
1512  # hemisphere "years" start at July 1, so July-Dec storms have the
1513  # next physical year in their longstormid.
1514 
1515 def expand_basin(basin,subbasin=None):
1516  """!Converts basin identifiers
1517 
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:
1523 
1524  1. The internal (HWRF/GFDL) two-letter basin identifier. These
1525  have an unambiguous mapping to the one-letter basin.
1526 
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
1529  be S or P.
1530 
1531  3. The one-letter basin identifier.
1532 
1533  4. A description of the meaning of the basin.
1534 
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)' )
1559  elif b=='WP':
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)' )
1564  # no subbasin is same as s=='W' when basin is 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)' )
1569  elif b=='SH':
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)' )
1578  elif b=='IO':
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)' )
1585  # These three are never used but are defined in the 2007
1586  # tcvitals documentation:
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)' )
1593  else:
1594  raise InvalidBasinError(basin,subbasin)
1595  return bb
def clean_up_vitals
Given a list of StormInfo, sorts using the vitcmp comparison, discards suspect storm names and number...
Definition: storminfo.py:187
def name_number_okay(vl)
Given an array of StormInfo objects, iterate over those that have valid names and numbers...
Definition: storminfo.py:105
NW50
NW quadrant 50kt wind radius.
Definition: storminfo.py:1300
fstrlat
Original flat string.
Definition: storminfo.py:1301
def swap_numbers(self)
Swaps the new and old stormid variables.
Definition: storminfo.py:1140
def hwrf_domain_center
Uses the 2013 operational HWRF method of deciding the domain center based on the storm location...
Definition: storminfo.py:546
wmax
Maximum wind as a float.
Definition: storminfo.py:1296
flat
Forecast latitude in degrees North.
Definition: storminfo.py:1290
def expand_basin
Converts basin identifiers.
Definition: storminfo.py:1515
def to_timedelta
Converts an object to a datetime.timedelta.
Definition: numerics.py:371
old_stormid3lc
stormid3lc before Invest renumbering
Definition: storminfo.py:1310
old_stormid4
stormid4 before Invest renumbering
Definition: storminfo.py:1311
def __init__
StormInfo constructor.
Definition: storminfo.py:441
def basin_center_okay(vl)
Given a list of StormInfo objects, iterates over those that have the right basins for the right cente...
Definition: storminfo.py:116
def _set_basin
This is a utility function that creates the one and two letter basins from a raw one and/or two lette...
Definition: storminfo.py:1271
old_stormtype
stormtype before Invest renumbering
Definition: storminfo.py:1308
longstormid
Storm basin, number and year: AL092012.
Definition: storminfo.py:1131
Constants used throughout the hwrf package.
Definition: constants.py:1
roci
Radius of the outermost closed isobar.
Definition: storminfo.py:1297
def to_fraction
Converts an object or two to a fraction.
Definition: numerics.py:269
def to_datetime_rel(d, rel)
Converts objects to a datetime relative to another datetime.
Definition: numerics.py:319
def _parse_atcf_time
Internal function for getting the time out of ATCF data.
Definition: storminfo.py:729
rawbasin
Raw parser data for the basin.
Definition: storminfo.py:1062
center
The forecast center (RSMC) whose forecaster provided this information.
Definition: storminfo.py:1290
lon
Storm center longitude in degrees East, a float.
Definition: storminfo.py:1295
rawYYMMDD
Raw parser data for the date.
Definition: storminfo.py:1304
qset
Set of quadrant information keys.
Definition: storminfo.py:1293
windcode34
Code sent for 34kt wind radii.
Definition: storminfo.py:1293
NE50
NE quadrant 50kt wind radius.
Definition: storminfo.py:1299
def as_tcvitals_or_message
Internal function that underlies as_tcvitals() and as_message()
Definition: storminfo.py:1168
def old(self)
Returns a copy of this StormInfo, but with the last renumbering or renaming of the vitals undone...
Definition: storminfo.py:511
def __str__(self)
Return a human-readable string representation of this error.
Definition: storminfo.py:65
SW64
SW quadrant 64kt wind radius.
Definition: storminfo.py:1302
def __add__(self, amount)
Returns a copy of this object, with the vitals extrapolated forward "amount" hours.
Definition: storminfo.py:522
def floatlatlon
Converts a string like "551N" to 55.1, correctly handling the sign of each hemisphere.
Definition: storminfo.py:234
Raised when a syntax error is found in the tcvitals, and the code cannot guess what the operator inte...
Definition: storminfo.py:92
NW34
NW quadrant 34kt wind radius.
Definition: storminfo.py:1307
stormname
Upper-case storm name.
Definition: storminfo.py:1102
def parse_tcvitals
Reads data from a tcvitals file.
Definition: storminfo.py:302
when
The datetime.datetime for the valid time.
Definition: storminfo.py:1292
fstrlon
Original flon string.
Definition: storminfo.py:1301
eyediam
Eye diameter from the tcvitals.
Definition: storminfo.py:1298
format
The linetype argument to the constructor.
Definition: storminfo.py:474
flon
Forecast longitude in degrees East.
Definition: storminfo.py:1290
rawstormname
Raw parser data for the storm name.
Definition: storminfo.py:1303
subbasin
The problematic subbasin, or None if no subbasin was given.
Definition: storminfo.py:57
rawcentury
Raw parser data for the century.
Definition: storminfo.py:1303
tau
Tau value from the ATCF.
Definition: storminfo.py:1295
old_longstormid
longstormid before Invest renumbering
Definition: storminfo.py:1311
rawHHMM
Raw parser data for the time.
Definition: storminfo.py:1304
def rename_storm
Sets the name of the storm.
Definition: storminfo.py:1094
NW64
NW quadrant 64kt wind radius.
Definition: storminfo.py:1302
technique
Technique field for ATCF.
Definition: storminfo.py:1294
stnum
Storm number: 1-49 for real storms, 0 for fake basin-scale, 50-79 for RSMC internal usage...
Definition: storminfo.py:1124
YMDH
The ten digit date and time of the valid time.
Definition: storminfo.py:1293
depth
Storm depth: S, M or D; or X for missing.
Definition: storminfo.py:1299
has_old_stnum
If True, the various old_* variables are present.
Definition: storminfo.py:475
NE64
NE quadrant 64kt wind radius.
Definition: storminfo.py:1301
havefcstloc
If True, the fhr, flat and flon are provided.
Definition: storminfo.py:1291
maxseas
Maximum sea height as a float.
Definition: storminfo.py:1298
def _parse_message_line(self, instr)
Do not call this routine directly.
Definition: storminfo.py:891
def to_datetime(d)
Converts the argument to a datetime.
Definition: numerics.py:346
This exception is raised when the StormInfo class receives an invalid tcvitals line or ATCF line that...
Definition: storminfo.py:77
Time manipulation and other numerical routines.
Definition: numerics.py:1
stormspeed
Storm movement speed from tcvitals, m/s float.
Definition: storminfo.py:1299
Base class of all exceptions in this module.
Definition: exceptions.py:8
def __init__
InvalidBasinError constructor.
Definition: storminfo.py:50
lat
Storm center latitude in degrees North, a float.
Definition: storminfo.py:1295
windcode64
Code sent for 64kt wind radii.
Definition: storminfo.py:1294
stormid3
Three character storm ID from tcvitals: 09L, 91S, 18P, etc.
Definition: storminfo.py:1125
def renumber_storm
Changes the storm number.
Definition: storminfo.py:1108
def vitcmp(a, b)
A cmp comparison for StormInfo objects intended to be used with sorted().
Definition: storminfo.py:151
pmin
Minimum pressure as a float.
Definition: storminfo.py:1296
This exception is raised when an invalid Tropical Cyclone basin is found.
Definition: storminfo.py:46
stormid4
Four character storm ID: AL09, SH91, IO18, etc.
Definition: storminfo.py:1126
def storm_key(vit)
Generates a hashable key for hashing StormInfo objects.
Definition: storminfo.py:174
stormtype
Two character best track storm type.
Definition: storminfo.py:1086
stormid3lc
Three character storm ID from tcvitals: 09l, 91s, 18p, etc.
Definition: storminfo.py:1127
stormdir
Storm movement direction from tcvitals, degrees.
Definition: storminfo.py:1298
def set_stormtype
Sets the two letter storm type self.stormtype.
Definition: storminfo.py:1077
lines
Multi-line input to the constructor.
Definition: storminfo.py:483
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.
Definition: storminfo.py:41
technum
Technique number for ATCF.
Definition: storminfo.py:1292
poci
Pressure of the outermost closed isobar.
Definition: storminfo.py:1296
old_stormname
stormname before Invest renumbering
Definition: storminfo.py:1308
SW50
SW quadrant 50kt wind radius.
Definition: storminfo.py:1300
def change_basin
Changes the basin of this StormInfo.
Definition: storminfo.py:1254
SE34
SE quadrant 34kt wind radius.
Definition: storminfo.py:1307
SE64
SE quadrant 64kt wind radius.
Definition: storminfo.py:1302
NE34
NE quadrant 34kt wind radius.
Definition: storminfo.py:1307
fhr
Forecast hour.
Definition: storminfo.py:1291
def parse_carq
Scans an A deck file connected to stream-like object fd, reading it into a list of StormInfo objects...
Definition: storminfo.py:378
Raised when an implausible century is found.
Definition: storminfo.py:95
def vit_cmp_by_storm(a, b)
A cmp comparison for StormInfo objects intended to be used with sorted().
Definition: storminfo.py:134
basin
The problematic basin.
Definition: storminfo.py:56
Exceptions raised by the hwrf package.
Definition: exceptions.py:1
strlon
Original string version of lon.
Definition: storminfo.py:1305
SW34
SW quadrant 34kt wind radius.
Definition: storminfo.py:1307
def copy(self)
Returns a copy if this object.
Definition: storminfo.py:515
This should be raised when the user requests a specific storm or cycle of a storm and no such vitals ...
Definition: storminfo.py:99
strlat
Original string version of lat.
Definition: storminfo.py:1305
line
Contents of the line of text sent to init
Definition: storminfo.py:477
def _parse_atcf_meat
Internal function that parses most of a line of ATCF data.
Definition: storminfo.py:807
SE50
SE quadrant 50kt wind radius.
Definition: storminfo.py:1300
def _parse_tcvitals_line
Parses one line of tcvitals data.
Definition: storminfo.py:902
def __repr__(self)
Return a Pythonic representation of this error.
Definition: storminfo.py:72
def find_tcvitals_for
Faster way of finding tcvitals data for a specific case.
Definition: storminfo.py:326
def as_message(self)
Returns a message line version of this data.
Definition: storminfo.py:1163
windcode50
Code sent for 50kt wind radii.
Definition: storminfo.py:1294
def _parse_atcf_radii_seas
Internal function for parsing radii and sea information in ATCF data.
Definition: storminfo.py:758
def __sub__(self, amount)
Same as self + (-amount)
Definition: storminfo.py:518
def _split_carq
Internal function for parsing CARQ data.
Definition: storminfo.py:682
stormnamelc
Lower-case storm name.
Definition: storminfo.py:1292
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...
Definition: storminfo.py:628
def quadrantinfo
Internal function that parses wind or sea quadrant information.
Definition: storminfo.py:259
old_stnum
stnum before Invest renumbering
Definition: storminfo.py:1309
Raised when invalid ATCF data is found.
Definition: storminfo.py:97
def __init__(self, message, badline)
InvalidStormInfoLine constructor.
Definition: storminfo.py:81
Storm vitals information from ATCF, B-deck, tcvitals or message files.
Definition: storminfo.py:411
old_stormid3
stormid3 before Invest renumbering
Definition: storminfo.py:1309
rmw
Radius of the maximum wind.
Definition: storminfo.py:1297
def as_tcvitals(self)
Returns a tcvitals version of this data.
Definition: storminfo.py:1158
badline
The line at which the problem happened.
Definition: storminfo.py:86