HWRF  trunk@4391
revital.py
1 """!Defines the Revital class which manipulates tcvitals files.
2 
3 This module deals with rewriting TCVitals files to remove errors,
4 change Invests to storms, and other such operations. """
5 
6 ##@var __all__
7 # List of symbols to export by "from hwrf.revital import *"
8 __all__=['Revital','RevitalError','RevitalInitError']
9 
10 import logging, datetime, getopt, sys, os.path,re, math, errno, collections
13 
14 from hwrf.numerics import great_arc_dist, to_fraction, to_timedelta
15 from produtil.fileop import isnonempty
16 from hwrf.exceptions import HWRFError
17 
19  """!Base class of errors related to rewriting vitals."""
21  """!This exception is raised when an argument to the Revital
22  constructor is invalid."""
23 
24 # Conveniences to simplify later code and avoid extra calls to to_timedelta:
25 
26 ##@var zero_time
27 # A datetime.timedelta that represents zero time difference
28 zero_time=to_timedelta(0)
29 
30 ##@var two_days
31 # A datetime.timedelta that represents positive 48 hours
32 two_days=to_timedelta(3600*24*2)
33 
34 ##@var six_hours
35 # A datetime.timedelta that represents positive 6 hours
36 six_hours=to_timedelta(3600*6)
37 
38 class Revital:
39  """!This class reads one or more tcvitals files and rewrites them
40  as requested."""
41  def __init__(self,logger=None,invest_number_name=False,stormid=None,
42  adeckdir=None,renumberlog=None,
43  search_dx=200e3, search_dt=None, debug=True,copy=None):
44  """!Creates a Revital object:
45 
46  @param logger A logging.Logger object for logging or None to
47  disable. Default: None
48  @param invest_number_name Rename storms to have the last
49  non-INVEST name seen
50  @param stormid Ignored.
51  @param adeckdir Directory with A deck files. This is used to
52  read the storm type information from CARQ entries and
53  append them to produce vitals with the storm type from
54  vitals that lack it.
55  @param renumberlog Ignored.
56  @param search_dx Search radius in km for deciding whether two
57  storms are the same.
58  @param search_dt Search timespan for finding two storms that are
59  the same.
60  @param debug If True, enables DEBUG level logging.
61  @param copy Used by copy() to make a shallow copy of a
62  Revital. If specified, the other arguments are ignored, and
63  the copy's contents are copied. Do not use this argument.
64  If you need a copy, use copy() instead."""
65  if copy is not None:
66  ( self.search_dx, self.search_dt, self.logger, self.debug ) = \
67  ( copy.search_dx, copy.search_dt, copy.logger, copy.debug )
68  ( self.invest_number_name, self.adeckdir, self.is_cleaned ) = \
69  ( copy.invest_number_name, copy.adeckdir, copy.is_cleaned )
70  self.carqdat=dict()
71  for key,cdat in copy.carqdat.iteritems():
72  self.carqdat[key]=dict()
73  for ymdh,card in cdat.iteritems():
74  self.carqdat[key][ymdh]=card.copy()
75  self.carqfail=set(copy.carqfail)
76  self.vitals=[ v.copy() for v in copy.vitals ]
77  return
78  self.search_dx=float(search_dx)
79  self.search_dt=six_hours if(search_dt is None) else search_dt
80  self.logger=logger
81  self.debug=debug and logger is not None
82  self.invest_number_name=bool(invest_number_name)
83  self.adeckdir=adeckdir
84  if adeckdir is not None:
85  if not os.path.isdir(adeckdir):
86  raise RevitalInitError(
87  'Specified directory %s is not a directory.'
88  %(adeckdir,))
89 
90  self.carqdat=dict()
91  self.carqfail=set()
92  self.vitals=list()
93  self.is_cleaned=False
94  return
95 
96  ##@var search_dx
97  # Search radius in km for deciding whether two storms are the same.
98 
99  ##@var search_dt
100  # Search timespan for finding two storms that are the same
101 
102  ##@var logger
103  # The logging.Logger to use for log messages
104 
105  ##@var debug
106  # If True, send numerous extra log messages at DEBUG level.
107 
108  ##@var invest_number_name
109  # Rename stores to have the last non-INVEST name seen
110 
111  ##@var adeckdir
112  # Directory with A deck files. This is used to
113  # read the storm type information from CARQ entries and
114  # append them to produce vitals with the storm type from
115  # vitals that lack it.
116 
117  ##@var carqdat
118  # Contains CARQ entries read from the adeckdir A deck files.
119 
120  ##@var carqfail
121  # Set of longstormid entries that had no CARQ data.
122 
123  ##@var vitals
124  # The list of hwrf.storminfo.StormInfo objects being revitalized.
125 
126  ##@var is_cleaned
127  # Has clean_up_vitals() been called since the last operation that
128  # modified the vitals?
129 
130  def append(self,vital):
131  """!Appends a vital entry to self.vitals.
132  @param vital an hwrf.storminfo.StormInfo to append"""
133  if not isinstance(vital,hwrf.storminfo.StormInfo):
134  raise TypeError('The argument to Revital.append must be an hwrf.storminfo.StormInfo')
135  self.vitals.append(vital)
136  def extend(self,vitals):
137  """!Given the specified iterable object, appends its contents
138  to my own.
139  @param vitals an iterable object filled with hwrf.storminfo.StormInfo"""
140  self.vitals.extend(vitals)
141  self.is_cleaned=False
142  def copy(self):
143  """!Returns a deep copy of this Revital. Modifying the copy
144  will not modify the original."""
145  return Revital(copy=self)
146 
147  def readvitals(self, vitalslist, raise_all=True):
148  """!Same as readfiles except the tcvitals have already been
149  parsed and the vitals are being passed in, not the
150  filelist. This was created to handle the multistorm fake
151  storm. This is being used to validate any self generated
152  vitals by the fake storm.
153  @param vitalslist a list of strings with tcvitals data
154  @param raise_all if True, raise exceptions for any parsing errors.
155  @returns self"""
156 
157  if len(vitalslist) == 0:
158  self.logger.critical('No parsed tcvitals provided to revital.readvitals')
159  else:
160  self.logger.info('Processing parsed tcvitals, line1: %s'%(vitalslist[:1]))
161 
162  self.vitals.extend(hwrf.storminfo.parse_tcvitals(
163  vitalslist,raise_all=raise_all,logger=self.logger))
164  self.is_cleaned=False
165  if self.logger is not None and self.debug:
166  self.logger.debug('line count: %d'%(len(self.vitals),))
167  return self
168 
169  def readfiles(self,filelist,raise_all=True):
170  """!Reads the list of files and parses them as tcvitals files.
171  @param filelist a list of string filenames
172  @param raise_all if True, all exceptions are raised. If False,
173  then exceptions are ignored, and the function will attempt to
174  process all files, even if earlier ones failed."""
175  if isinstance(filelist,basestring):
176  filelist=[filelist]
177  lines=list()
178  opened=False
179  for tcvitals in filelist:
180  if self.logger is not None:
181  self.logger.info('read file: %s'%(tcvitals,))
182  try:
183  with open(tcvitals,'rt') as f:
184  lines.extend(f.readlines())
185  opened=True
186  except EnvironmentError as e:
187  if e.errno==errno.ENOENT or e.errno==errno.EISDIR:
188  self.logger.warning(tcvitals+': cannot open: '+str(e))
189  if raise_all: raise
190  else:
191  self.logger.warning(tcvitals+': cannot open: '+str(e))
192  raise
193  if not opened:
194  self.logger.critical('No message files or tcvitals files '
195  'provided to revital.readfiles.')
196  self.vitals.extend(hwrf.storminfo.parse_tcvitals(
197  lines,raise_all=raise_all,logger=self.logger))
198  self.is_cleaned=False
199  if self.logger is not None and self.debug:
200  self.logger.debug('line count: %d'%(len(self.vitals),))
201 
202  def move_latlon(self,vital,dt):
203  """!Returns a tuple containing the latitude and longitude of
204  the storm at a different position according to the storm
205  motion vector.
206  @param vital the hwrf.storminfo.StormInfo for the storm fix being extrapolated
207  @param dt the time difference in hours"""
208  stormspeed=getattr(vital,'stormspeed',None)
209  stormdir=getattr(vital,'stormdir',None)
210  if stormspeed is None or stormspeed<0 or stormdir is None or \
211  stormdir<0 or stormspeed==0:
212  # No storm motion vector
213  return (None,None)
214  pi180=math.pi/180.
215  Rearth=6378137.
216  k=stormspeed*dt/Rearth / pi180
217  moveangle=pi180*(90.0-stormdir)
218  dlat=k*math.sin(moveangle)
219  dlon=k*math.cos(moveangle)/math.cos(vital.lat*pi180)
220  return (vital.lat+dlat, vital.lon+dlon)
221 
222  def readcarq(self,longstormid):
223  """!Tries to find the CARQ data for the specified storm. Reads
224  it into the self.carqdat array, or adds the stormid to
225  self.carqfail if the data cannot be read in.
226  @param longstormid the long stormid of the storm to read
227  @post the self.carqfail will contain longstormid OR
228  self.carqdat[longstormid] will contain data for that storm"""
229  if longstormid in self.carqfail: return
230  filename=os.path.join(self.adeckdir,'a%s.dat'%(longstormid,))
231  if not isnonempty(filename):
232  self.carqfail.add(longstormid)
233  return
234  with open(filename,'rt') as f:
235  data=[ line for line in f.readlines() ]
236  carq=hwrf.storminfo.parse_carq(data,logger=self.logger)
237  cdat=dict()
238  for card in carq:
239  cdat[card.YMDH]=card
240  self.carqdat[longstormid]=cdat
241 
242  def clean_up_vitals(self,name_number_checker=None,
243  basin_center_checker=None,vitals_cmp=None):
244  """!Calls the hwrf.storminfo.clean_up_vitals on this object's
245  vitals. The optional arguments are passed to
246  hwrf.storminfo.clean_up_vitals.
247 
248  @param name_number_checker a function like
249  hwrf.storminfo.name_number_okay() for validating the storm
250  name and number
251  @param vitals_cmp a cmp-like function for ordering hwrf.storminfo.StormInfo objects
252  @param basin_center_checker a function like
253  hwrf.storminfo.basin_center_okay() for checking the storm
254  basin and forecast center (RSMC)
255 
256  @post is_cleaned=True"""
258  self.vitals,name_number_checker=name_number_checker,
259  basin_center_checker=basin_center_checker,
260  vitals_cmp=vitals_cmp)
261  self.is_cleaned=True
262 
263  ##################################################################
264 
265  def renumber_one(self,vital,lastvit,vit_motion,other_motion,threshold):
266  """!Internal function that handles renumbering of storms.
267 
268  @protected
269  This is an internal implementation function that should
270  never be called directly. It handles part of the work of
271  renumbering storms in the list of vitals. You should call
272  "renumber" instead.
273  @param vital the vital being renumbered
274  @param lastvit the last vital seen
275  @param vit_motion time since vital
276  @param other_motion time since other storms vital
277  @param threshold cold start threshold, used to decide when to
278  stop connecting an invest to a non-invest"""
279  renumbered=False
280  debug=self.debug and self.logger is not None
281  logger=self.logger
282 
283  assert(vit_motion>=0)
284  assert(other_motion<=0)
285 
286  # Get the storm's lat and lon for searching. For the "subtract
287  # the storm motion vector" mode (sub_motion=True), this is the
288  # location minus the storm motion vector. Otherwise, it is the
289  # location.
290  if vit_motion>0:
291  (lat,lon)=self.move_latlon(vital,3600.0*vit_motion)
292  if self.debug and lat is not None and lon is not None:
293  self.logger.debug(' -- lat=%.3f lon=%.3f'%(lat,lon))
294  else:
295  (lat,lon)=(vital.lat,vital.lon)
296  if debug and lat is None or lon is None:
297  if debug: logger.debug(' -- no lat,lon for search')
298  return False
299 
300  logger=self.logger
301  debug=self.debug and logger is not None
302  renumbered=False
303  for stormid in lastvit.keys():
304  othervit=lastvit[stormid]
305  if threshold:
306  old_id=getattr(othervit,'old_stnum',0)
307  if old_id>=90 and othervit.wmax<threshold:
308  if debug:
309  logger.debug(
310  'Old %s vit was a low intensity invest, so '
311  'not considering it: %s'%(
312  othervit.old_stormid3,othervit.line))
313  continue
314  if debug: logger.debug(' vs. %s'%(othervit.line,))
315  dt=othervit.when-vital.when
316  if dt<zero_time:
317  dt=-dt # allow reverse traversal
318  if dt==zero_time: continue # same time, so nothing to do
319  if dt>two_days:
320  # Old storm, so age out of lastvit to save CPU time:
321  if debug: logger.debug(' -- age out othervit')
322  del lastvit[stormid]
323  continue
324  if dt>self.search_dt:
325  if debug: logger.debug(' -- dt is too large')
326  continue
327  if debug: logger.debug(' -- within dt')
328  if othervit.has_old_stnum:
329  if othervit.old_stnum==vital.stnum and \
330  othervit.basin1==vital.basin1:
331  if debug:
332  logger.debug(' -- continue renumbering (%s) %s'
333  %(vital.stormid3,vital.line))
334  if self.invest_number_name:
335  vital.rename_storm('INVEST%02d%1s'%
336  (int(vital.stnum),vital.basin1))
337  vital.change_basin(othervit.basin1,othervit.pubbasin2)
338  vital.renumber_storm(int(othervit.stormid3[0:2]))
339  renumbered=True
340  if debug:
341  logger.debug(' NOW %s'%(vital.line,))
342  lastvit[othervit.stormid3]=vital
343  continue
344 
345  if other_motion<0:
346  (otherlat,otherlon)=self.move_latlon(
347  othervit,3600.0*other_motion)
348  if debug and otherlat is not None and otherlon is not None:
349  logger.debug(' -- vs lat=%.3f lon=%.3f'
350  %(otherlat,otherlon))
351  else:
352  (otherlat,otherlon)=(othervit.lat,othervit.lon)
353  if otherlat is None or otherlon is None:
354  if debug:
355  logger.debug(
356  ' -- cannot get other vitals location; moving on.')
357  continue
358 
359  dist=great_arc_dist(lon,lat,otherlon,otherlat)
360  if not (dist<self.search_dx and dt==six_hours):
361  if debug:
362  logger.debug(
363  ' -- not kinda near (distance %f km)'
364  %(dist/1e3,))
365  continue
366 
367  if debug:
368  logger.debug(' -- within dx: renumber to %s and store'%\
369  (othervit.stormid3,))
370  if self.invest_number_name:
371  vital.rename_storm('INVEST%02d%1s'%
372  (int(vital.stnum),vital.basin1))
373  vital.change_basin(othervit.basin1,othervit.pubbasin2)
374  vital.renumber_storm(int(othervit.stormid3[0:2]))
375  renumbered=True
376  if debug: logger.debug(' NOW %s'%(vital.line,))
377  lastvit[othervit.stormid3]=vital
378  if debug: logger.debug(' - renumbered = %s'%(repr(renumbered),))
379  return renumbered
380 
381  def renumber(self,unrenumber=False,clean=True,threshold=0,
382  discard_duplicates=True):
383  """!Renumbers storms with numbers 90-99, if possible, to have
384  the same number as later 1-49 numbered storms.
385 
386  Loops over all vitals from last to first, renumbering 90-99
387  storms to have the same storm number as later 1-49 storms.
388 
389  @param threshold If a threshold is given, then a cycle will
390  only be considered for renumbering if it is either above that
391  threshold, or is not an Invest.
392  @param unrenumber If unrenumber is True, the original storm
393  numbers are restored after renumbering.
394  @param discard_duplicate If True, discard invests that are
395  duplicates of non-invests. This feature is disabled if
396  unrenumber is enabled or cleaning is disabled.
397  @param clean If clean is True (the default), then
398  self.clean_up_vitals is called, which will (among other
399  things) delete vitals lines that have the same time and storm
400  ID. The cleaning is done after unrenumbering, so if both
401  options are turned on, the result will contain only one entry
402  per storm ID per time, but with all storm IDs that are
403  available for a given storm at any one time."""
404 
405  if not self.is_cleaned: self.clean_up_vitals()
406  self.is_cleaned=False
407 
408  if not threshold: threshold=0
409  threshold=int(threshold)
410 
411  lastvit=dict()
412  debug=self.debug and self.logger is not None
413  logger=self.logger
414 
415  for vital in reversed(self.vitals):
416  if debug: logger.debug('VITAL %s'%(vital.line,))
417  key=vital.stormid3
418  if vital.stnum>=50 and vital.stnum<90:
419  continue # discard test and internal use numbers
420  elif vital.stnum<90:
421  lastvit[vital.stormid3]=vital
422  continue
423  elif self.renumber_one(vital,lastvit,0,0,threshold):
424  if debug: logger.debug(' -- done renumbering this one')
425  continue
426  if debug:
427  logger.debug(
428  ' - SEARCH AGAIN: subtract storm motion from later cycle')
429  if self.renumber_one(vital,lastvit,0,-6,threshold):
430  continue
431  if debug:
432  logger.debug(
433  ' - SEARCH AGAIN: add storm motion to earlier cycle')
434  if self.renumber_one(vital,lastvit,6,0,threshold):
435  continue
436  if debug:
437  logger.debug(
438  ' - SEARCH AGAIN: add half motion to later and '
439  'earlier cycle')
440  if self.renumber_one(vital,lastvit,3,-3,threshold):
441  continue
442  if unrenumber:
443  self.swap_numbers()
444  if clean:
445  if not unrenumber and discard_duplicates:
446  if logger is not None:
447  logger.info('Delete Invests that are duplicates of non-Invests.')
449  if logger is not None:
450  logger.info('Clean up the vitals again after renumbering...')
452 
454  """!Deletes Invest entries that have the same location and time
455  as non-invest entries."""
456  o=collections.defaultdict(dict)
457  # First pass: put in all entries with stnum<50. If more than
458  # one such entry has the same location, the last is kept.
459  # Locations are rounded to the nearest 0.2 degrees to allow
460  # for slight adjustments in lat/lon to be considered the same
461  # location.
462  for v in self.vitals:
463  if v.stnum<50:
464  k=(int(v.lat*5), int(v.lon*5))
465  o[v.YMDH][k]=v
466  # Second pass: put in storms with id >90 if no other storm has
467  # the same location.
468  for v in self.vitals:
469  if v.stnum>=90:
470  k=(int(v.lat*5), int(v.lon*5))
471  if k not in o[v.YMDH]:
472  o[v.YMDH][k]=v
473  # Final pass: create the new list:
474  l=list()
475  for yv in o.itervalues():
476  for v in yv.itervalues():
477  l.append(v)
478  self.vitals=l
479 
480  def swap_numbers(self):
481  """!Calls swap_numbers on all vitals to swap old and new storm IDs."""
482  for vital in self.vitals:
483  vital.swap_numbers()
484  self.is_cleaned=False
485 
487  """!Duplicates all vitals that have been renumbered, creating
488  one StormInfo with the old number and one with the new
489  number."""
490  newvit=list()
491  for vital in self.vitals:
492  newvit.append(vital)
493  if 'old_stormid3' in vital.__dict__:
494  newvit.append(vital.old())
495  self.vitals=newvit
496  self.is_cleaned=False
497 
498  def discard_except(self,keep_condition):
499  """!Discards all vitals except those for which the
500  keep_condition function returns True.
501 
502  @param keep_condition A function that receives a StormInfo
503  object as its only argument, returning True if the vital
504  should be kept and False if not.
505  @note The list will be unmodified if an exception is thrown."""
506  newvit=list()
507  for vit in self.vitals:
508  if keep_condition(vit):
509  newvit.append(vit)
510  self.vitals=newvit
511 
512  ##################################################################
513 
514  def swap_names(self):
515  """!This subroutine undoes the effect of renaming by swapping
516  old and new names"""
517  for vital in self.vitals:
518  if hasattr(vital,'old_stormname'):
519  (vital.old_stormname,vital.stormname) = \
520  (vital.stormname,vital.old_stormname)
521 
522  def rename(self):
523  """!This subroutine renames storms so that they have the last name seen
524  for their storm number."""
525  # Rename storms to have the last name seen
526  logger=self.logger
527  debug=self.debug and logger is not None
528  lastname=dict()
529  for vital in reversed(self.vitals):
530  key=vital.stormid3
531  if key in lastname:
532  name=lastname[key]
533  if vital.stormname!=name:
534  if debug:
535  logger.debug('Rename to %s: %s'%(name,vital.line))
536  vital.rename_storm(name)
537  if debug: logger.debug('Now: %s'%(vital.line,))
538  else:
539  lastname[key]=vital.stormname[0:9]
540 
541  def add_stormtype(self):
542  """!Add the storm type parameter from the CARQ entries in the A
543  deck."""
544  logger=self.logger
545  debug=self.debug and logger is not None
546  for vital in self.vitals:
547  lsid=vital.longstormid.lower()
548  when=vital.YMDH
549  if not lsid in self.carqdat:
550  self.readcarq(lsid) # try to read this storm's data
551  if lsid not in self.carqdat:
552  if logger is not None:
553  logger.warning('storm %s: no CARQ data found. '
554  'Using stormtype XX.'%(lsid,))
555  vital.set_stormtype('XX')
556  continue
557  carq=self.carqdat[lsid]
558  if when not in carq:
559  if logger is not None:
560  logger.warning('storm %s cycle %s: no CARQ data for '
561  'this cycle. Using stormtype XX.'
562  %(lsid,when))
563  vital.set_stormtype('XX')
564  continue
565  vital.set_stormtype(carq[when])
566 
567  def sort_by_function(self,cmpfun):
568  """!Resorts the vitals using the specified cmp-like function.
569  @param cmpfun a cmp-like function for comparing hwrf.storminfo.StormInfo objects"""
570  self.vitals=sorted(self.vitals,cmp=cmpfun)
571 
572  def sort_by_storm(self):
573  """!Resorts the vitals by storm instead of date. See
574  hwrf.storminfo.vit_cmp_by_storm for details."""
575  self.vitals=sorted(self.vitals,cmp=hwrf.storminfo.vit_cmp_by_storm)
576  def __iter__(self):
577  """!Iterates over all vitals, yielding StormInfo objects for
578  each one."""
579  for x in self.vitals: yield x
580  def each(self,stormid=None,old=False):
581  """!Iterates over all vitals that match the specified stormid.
582  If no stormid is given, iterates over all vitals.
583 
584  @param stormid the storm ID to search for. This can be
585  a stormid3, stormid4 or longstormid.
586  @param old If old=True, also searches the old_ copy of the
587  stormid. Any of stormid3, stormid4 or longstormid are
588  accepted."""
589  # Define a lexical scope function "selected" that will tell us
590  # if a StormInfo object should be printed:
591  if stormid is None:
592  def selected(vital): return True
593  else:
594  stormid=str(stormid).upper()
595  if re.search('\A\d\d[a-zA-Z]\Z',stormid):
596  def selected(vital): return vital.stormid3==stormid
597  if old:
598  def old_selected(vital):
599  return 'old_stormid3' in vital.__dict__ and \
600  vital.old_stormid3==stormid
601  elif re.search('\A[a-zA-Z]{2}\d\d\Z',stormid):
602  def selected(vital): return vital.stormid4==stormid
603  if old:
604  def old_selected(vital):
605  return 'old_stormid4' in vital.__dict__ and \
606  vital.old_stormid4==stormid
607  elif re.search('\A[a-zA-Z]{2}\d{6}\Z',stormid):
608  def selected(vital): return vital.longstormid==stormid
609  if old:
610  def old_selected(vital):
611  return 'old_longstormid' in vital.__dict__ and \
612  vital.old_longstormid==stormid
613  else:
614  raise RevitalError('Invalid storm id %s. It must be '
615  'one of these three formats: 04L '
616  'AL04 AL042013'%(str(val),))
617 
618  # Loop over all vitals sending them to the given stream:
619  for vit in self.vitals:
620  if selected(vit):
621  yield vit
622  elif old:
623  if old_selected(vit):
624  yield vit
625  def print_vitals(self,stream,renumberlog=None,format='line',stormid=None,
626  old=False):
627  """!Print the vitals to the given stream in a specified format.
628 
629  @param stream The stream (eg.: opened file) to receive the vitals.
630  @param format Either "tcvitals" to reformat as tcvitals (cleaning up
631  any errors); or "line" to simply print the original data for each
632  line; or "HHS" to use the HHS output format. (Do not use the "HHS"
633  option unless you are HHS.)
634  @param renumberlog If given, sends information about renaming and
635  renumbering of the vitals to a second stream.
636  @param stormid The "stormid" argument is used to restrict
637  printing to only a certain stormid.
638  @param old If True, then vitals with an old_stormid that matches are
639  also printed."""
640  for vit in self.each(stormid=stormid,old=old):
641  if renumberlog is not None:
642  xstormid3=vit.stormid3
643  oldid=getattr(vit,'old_stormid3',xstormid3)
644  name=vit.stormname
645  oldname=getattr(vit,'old_stormname',name)
646  renumberlog.write('%10s %3s %3s %-9s %-9s\n'%
647  (vit.YMDH,oldid,xstormid3,oldname[0:9],name[0:9]))
648  if format=='tcvitals':
649  print>>stream, vit.as_tcvitals()
650  elif format=='renumbering':
651  s=vit.as_tcvitals()
652  oldid=getattr(vit,'old_stormid3',vit.stormid3)
653  oldname=getattr(vit,'old_stormname',vit.stormname)
654  print>>stream,'%3s %9s => %s'%(oldid,oldname,s)
655  elif format=='HHS':
656  print>>stream, '%s %s "TCVT"'%(vit.longstormid.lower(),vit.YMDH)
657  else:
658  print>>stream, vit.line
659 
660  def hrd_multistorm_sorter(self,a,b):
661  """!A drop-in replacement for "cmp" that can be used for sorting or
662  comparison. Returns -1 if a<b, 1 if a>b or 0 if a=b. Decision is
663  made in this order:
664 
665  1. User priority (a.userprio): lower (priority 1) is "more
666  important" than higher numbers (priority 9999 is fill value).
667 
668  2. Invest vs. non-invest: invest is less important
669 
670  3. wind: stronger wind is more important than weaker wind
671 
672  4. North Atlantic (L) storms: farther west is more important
673 
674  5. North East Pacific (E) storms: farther East is more important
675 
676  If all of the above values are equal, 0 is returned.
677  @returns -1, 0 or 1
678  @param a,b the vitals to compare"""
679  a_userprio=getattr(a,'userprio',9999)
680  b_userprio=getattr(b,'userprio',9999)
681  a_invest=1 if (a.stormname=='INVEST') else 0
682  b_invest=1 if (b.stormname=='INVEST') else 0
683 
684  c = cmp(a_userprio,b_userprio) or cmp(a_invest,b_invest) or\
685  -cmp(a.wmax,b.wmax) or\
686  (a.basin1=='L' and b.basin1=='L' and cmp(a.lon,b.lon)) or \
687  (a.basin1=='E' and b.basin1=='E' and -cmp(a.lon,b.lon))
688  return c
689 
691  """!Does nothing."""
692  pass
def copy(self)
Returns a deep copy of this Revital.
Definition: revital.py:142
def clean_up_vitals
Given a list of StormInfo, sorts using the vitcmp comparison, discards suspect storm names and number...
Definition: storminfo.py:187
This module provides a set of utility functions to do filesystem operations.
Definition: fileop.py:1
def swap_names(self)
This subroutine undoes the effect of renaming by swapping old and new names.
Definition: revital.py:514
vitals
The list of hwrf.storminfo.StormInfo objects being revitalized.
Definition: revital.py:76
def move_latlon(self, vital, dt)
Returns a tuple containing the latitude and longitude of the storm at a different position according ...
Definition: revital.py:202
def readfiles
Reads the list of files and parses them as tcvitals files.
Definition: revital.py:169
def rename(self)
This subroutine renames storms so that they have the last name seen for their storm number...
Definition: revital.py:522
def renumber
Renumbers storms with numbers 90-99, if possible, to have the same number as later 1-49 numbered stor...
Definition: revital.py:382
def renumber_one(self, vital, lastvit, vit_motion, other_motion, threshold)
Internal function that handles renumbering of storms.
Definition: revital.py:265
def print_vitals
Print the vitals to the given stream in a specified format.
Definition: revital.py:626
def mirror_renumbered_vitals(self)
Duplicates all vitals that have been renumbered, creating one StormInfo with the old number and one w...
Definition: revital.py:486
Sets up signal handlers to ensure a clean exit.
Definition: sigsafety.py:1
debug
If True, send numerous extra log messages at DEBUG level.
Definition: revital.py:81
def __init__
Creates a Revital object:
Definition: revital.py:43
logger
The logging.Logger to use for log messages.
Definition: revital.py:80
search_dx
Search radius in km for deciding whether two storms are the same.
Definition: revital.py:78
def parse_tcvitals
Reads data from a tcvitals file.
Definition: storminfo.py:302
def append(self, vital)
Appends a vital entry to self.vitals.
Definition: revital.py:130
Defines StormInfo and related functions for interacting with vitals ATCF data.
Definition: storminfo.py:1
Base class of errors related to rewriting vitals.
Definition: revital.py:18
This exception is raised when an argument to the Revital constructor is invalid.
Definition: revital.py:20
def readvitals
Same as readfiles except the tcvitals have already been parsed and the vitals are being passed in...
Definition: revital.py:147
def multistorm_priority(self)
Does nothing.
Definition: revital.py:690
def readcarq(self, longstormid)
Tries to find the CARQ data for the specified storm.
Definition: revital.py:222
def clean_up_vitals
Calls the hwrf.storminfo.clean_up_vitals on this object's vitals.
Definition: revital.py:243
Time manipulation and other numerical routines.
Definition: numerics.py:1
Base class of all exceptions in this module.
Definition: exceptions.py:8
carqfail
Set of longstormid entries that had no CARQ data.
Definition: revital.py:75
def extend(self, vitals)
Given the specified iterable object, appends its contents to my own.
Definition: revital.py:136
def delete_invest_duplicates(self)
Deletes Invest entries that have the same location and time as non-invest entries.
Definition: revital.py:453
is_cleaned
Has clean_up_vitals() been called since the last operation that modified the vitals?
Definition: revital.py:93
def each
Iterates over all vitals that match the specified stormid.
Definition: revital.py:580
def discard_except(self, keep_condition)
Discards all vitals except those for which the keep_condition function returns True.
Definition: revital.py:498
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
adeckdir
Directory with A deck files.
Definition: revital.py:83
Exceptions raised by the hwrf package.
Definition: exceptions.py:1
carqdat
Contains CARQ entries read from the adeckdir A deck files.
Definition: revital.py:70
def add_stormtype(self)
Add the storm type parameter from the CARQ entries in the A deck.
Definition: revital.py:541
def sort_by_function(self, cmpfun)
Resorts the vitals using the specified cmp-like function.
Definition: revital.py:567
def hrd_multistorm_sorter(self, a, b)
A drop-in replacement for "cmp" that can be used for sorting or comparison.
Definition: revital.py:660
invest_number_name
Rename stores to have the last non-INVEST name seen.
Definition: revital.py:82
def sort_by_storm(self)
Resorts the vitals by storm instead of date.
Definition: revital.py:572
search_dt
Search timespan for finding two storms that are the same.
Definition: revital.py:79
def __iter__(self)
Iterates over all vitals, yielding StormInfo objects for each one.
Definition: revital.py:576
Storm vitals information from ATCF, B-deck, tcvitals or message files.
Definition: storminfo.py:411
def swap_numbers(self)
Calls swap_numbers on all vitals to swap old and new storm IDs.
Definition: revital.py:480
This class reads one or more tcvitals files and rewrites them as requested.
Definition: revital.py:38