HWRF  trunk@4391
setup_hurricane.py
1 #! /usr/bin/env python
2 
3 ##@namespace ush.setup_hurricane
4 # @brief This script is run by the NOAA Senior Duty Meteorologist four times
5 # a day to generate the list of storms for the HWRF and GFDL hurricane
6 # models to run.
7 #
8 # This is an interactive program that uses the curses library to make
9 # a text-base, mouse-capable interface for deciding whether to run the
10 # HWRF and GFDL models for each storm. The list of possible storms
11 # are sent by the National Hurricane Center (NHC) and Joint Typhoon
12 # Warning Center. The script is able to use the data those centers
13 # send in real-time, and also the archived TCVitals database, which
14 # contains years of message files.
15 #
16 # The setup_hurricane script is configured using the following
17 # UNIX conf file:
18 # @code{.conf}
19 # [setup_hurricane]
20 # deliver=no ; should the final messages be delivered (yes) or just printed (no)
21 # source=tcvitals ; get data from the historical tcvitals
22 # source=stormfiles ; get data from the real-time locations instead (comment out one)
23 # envir={ENV[envir|-test]} ; run environment: prod, para, test
24 # gfdl_output=/com2/hur/{envir}/inpdata ; where to place GFDL messages
25 # hwrf_output=/com2/hur/{envir}/inphwrf ; where to place HWRF messages
26 # maxgfdl=5 ; maximum number of GFDL storms
27 # maxhwrf=8 ; maximum number of HWRF storms
28 # nhc_max_storms=8 ; maximum number of NHC storms
29 # jtwc_max_storms=9 ; maximum number of JTWC storms
30 # dcomroot={ENV[DCOMROOT|-/dcom/us007003]} ; dcom directory
31 # nhcdir={ENV[nhcdir|-/nhc/save/guidance/storm-data/ncep]} ; nhc storm directoyr
32 # nhc_input={nhcdir}/storm{istorm} ; nhc storm file name and path
33 # jtwc_input={dcomroot}/{YMD}/wtxtbul/storm_data/storm{istorm} ; jtwc storm file path
34 # tcvitals={ENV[COMINARCH|-/com/arch/prod/syndat]}/syndat_tcvitals.{year} ; tcvitals location
35 # @endcode
36 #
37 # It finds the file from one of several locations, checked in this order:
38 # * $SETUP_HURRICANE_CONF environment variable
39 # * parm/setup_hurrucane_$USER.conf
40 # * parm/setup_hurricane.conf
41 
42 import logging, sys, os, curses, collections, time, datetime, re, StringIO
45 from hwrf.storminfo import InvalidVitals
46 
47 def basin_center_checker(vl):
48  """!Given a list of StormInfo objects, iterates over those that
49  have the right basins for the right centers. Rejects JTWC "L"
50  basin storms, and rejects basins other than: ABWSPECQXL. The "X"
51  basin is a fake basin used to report parser and I/O errors in
52  message files. Rejects all forecast centers except NHC and JTWC
53  @param vl a list of hwrf.storminfo.StormInfo objects"""
54  for vital in vl:
55  center=vital.center
56  if center not in ('JTWC',"NHC"): continue
57  if vital.basin1 not in 'ABWSPECQXL': continue
58  yield vital
59 
60 def name_number_checker(vl):
61  """!Given an array of StormInfo objects, iterate over those that
62  have valid names and numbers. Discards "UNKNOWN" named storms,
63  and numbers 50-79. Discards "TEST" named storms if the number is
64  <50 or >=90.
65  @param vl a list of hwrf.storminfo.StormInfo objects"""
66  for vital in vl:
67  if vital.stormname=='UNKNOWN' or \
68  (vital.stnum>50 and vital.stnum<90):
69  continue
70  if vital.stormname=='TEST' and (vital.stnum<50 or vital.stnum>90):
71  continue
72  yield vital
73 
74 def sort_vitals(a,b):
75  """!Comparison function for sorting storms by center, then
76  priority, then by wind. Lower priority numbers indicate more
77  important storms, and hence are listed first.
78  @param a,b hwrf.storminfo.StormInfo objects to order"""
79  if a.center=='NHC' and b.center=='JTWC':
80  # NHC goes before JTWC:
81  return -1
82  elif a.center=='JTWC' and b.center=='NHC':
83  # JTWC goes after NHC:
84  return 1
85  elif hasattr(a,'priority') and hasattr(b,'priority') and \
86  a.priority!=b.priority:
87  # Lower priorities go after higher priorities:
88  return cmp(a.priority,b.priority)
89  if hasattr(a,'source') and hasattr(b,'source') and \
90  a.source!=b.source:
91  return -cmp(a.source,b.source)
92  return -cmp(a.wmax,b.wmax) or cmp(a.stnum,b.stnum)
93 
94 
95 def sort_by_prio(a,b):
96  """!Comparison function for sorting storms by priority. Top five
97  NHC go on top, followed by the top five JTWC, then the remaining
98  NHC, then the remaining JTWC. Has no tie breaker for equal
99  priority.
100  @param a,b hwrf.storminfo.StormInfo objects to order"""
101  ap=a.priority
102  bp=b.priority
103  if a.center!='NHC': ap+=10
104  if b.center!='NHC': bp+=10
105  if a.priority>8 and a.priority<10: ap+=20
106  if b.priority>8 and b.priority<10: bp+=20
107  if a.priority<1 or a.priority>9: ap+=40
108  if b.priority<1 or b.priority>9: bp+=40
109  return cmp(ap,bp)
110 
111 def fake_prio(rv,firstprio=None):
112  """!This function generates fake NHC and JTWC storm priority
113  information for vitals that have none, simply based on the order
114  the storm showes up in the rv.vitals list. The optional second
115  argument is the first number to use as the storm priority. Any
116  vitals that have a priority will not be modified.
117  @param rv an iterable of hwrf.storminfo.StormInfo objects
118  @param firstprio lowest priority number to assign"""
119  if firstprio is None:
120  firstprio=1
121  firstprio=int(firstprio)-1
122  d=collections.defaultdict(lambda: firstprio)
123  for v in rv:
124  if not hasattr(v,'priority'):
125  prio=d[v.center]+1
126  setattr(v,'priority',prio)
127  d[v.center]=prio
128 
129 class StormCurses(object):
130  """!This class implements a user interface for selecting which
131  storms GFDL and HWRF should run."""
132  def __init__(self,vitals,YMDH,logger=None,maxhwrf=8,maxgfdl=5,
133  fake_sources=False):
134  """!Creates a StormCurses object that will assist the SDM in
135  choosing between the storms in the listed vitals.
136  @param vitals the list of hwrf.storminfo.StormInfo objects to select from
137  @param YMDH the cycle of interest
138  @param logger a logging.Logger for log messages
139  @param maxhwrf maximum number of HWRF storms
140  @param maxgfdl maximum number of GFDL storms
141  @param fake_sources if True, we're using TCVitals for a test run;
142  if False, we're using the NHC and JTWC storm files"""
143  when=hwrf.numerics.to_datetime(YMDH)
144  self.YMDH=when.strftime('%Y%m%d%H')
146  strftime('%Y%m%d%H')
147  self.messagequeue=list()
148  self.vitals=[x for x in vitals]
149  self.stdscr=None
150  self.logger=logger
151  self.maxhwrf=maxhwrf
152  self.maxgfdl=maxgfdl
153  self.fake_sources=fake_sources
154  self.resort()
155  self.C_NORMAL=None
156  self.C_SELECT=None
157  self.C_WARN=None
158  self.C_WARN_SELECT=None
159  self.C_OCEAN=None
160  self.C_LAND=None
161  self.warnings=[ collections.defaultdict(list) for x in self.vitals ]
162  # The warnings data structure provides a list of warnings
163  # about each field in each vital line. This is a
164  # two-dimensional array of lists. It is accessed as:
165  # self.warnings[i][field] = [ (reason1,details1),
166  # (reason2,details2), ... ]
167  # where i is the index in self.vitals and "field" is the name
168  # of the vitals field (getattr(self.vitals[i],field)) The
169  # (reason1,details1) tuple contains a short 1-2 word
170  # explanation of the problem (reason1) and a detailed
171  # explanation (details1)
172 
173  # Flags for "model X cannot run this storm:"
174  self.gfdlcannot=[False]*len(self.vitals)
175  self.hwrfcannot=[False]*len(self.vitals)
176 
177  # Flags for "model X WILL run this storm:"
178  self.gfdlwill=[False]*len(self.vitals)
179  self.hwrfwill=[False]*len(self.vitals)
180 
181  # Set the default values for the cannot and will flags, and
182  # fill the self.warnings data structure:
183  self.init_hwrf_gfdl()
184 
185  ##@var YMDH
186  # the cycle of interst
187 
188  ##@var YMDHm6
189  # the cycle before the cycle of interest
190 
191  ##@var messagequeue
192  # a list of messages to display
193 
194  ##@var vitals
195  # a list of hwrf.storminfo.StormInfo to select from
196 
197  ##@var stdscr
198  # the curses screen used for display of text
199 
200  ##@var logger
201  # a logging.Logger for log messages
202 
203  ##@var maxhwrf
204  # Maximum number of HWRF storms allowed.
205 
206  ##@var maxgfdl
207  # Maximum number of GFDL storms allowed.
208 
209  ##@var fake_sources
210  # True=tcvitals in use, False=storm files
211 
212  ##@var C_NORMAL
213  # Normal font
214 
215  ##@var C_SELECT
216  # Font for selected text
217 
218  ##@var C_WARN
219  # Font for text of storms that have warning messages
220 
221  ##@var C_WARN_SELECT
222  # Font for text of storms that are selected AND have warning messages
223 
224  ##@var C_OCEAN
225  # Unused: font for ocean locations on the map
226 
227  ##@var C_LAND
228  # Unused: font for land locations on the map
229 
230  ##@var warnings
231  # A mapping from storm to list of warning messages for that storm
232 
233  ##@var gfdlcannot
234  # Array of logical telling whether each storm cannot be run by GFDL
235 
236  ##@var hwrfcannot
237  # Array of logical telling whether each storm cannot be run by HWRF
238 
239  ##@var gfdlwill
240  # Array of logical telling whether GFDL will be run by each storm
241 
242  ##@var hwrfwill
243  # Array of logical telling whether HWRF will be run by each storm
244 
245  def __enter__(self):
246  """!Sets up the curses library. Use in a Python "with" block."""
247  self.stdscr=curses.initscr()
248  curses.start_color()
249  curses.use_default_colors()
250  curses.noecho()
251  curses.cbreak()
252  curses.mousemask(1)
253  self.stdscr.keypad(1)
254 
255  curses.init_pair(1,-1,-1)
256  self.C_NORMAL=curses.color_pair(1)
257 
258  curses.init_pair(2,curses.COLOR_RED,-1)
259  self.C_WARN=curses.color_pair(2)
260 
261  curses.init_pair(3,curses.COLOR_WHITE,curses.COLOR_RED)
262  self.C_WARN_SELECT=curses.color_pair(3)|curses.A_BOLD
263 
264  curses.init_pair(4,curses.COLOR_WHITE,curses.COLOR_BLUE)
265  self.C_OCEAN=curses.color_pair(4)|curses.A_BOLD
266 
267  curses.init_pair(5,curses.COLOR_WHITE,curses.COLOR_GREEN)
268  self.C_LAND=curses.color_pair(5)|curses.A_BOLD
269 
270  self.stdscr.attrset(self.C_NORMAL)
271 
272  def __exit__(self,type,value,tb):
273  """!Ends the curses library and restores standard terminal
274  functions. Use in a Python "with" block.
275  @param type,value,tb exception information"""
276  self.stdscr.keypad(0)
277  curses.nocbreak()
278  curses.echo()
279  curses.endwin()
280  self.stdscr=None
281 
282  def test_screen(self):
283  """!This routine is for testing only. It displays text with
284  all color combinations used by this class."""
285  self.stdscr.clear()
286  self.addstr(3,2,'TEST NORMAL',self.C_NORMAL)
287  self.addstr(5,2,'TEST WARN',self.C_WARN)
288  self.addstr(7,2,'TEST HIGHLIGHTED',
289  self.C_NORMAL|curses.A_STANDOUT)
290  self.addstr(9,2,'TEST HIGHLIGHTED WARN',
291  curses.A_STANDOUT|self.C_WARN)
292  self.addstr(11,2,'TEST OCEAN',self.C_OCEAN)
293  self.addstr(13,2,'TEST LAND',self.C_LAND)
294  self.stdscr.refresh()
295 
297  """!Sets the "hwrfmessage" and "gfdlmessage" attributes in all
298  of self.vitals[*] to "messageN" (for an integer N), "-CANNOT-"
299  or "---NO---" using setattr. Uses self.hwrfwill,
300  self.gfdlwill, self.hwrfcannot and self.gfdlcannot to make
301  these judgements."""
302  igfdl=0
303  ihwrf=0
304  for i in xrange(len(self.vitals)):
305  if self.hwrfwill[i]:
306  ihwrf+=1
307  setattr(self.vitals[i],'hwrfmessage','message%d'%ihwrf)
308  elif self.hwrfcannot[i]:
309  setattr(self.vitals[i],'hwrfmessage','-CANNOT-')
310  else:
311  setattr(self.vitals[i],'hwrfmessage','---NO---')
312  if self.gfdlwill[i]:
313  igfdl+=1
314  setattr(self.vitals[i],'gfdlmessage','message%d'%igfdl)
315  elif self.gfdlcannot[i]:
316  setattr(self.vitals[i],'gfdlmessage','-CANNOT-')
317  else:
318  setattr(self.vitals[i],'gfdlmessage','---NO---')
319 
320  def init_hwrf_gfdl(self):
321  """!Decides if HWRF and GFDL can or should run each storm
322  listed in the vitals. Sets any warning or error flags for
323  various fields."""
324  i=-1
325  gfdlcount=0
326  hwrfcount=0
327  hwrfdisable=[False] * len(self.hwrfwill)
328  for v in self.vitals:
329  i+=1
330  if v.YMDH!=self.YMDH:
331  self.adderr(i,'source','wrong cycle',
332  'This data is for the wrong cycle: %s.'%(v.YMDH,))
333  if v.center!='NHC':
334  self.gfdlcannot[i]="Not NHC."
335  if v.basin1 not in 'LEC':
336  self.gfdlcannot[i]="Wrong basin."
337  if getattr(v,'invalid',False) is True:
338  self.hwrfcannot[i]="Invalid vitals."
339  self.gfdlcannot[i]="Invalid vitals."
340  self.adderr(i,'stormname',v.stormname,
341  getattr(v,'explanation','Invalid vitals.'))
342  source=getattr(v,'source','unknown')
343  if source=='extrapolated tcvitals':
344  self.addwarn(i,'source','extrapolated',
345  'Extrapolated from the previous cycle\'s tcvitals.')
346  if v.wmax>=30:
347  self.addwarn(i,'wmax','missing',
348  'Strong storm was not requested (missing bulletin?)')
349  self.addwarn(i,'center','??',
350  'PROBABLE COMMUNICATION ERROR BETWEEN JTWC AND NCEP.')
351  self.addwarn(i,'center','??',
352  'SUGGEST CALLING JTWC, RUN THIS STORM IF IT IS REAL.')
353  else:
354  # Disable HWRF by default for weak extrapolated storms:
355  hwrfdisable[i]=True
356  # Always disable GFDL for extrapolated storms. This
357  # is redundant because these are JTWC, which GFDL does
358  # not run:
359  self.gfdlcannot[i]='this is extrapolated tcvitals data.'
360  self.gfdlwill[i]=False
361  if source=='tcvitals':
362  self.addwarn(i,'center','??',
363  'PROBABLE COMMUNICATION ERROR BETWEEN JTWC AND NCEP.')
364  self.addwarn(i,'center','??',
365  'SUGGEST CALLING JTWC, RUN THIS STORM IF IT IS REAL.')
366  self.addwarn(i,'source','TCVitals',
367  'These vitals are from the tcvitals, not storm files.')
368  if v.basin1 not in 'LECWPQSAB':
369  self.adderr(i,'stormid3','unknown basin'
370  'The only supported basins are: '
371  'L E C W P Q S A B')
372  if v.wmax<10:
373  self.addwarn(i,'wmax','Vmax<10m/s',
374  'Wind is very weak.')
375  if v.wmax>80:
376  self.addwarn(i,'wmax','Vmax>80m/s',
377  'Wind is very strong.')
378  if v.pmin<890:
379  self.addwarn(i,'pmin','Pmin<890',
380  'Extremely low pressure (<890 mbar)')
381  if v.pmin>1012:
382  self.addwarn(i,'pmin','Pmin>1012',
383  'Extremely high pressure (>1012 mbar)')
384  if v.pmin>v.poci:
385  self.addwarn(i,'pmin','Pmin>Penvir',
386  'ERROR! Central pressure is higher than '
387  'outermost closed isobar! HWRF will fail.')
388  self.addwarn(i,'poci','Pmin>Penvir',
389  'ERROR! Central pressure is higher than '
390  'outermost closed isobar! HWRF will fail.')
391  if v.basin1 in 'LECWAB' and v.lat<=0:
392  self.addwarn(i,'lat','south',
393  'latitude should be >0 for LECWAB basins')
394  self.addwarn(i,'stormid3','south',
395  'latitude should be >0 for LECWAB basins')
396  if v.basin1 in 'PQS' and v.lat>=0:
397  self.addwarn(i,'lat','north',
398  'latitude should be <0 for PQS basins')
399  self.addwarn(i,'stormid3','north',
400  'latitude should be <0 for PQS basins')
401  if v.lat>60 or v.lat<-60:
402  self.addwarn(i,'lat','subarctic',
403  'latitude is far away from the tropics')
404  if v.lat<5 and v.lat>-5:
405  self.addwarn(i,'lat','equatorial',
406  'latitude is very close to the equator')
407  if gfdlcount<self.maxgfdl:
408  self.gfdlwill[i] = not self.gfdlcannot[i]
409  if self.gfdlwill[i]: gfdlcount+=1
410  if hwrfcount<self.maxhwrf:
411  self.hwrfwill[i] = not hwrfdisable[i] and not self.hwrfcannot[i]
412  if self.hwrfwill[i]: hwrfcount+=1
413  self.make_storm_indices()
414 
415  def toggle_run(self,istorm,hwrf=True,gfdl=True):
416  """!Turns on or off the GFDL and/or HWRF model for the storm at
417  index istorm of self.vitals.
418  @param istorm index of the storm in self.vitals
419  @param hwrf if True, toggle HWRF
420  @param gfdl if True, toggle GFDL"""
421  if istorm>=len(self.vitals): return
422  nhwrf=0
423  ngfdl=0
424  for i in xrange(len(self.vitals)):
425  if self.hwrfwill[i]: nhwrf+=1
426  if self.gfdlwill[i]: ngfdl+=1
427  i=istorm
428  if hwrf:
429  if self.hwrfcannot[i]:
430  self.messagequeue.append('%s: HWRF cannot run: %s'%(self.vitals[i].stormid3,self.hwrfcannot[i]))
431  else:
432  if self.hwrfwill[i]:
433  self.hwrfwill[i]=False
434  elif nhwrf>=self.maxhwrf:
435  self.messagequeue.append('Too many HWRF storms.')
436  else:
437  self.hwrfwill[i]=True
438  if gfdl:
439  if self.gfdlcannot[i]:
440  self.messagequeue.append('%s: GFDL cannot run: %s'%(self.vitals[i].stormid3,self.gfdlcannot[i]))
441  else:
442  if self.gfdlwill[i]:
443  self.gfdlwill[i]=False
444  elif ngfdl>=self.maxgfdl:
445  self.messagequeue.append('Too many GFDL storms.')
446  else:
447  self.gfdlwill[i]=True
448  self.make_storm_indices()
449 
450  def hasmessage(self,i,field):
451  """!Returns True if there are warnings or errors for storm i,
452  and False otherwise.
453  @param i index of the storm in self.vitals
454  @param field the field of interest"""
455  if field not in self.warnings[i]:
456  return False
457  warnings=self.warnings[i][field]
458  return len(warnings)>0
459 
460  def adderr(self,i,field,reason,details,gfdl=True,hwrf=True):
461  """!Records that vitals at index i cannot be used by either
462  model due to an error in the specified field. The "reason" is
463  a short string explaining why and the "details" is a longer
464  string with a full explanation.
465  @param i index of the storm in self.vitals
466  @param field the field that is having trouble
467  @param reason why the storm cannot run
468  @param details detailed reason why the storm cannot be run
469  @param gfdl,hwrf if True, that model cannot run"""
470  self.addwarn(i,field,reason,details)
471  if hwrf: self.hwrfcannot[i]=reason
472  if gfdl: self.gfdlcannot[i]=reason
473 
474  def addwarn(self,i,field,reason,details):
475  """Records that there is a problem with the specified field in
476  the vitals entry at index i. The "reason" variable gives a
477  short 1-2 word reason, while the "details" variable gives a
478  potentially long explanation of what is wrong.
479  @param i index of the storm in self.vitals
480  @param field the field with problems
481  @param reason,details short and long explanation of the problem"""
482  self.warnings[i][field].append([reason,details])
483 
484  def quit_confirmation(self):
485  """!Clears the screen and informs the user that they asked to
486  quit. Waits for a keypress and then returns True."""
487  self.stdscr.clear()
488  self.addstr(0,0,
489  'You have asked to quit without setting up the models.')
490  self.addstr(1,0,'Press any key to quit...')
491  self.stdscr.refresh()
492  self.stdscr.getch()
493  return True
494 
496  """!Clears the screen and shows a setup confirmation screen,
497  displaying what models will run what storms. Asks the user if
498  they're sure. Returns True if the user is sure, or False
499  otherwise."""
500  self.stdscr.clear()
501  self.addstr(0,0,
502  'You have asked to setup the following simulations:')
503 
504  self.addstr(1,0,'HWRF:')
505  nhwrf=0
506  i=2
507  for istorm in xrange(len(self.vitals)):
508  if not self.hwrfwill[istorm]: continue
509  more=''
510  v=self.vitals[istorm]
511  if hasattr(v,'source') and not v.source.startswith('storm'):
512  more='(source: %s)'%(v.source)
513  line=' {v.hwrfmessage:6s} = {v.center:4s} #{v.priority:1d} '\
514  '{v.stormid3:3s} {v.stormname:10s} {more:s}'.format(
515  v=v,more=more)
516  self.addstr(i,0,line)
517  i+=1
518  nhwrf+=1
519  if nhwrf==0:
520  self.addstr(1,6,'NO STORMS!')
521  else:
522  self.addstr(1,6,'%d storms:'%nhwrf)
523 
524  i0=i
525  self.addstr(i,0,'GFDL:')
526  i+=1
527  ngfdl=0
528  for istorm in xrange(len(self.vitals)):
529  if not self.gfdlwill[istorm]: continue
530  line=' {v.gfdlmessage:6s} = {v.center:4s} #{v.priority:1d} '\
531  '{v.stormid3:3s} {v.stormname:10s}'.format(
532  v=self.vitals[istorm])
533  self.addstr(i,0,line)
534  i+=1
535  ngfdl+=1
536  if ngfdl==0:
537  self.addstr(i0,6,'NO STORMS!')
538  else:
539  self.addstr(i0,6,'%d storms:'%ngfdl)
540 
541  self.addstr(i,0,'Type YES to confirm, or press any key to go back...')
542  i+=1
543  self.addstr(i,0,' Y E S ')
544  # 01234567890123456789
545  self.stdscr.refresh()
546  ikey=0
547  while True:
548  k=self.stdscr.getch()
549  if ikey==0 and k in (ord('y'),ord('Y')):
550  ikey+=1
551  self.addstr(i,3,'[-Y-]')
552  self.stdscr.refresh()
553  elif ikey==1 and k in (ord('e'),ord('E')):
554  ikey+=1
555  self.addstr(i,8,'[-E-]')
556  self.stdscr.refresh()
557  elif ikey==2 and k in (ord('s'),ord('S')):
558  ikey+=2
559  self.addstr(i,13,'[-S-]')
560  for x in xrange(3):
561  self.stdscr.refresh()
562  time.sleep(0.5)
563  self.addstr(i,0,'--- CONFIRMED - SETUP - SEQUENCE ---')
564  self.stdscr.refresh()
565  time.sleep(0.5)
566  self.addstr(i,0,'--- CONFIRMED - SETUP - SEQUENCE ---',
567  curses.A_STANDOUT)
568  return True
569  else:
570  self.addstr(i,0,' CANCEL - GOING BACK ')
571  self.stdscr.refresh()
572  time.sleep(0.5)
573  return False
574 
575  def show_storm_screen(self,ihighlight=None):
576  """!Prints the storm selection screen starting at line 0, and
577  returns the number of lines printed.
578  @param ihighlight index of the highlighted storm in self.vitals.
579  To highlight nothing, provide None or an invalid index."""
580  i=0
581  self.stdscr.clear()
582  i=self.show_storm_table(i,ihighlight)
583  self.addstr(i,0,' ')
584  i+=1
585  self.addstr(i,0,'Controls: [N]ext [P]rev, toggle [H]WRF [G]FDL [B]oth')
586  i+=1
587  self.addstr(i,0,'When done: [S]etup models or [Q]uit without doing anything')
588  i+=1
589  self.addstr(i,0,' ')
590  i+=1
591  if ihighlight is not None and len(self.vitals)>=ihighlight:
592  i=self.show_storm_details(i,ihighlight)
593  self.stdscr.refresh()
594  return i
595 
596  def show_storm_heading(self,iline):
597  """!Prints the storm selection table header starting at the
598  specified line, and returns iline+2.
599  @param iline the starting line"""
600  title ='RSMC Source SID Storm-Name -Lat- -Lon-- Vmax Pmin Penv --GFDL-- --HWRF--'
601  bar ='-------------------------------------------------------------------------'
602  iline=int(iline)
603  assert(iline>=0)
604  self.addstr(iline,0,title) ; iline+=1
605  self.addstr(iline,0,bar) ; iline+=1
606  return iline
607 
608  def show_storm_table_line(self,iline,istorm,highlight=False):
609  """!Prints one line of the storm list table, for storm
610  self.vitals[istorm], at line iline on the console. If
611  highlight=True, then the line is highlighted. Returns
612  iline+1.
613  @param iline the starting line
614  @param istorm index of the storm in self.vitals
615  @param highlight if True, highlight that line"""
616  formats=[ ('center','4s'), ('source','6s'),
617  ('stormid3','3s'), ('stormname','10s'),
618  ('lat','4.1f'),('lon','5.1f'),
619  ('wmax','5.1f'), ('pmin','4.0f'),('poci','4.0f') ,
620  ('gfdlmessage','6s'), ('hwrfmessage','6s'),]
621  i=int(istorm)
622  v=self.vitals[i]
623 
624  # Loop over format keys k, and format values f, constructing
625  # two variables for each: d=the data to print, and m=the
626  # display attributes.
627  y=int(iline)
628  x=0
629  first=True
630  for (k,f) in formats:
631  fmt='{0:'+f+'}'
632  if highlight:
633  m=curses.A_REVERSE|self.C_NORMAL
634  else:
635  m=self.C_NORMAL
636 
637  # Handle keys that are not in self.vitals[i]:
638  if k=='i': d=fmt.format(i+1,)
639  elif k=='source':
640  if v.priority>9:
641  if v.source=='extrapolated tcvitals':
642  strprio='extrap'
643  elif v.source=='tcvitals':
644  strprio='syndat'
645  else:
646  strprio='??????'
647  elif v.YMDH!=self.YMDH:
648  strprio='storm%d'%v.priority
649  else:
650  strprio='storm%d'%v.priority
651  d=fmt.format(strprio)
652  if self.hasmessage(i,k):
653  if highlight:
654  m=self.C_WARN_SELECT
655  else:
656  m=self.C_WARN|curses.A_BOLD
657  # Else, key is from self.vitals[i]:
658  else:
659  if self.hasmessage(i,k):
660  if highlight:
661  m=self.C_WARN_SELECT
662  else:
663  m=self.C_WARN|curses.A_BOLD
664 
665  val=getattr(v,k)
666  if k=='lat':
667  d=fmt.format(abs(val),)+( 'N' if(val>=0) else 'S' )
668  elif k=='lon':
669  d=fmt.format(abs(val),)+( 'E' if(val>=0) else 'W' )
670  else:
671  d=fmt.format(val,)
672 
673  # Put a space before each column except the first:
674  if not first:
675  d=' '+d
676  else:
677  first=False
678 
679  # Put the colored text on the screen:
680  try:
681  self.addstr(y,x,d,m)
682  except curses.error as e:
683  pass
684  x+=len(d)
685  return x
686 
687  def show_storm_table(self,iline,ihighlight=None):
688  """!Fills self.stdscr with a list of storms starting at line
689  iline on the screen. If ihighlight is not None, then it
690  specifies the 1-based index of the storm that is presently
691  highlighted.
692  @param iline first line
693  @param ihighlight index of the storm in self.vitals to highlight, or None to
694  highlight no storms"""
695  iline=self.show_storm_heading(iline)
696  for i in xrange(len(self.vitals)):
697  self.show_storm_table_line(iline,i,
698  ihighlight is not None and i==ihighlight)
699  iline+=1
700  return iline
701 
702  def addstr(self,y,x,s,a=None):
703  """!Puts a string s on the screen at the given y column and x
704  row. Optionally, sets the attributes a. This is a simple
705  wrapper around the curses addstr command. It ignores any
706  curses errors, which allows one to write a string that is not
707  entirely on the terminal.
708  @param y,x curses location
709  @param s string to print
710  @param a curses attributes
711  @returns the number of characters printed (0 or len(s))"""
712  try:
713  if a is None:
714  self.stdscr.addstr(y,x,s)
715  else:
716  self.stdscr.addstr(y,x,s,a)
717  except curses.error as e:
718  return 0
719  return len(s)
720 
721  def show_storm_details(self,iline,istorm):
722  """!Shows details about a storm self.vitals[istorm] starting on
723  the given line number.
724  @param iline the line number
725  @param istorm the index of the storm in self.vitals"""
726  if istorm>=len(self.vitals): return iline
727  istorm=int(istorm)
728  v=self.vitals[istorm]
729 
730  # Storm name and forecast center info:
731  priority=getattr(v,'priority',-999)
732  if priority is None or priority<0:
733  priority='none'
734  else:
735  priority='#%d'%(priority,)
736  line='{v.center:s} {priority:s} = {v.YMDH:s} {v.stormid3:3s} {v.stormname:s} (from {v.source:s})'\
737  .format(i=istorm+1,v=v,priority=priority)
738  self.addstr(iline,0,line)
739  iline+=1
740 
741  # location and movement:
742  latstr='%.1f%c'%(
743  abs(v.lat), 'N' if(v.lat>=0) else 'S')
744  lonstr='%.1f%c'%(
745  abs(v.lon), 'E' if(v.lon>=0) else 'W')
746 
747  line=' at {latstr:s} {lonstr:s} moving {v.stormspeed:.1f}m/s '\
748  'at {v.stormdir:.0f} degrees from north'.format(
749  lonstr=lonstr,latstr=latstr,v=v)
750  self.addstr(iline,0,line)
751  iline+=1
752 
753  # Intensity and wind radii (two lines):
754  line=' wind={v.wmax:.0f}m/s RMW={v.rmw:.0f}km R34: NE={v.NE34:.0f}, '\
755  'SE={v.SE34:.0f}, SW={v.SW34:.0f}, NW={v.NW34:.0f} km'.format(v=v)
756  self.addstr(iline,0,line)
757  iline+=1
758  line=' Pmin={v.pmin:.1f}mbar, outermost closed isobar '\
759  'P={v.poci:.1f} at {v.roci:.1f}km radius'.format(v=v)
760  self.addstr(iline,0,line)
761  iline+=1
762 
763  # Warnings:
764  have=False
765  for (field,warnings) in self.warnings[istorm].iteritems():
766  iwarn=0
767  for (short,long) in warnings:
768  iwarn+=1
769  have=True
770  self.addstr(iline,0,'%s(%d) - %s: %s'%(
771  field,iwarn,short,long),
772  self.C_WARN|curses.A_BOLD)
773  iline+=1
774  if not have:
775  self.addstr(iline,0,
776  'I see no obvious errors in the vitals data for '
777  '{v.stormname:s} {v.stormid3:3s}. '.format(v=v))
778  iline+=1
779  return iline
780 
782  """!If the message queue is not empty, clears the screen and
783  shows the contents of the message queue. Waits for any key
784  press, then clears the message queue and returns."""
785  if not self.messagequeue: return
786  self.stdscr.clear()
787  self.addstr(0,0,"ERROR:")
788  for imessage in xrange(len(self.messagequeue)):
789  self.addstr(imessage+1,3,self.messagequeue[imessage])
790  self.addstr(imessage+2,5,"... PRESS ANY KEY ...")
791  self.stdscr.refresh()
792  try:
793  self.stdscr.getch()
794  except curses.error:
795  pass
796  self.messagequeue=list()
797 
798  def resort(self):
799  """!Re-sorts the vitals into priority order."""
800  YMDH=self.YMDH
801 
803  rv.extend(self.vitals)
804  self.logger.info('Sorting vitals.')
805  #self.logger.info('Cleaning up the vitals and removing duplicates.')
806  #rv.discard_except(lambda v: v.YMDH==YMDH)
807  rv.sort_by_function(sort_vitals)
808  #rv.clean_up_vitals()
809  #rv.sort_by_function(sort_vitals)
810  #fake_prio(rv)
811  #rv.sort_by_function(sort_by_prio)
812  self.vitals=[x for x in rv.vitals]
813  del rv
814  if self.fake_sources:
816 
818  """!Sets the "priority" attribute in all vitals to storm1...stormN"""
819  ijtwc=0
820  inhc=0
821  for v in self.vitals:
822  if v.center=='JTWC':
823  ijtwc+=1
824  v.source='storm%d'%ijtwc
825  setattr(v,'priority',ijtwc)
826  elif v.center=='NHC':
827  inhc+=1
828  v.source='storm%d'%inhc
829  setattr(v,'priority',inhc)
830 
831  def get_curses_mouse(self):
832  """!Gets a curses mouse event's details.
833 
834  !returns a tuple containing a string, and an index within
835  self.vitals. The string is "HWRF" if an hwrf message was
836  clicked, "GFDL" for a GFDL message or "select" if the user
837  clicked outside the message selection region. If no vitals
838  were clicked, this routine returns (None,None)."""
839  (_,x,y,_,_) = curses.getmouse()
840  #if not bstate&curses.BUTTON1_CLICKED:
841  # return (None,None) # don't care about this event
842  ivital=y-2
843  if ivital<0 or ivital>=len(self.vitals):
844  return (None,None) # not in vital list
845  if x>=56 and x<=63:
846  return ('GFDL',ivital)
847  elif x>=65 and x<=72:
848  return ('HWRF',ivital)
849  else:
850  return ('select',ivital)
851 
852  def event_loop(self,iselect=0):
853  """!The main event loop for the storm selection and setup
854  confirmation screens. Handles mouse and key events.
855 
856  @param iselect first storm selected (highlighted)
857  @returns True if the user asked to setup the models, and
858  confirmed the setup request. Returns False or None otherwise."""
859  iselect=int(iselect)
860  istorms=len(self.vitals)
861  self.show_storm_screen(iselect)
862  while True:
863  try:
864  k=self.stdscr.getch()
865  except curses.error:
866  self.show_storm_screen(iselect)
867  time.sleep(0.1)
868  inew=iselect
869  if k==curses.KEY_MOUSE:
870  (action,ivital)=self.get_curses_mouse()
871  if action=='select':
872  inew=ivital
873  if action=='GFDL':
874  inew=ivital
875  iselect=inew
876  self.toggle_run(inew,hwrf=False,gfdl=True)
877  self.show_message_queue()
878  self.show_storm_screen(iselect)
879  elif action=='HWRF':
880  inew=ivital
881  iselect=inew
882  self.toggle_run(inew,hwrf=True,gfdl=False)
883  self.show_message_queue()
884  self.show_storm_screen(iselect)
885  elif k in ( ord('p'), ord('P'), ord('u'), ord('U'),
886  curses.KEY_UP, curses.KEY_LEFT ):
887  if istorms>1:
888  inew=(iselect+istorms-1)%istorms
889  elif k in ( ord('n'), ord('N'), ord('d'), ord('D'),
890  curses.KEY_DOWN, curses.KEY_RIGHT ):
891  if istorms>1:
892  inew=(iselect+istorms+1)%istorms
893  elif k in ( ord('h'), ord('H') ):
894  self.toggle_run(iselect,hwrf=True,gfdl=False)
895  self.show_message_queue()
896  self.show_storm_screen(iselect)
897  elif k in (ord('g'), ord('G')):
898  self.toggle_run(iselect,hwrf=False,gfdl=True)
899  self.show_message_queue()
900  self.show_storm_screen(iselect)
901  elif k in (ord('b'), ord('B')):
902  self.toggle_run(iselect,hwrf=True,gfdl=True)
903  self.show_message_queue()
904  self.show_storm_screen(iselect)
905  elif k in ( ord('q'), ord('Q') ):
906  if self.quit_confirmation():
907  return False
908  else:
909  self.show_storm_screen(iselect)
910  elif k in ( ord('S'), ord('s') ):
911  if self.setup_confirmation():
912  return True
913  else:
914  # setup was canceled
915  self.show_storm_screen(iselect)
916  if inew!=iselect:
917  iselect=inew
918  self.show_storm_screen(iselect)
919 
920  def setup(self,conf):
921  """!Either setup the models, or print what would be done.
922 
923  @param conf Uses the specified configuration object (ideally,
924  an HWRFConfig) to find output locations."""
925  logger=self.logger
926  self.make_storm_indices()
927  sh='setup_hurricane'
928  deliver=conf.getbool(sh,'deliver')
929  if deliver:
930  logger.warning('deliver=yes: will write message files')
931  else:
932  logger.warning('deliver=no: will NOT write message files')
933  self._setup_one(conf,sh,'gfdl',deliver)
934  self._setup_one(conf,sh,'hwrf',deliver)
935  if not deliver:
936  logger.warning("I DID NOT REALLY WRITE ANYTHING!!")
937  logger.warning('You have deliver=no in your setup_hurricane '
938  'configuration file.')
939  logger.warning('Change that to deliver=yes to enable delivery.')
940 
941  def _setup_one(self,conf,sh,gh,deliver):
942  """!Internal function that sets up one storm.
943 
944  This is an internal implementation function. Do not call it
945  directly. It sets up HWRF or GFDL or just prints what would
946  be done (if deliver=False).
947  @param deliver False to just print what would be done, or
948  True to actually deliver
949  @param gh "gfdl" or "hwrf" (lower-case)
950  @param sh the name of the conf section to use ("setup_hurricane").
951  @param conf an hwrf.config.HWRFConfig for configuration info"""
952  logger=self.logger
953  outdir=conf.getstr(sh,gh+'_output')
954  produtil.fileop.makedirs(outdir,logger=logger)
955  maxstorm=conf.getstr(sh,'max'+gh)
956  n=0
957  nhc=list()
958  jtwc=list()
959  allstorms=list()
960  if deliver:
961  would=''
962  else:
963  would='would '
964  for v in self.vitals:
965  filename=getattr(v,gh+'message')
966  if '-' in filename: continue
967  n+=1
968  history='history'+filename[7:]
969  filename=os.path.join(outdir,filename)
970  history=os.path.join(outdir,history)
971  message=v.as_message()
972  logger.info('%s: %swrite "%s"'%(filename,would,message))
973  logger.info('%s: %sappend "%s"'%(history,would,message))
974  if deliver:
975  with open(filename,'wt') as f:
976  f.write(message+'\n')
977  with open(history,'at') as f:
978  f.write(message+'\n')
979  allstorms.append(message)
980  if v.center=='NHC':
981  nhc.append(message)
982  else:
983  jtwc.append(message)
984  allfile=os.path.join(outdir,'storms.all')
985  if os.path.exists(allfile):
986  logger.info('%swrite prior cycle contents of storms.all '
987  'to storms.prev.'%(would,))
989  rv.readfiles([allfile],raise_all=False)
990  outstring=''
991  for v in rv:
992  message=v.as_message()
993  if v.YMDH==self.YMDHm6:
994  logger.info('%sinclude vit: %s'%(would,message))
995  outstring+=message+'\n'
996  else:
997  logger.info('wrong cycle: %s'%(message,))
998  if deliver:
999  with open(os.path.join(outdir,'storms.prev'),'wt') as f:
1000  f.write(outstring)
1001  else:
1002  logger.warning('%s: does not exist - cannot generate '
1003  'storms.prev'%(allfile,))
1004  logger.info('%swrite %d lines to storms.nhc'%(would,len(nhc)))
1005  logger.info('%swrite %d lines to storms.jtwc'%(would,len(jtwc)))
1006  logger.info('%swrite %d lines to storms.all'%(would,len(allstorms)))
1007  if deliver:
1008  with open(os.path.join(outdir,'storms.nhc'),'wt') as f:
1009  f.write('\n'.join(nhc)+'\n')
1010  with open(os.path.join(outdir,'storms.jtwc'),'wt') as f:
1011  f.write('\n'.join(jtwc)+'\n')
1012  with open(os.path.join(outdir,'storms.all'),'wt') as f:
1013  f.write('\n'.join(allstorms)+'\n')
1014  nfilename=os.path.join(outdir,'nstorms')
1015  dfilename=os.path.join(outdir,'stormdate')
1016  logger.info('%s: %swrite "%d"'%(nfilename,would,n))
1017  logger.info('%s: %swrite "%s"'%(dfilename,would,self.YMDH[2:]))
1018  if deliver:
1019  with open(nfilename,'wt') as f:
1020  f.write('%d\n'%n)
1021  with open(dfilename,'wt') as f:
1022  f.write(self.YMDH[2:]+'\n')
1023 
1024 ##@var conf_defaults
1025 # Default configuration information read before parsing config files.
1026 conf_defaults=\
1027 '''[setup_hurricane]
1028 deliver=no
1029 source=tcvitals
1030 envir=test
1031 gfdl_output=/com/hur/{envir}/inpdata
1032 hwrf_output=/com/hur/{envir}/inphwrf
1033 maxgfdl=5
1034 maxhwrf=8
1035 nhc_max_storms=8
1036 jtwc_max_storms=9
1037 nhc_input=/nhc/save/guidance/storm-data/ncep/storm{istorm}
1038 jtwc_input=/dcomdev/us07003/{aYMD}/wtxtbul/storm_data/storm{istorm}
1039 tcvitals=/com/arch/prod/syndat/syndat_tcvitals.{year}
1040 '''
1041 
1042 ########################################################################
1043 def read_tcvitals(logger,files,cyc):
1044  """!Reads tcvitals.
1045 
1046  @param files If "files" is None or empty, will go to production
1047  locations to read vitals for the specified cycle.
1048  @param logger a logging.Logger for messages
1049  @param cyc the cycle to read"""
1050  rv=hwrf.revital.Revital(logger)
1051  YMDH=cyc.strftime('%Y%m%d%H')
1052  if files:
1053  for f in files:
1054  rv.readfiles(f,raise_all=False)
1055  elif cyc:
1056  tcv=cyc.strftime('/com/arch/prod/syndat/syndat_tcvitals.%Y')
1057  logger.warning('Will read: %s'%(tcv,))
1058  rv.readfiles(tcv,raise_all=False)
1059  else:
1060  raise ValueError('In read_tcvitals, you must provide a cycle or files.')
1061  logger.info('Finished reading vitals. Clean them up...')
1062  rv.clean_up_vitals()
1063  rv.discard_except(lambda v: v.YMDH==YMDH)
1064  return rv
1065 
1066 def make_bad_message(center,priority,ymdh,stormname,explanation):
1067  """!Called when a storm's message cannot be
1068 
1069  read. Creates a dummy StormInfo object with the correct time,
1070  priority and center, but invalid data. The function expects a
1071  fake "stormname" that explains what went wrong concisely (eg.:
1072  "UNPARSABLE" or "MISSING"). The basin will be "E" (since both
1073  JTWC and NHC can send vitals of that basin) and the storm number
1074  will be the priority. A full explanation of the problem should be
1075  in the "explanation" variable, which can be a multi-line string.
1076  @param center the RSMC: JTWC or NHC
1077  @param priority the storm priority
1078  @param ymdh the date
1079  @param stormname the storm name for the fake vitals
1080  @param explanation a full explanation of the problem"""
1081 
1082  format='{center:4s} {priority:02d}E {stormname:10s} {ymd:08d} {hh:02d}00 100N 0100W 010 010 1000 1010 0100 10 034 -999 -999 -999 -999 X'
1083  if len(center)>4: center=center[0:4]
1084  if len(stormname)>10: stormname=stormname[0:10]
1085  line=format.format(
1086  center=center,ymd=int(str(int(ymdh))[0:8],10),
1087  hh=int(str(int(ymdh))[8:10]),
1088  stormname=stormname,priority=int(priority))
1089  si=hwrf.storminfo.StormInfo('tcvitals',line)
1090  assert(isinstance(si,hwrf.storminfo.StormInfo))
1091  setattr(si,'priority',int(priority))
1092  setattr(si,'invalid',True)
1093  setattr(si,'explanation',str(explanation))
1094  return si
1095 
1096 def read_message(logger,center,ymdh,filename,priority):
1097  """!Attempts to read a message from the specified file, logging any
1098  problems to the given logger.
1099 
1100  @param logger a logging.Logger for log messages
1101  @param center the RSMC: JTWC or NHC
1102  @param ymdh the cycle
1103  @param filename the file to read
1104  @param priority the storm priority for the RSMC
1105  @returns None if the message file did not exist or was empty.
1106  Otherwise, a StormInfo is returned, but may contain invalid data
1107  from make_bad_message. Invalid StormInfo objects can be detected
1108  by si.invalid==True."""
1109  if not os.path.exists(filename):
1110  logger.info('%s: does not exist'%(filename,))
1111  return None
1112  try:
1113  with open(filename,'rt') as f:
1114  line=f.readline()
1115  si=hwrf.storminfo.StormInfo('message',line)
1116  setattr(si,'invalid',False)
1117  try:
1118  logger.info('%s: read this: %s'%(filename,si.as_message()))
1119  except Exception as e:
1120  logger.info('%s: could not print contents: %s'
1121  %(filename,str(e)),exc_info=True)
1122  except InvalidVitals as iv:
1123  logger.error('%s: skipping unparsable file: %s'
1124  %(filename,str(iv)),exc_info=True)
1125  si=make_bad_message(center,priority,ymdh,'UNPARSABLE',str(iv))
1126  except (KeyError,ValueError,TypeError) as e:
1127  logger.error('%s: skipping: %s'%(filename,str(e)),exc_info=True)
1128  si=make_bad_message(center,priority,ymdh,'ERROR',str(e))
1129  setattr(si,'priority',priority)
1130  return si
1131 
1132 def read_nstorms(logger,filename,rsmc,max_nstorms):
1133  """!Reads the number of storms file
1134  @param logger a logging.Logger for log messages
1135  @param filename the files path
1136  @param rsmc the RSMC: JTWC or NHC
1137  @param max_nstorms maximum allowed value for nstorms
1138  @returns an integer number of storms"""
1139  logger.info('%s: get %s nstorms'%(filename,rsmc))
1140  nstorms=int(max_nstorms)
1141  try:
1142  with open(filename,'rt') as f:
1143  line=f.readline().strip()
1144  iline=int(line,10)
1145  nstorms=max(0,min(max_nstorms,iline))
1146  logger.info('%s: %s nstorms = %d'%(filename,rsmc,nstorms))
1147  except (ValueError,TypeError,EnvironmentError) as e:
1148  logger.error("Trouble reading %s nstorms: %s"%(rsmc,str(e)))
1149  return nstorms
1150 
1151 def read_nhc_and_jtwc_inputs(logger,conf):
1152  """!Reads NHC and JTWC storm files from locations specified in the
1153  given HWRFConfig object.
1154  @param logger a logging.Logger for messages
1155  @param conf an hwrf.config.HWRFConfig with configuration info"""
1156  sh='setup_hurricane'
1157  cyc=conf.cycle
1158  YMDH=conf.getstr('config','YMDH')
1159  cycm6=hwrf.numerics.to_datetime_rel(-6*3600,cyc)
1160  YMDHm6=cycm6.strftime('%Y%m%d%H')
1161  maxjtwc=conf.getint(sh,'jtwc_max_storms')
1162  maxnhc=conf.getint(sh,'nhc_max_storms')
1163  threshold=conf.getint(sh,'renumber_threshold',14)
1164  assert(maxjtwc>=0)
1165  assert(maxnhc>=0)
1166  logger.info('Current cycle is %s and previous is %s'%(YMDH,YMDHm6))
1167 
1168  jtwc_nstorms=maxjtwc
1169  #jtwc_nstorms=read_nstorms(logger,conf.getstr(sh,'jtwc_nstorms'),
1170  # 'jtwc',maxjtwc)
1171  nhc_nstorms=maxnhc
1172  #nhc_nstorms=read_nstorms(logger,conf.getstr(sh,'nhc_nstorms'),
1173  # 'nhc',maxnhc)
1174 
1176  for inhc in xrange(nhc_nstorms):
1177  filename=conf.strinterp(sh,'{nhc_input}',istorm=inhc+1)
1178  si=read_message(logger,'NHC',YMDH,filename,inhc+1)
1179  if si is None:
1180  logger.warning('%s: could not read message'%(filename,))
1181  continue
1182  if si.YMDH!=YMDH:
1183  logger.warning('Ignoring old storm: %s'%(si.as_message(),))
1184  continue
1185  setattr(si,'source','nhcstorm%d'%(1+inhc))
1186  setattr(si,'sourcefile',filename)
1187  rv.append(si)
1188 
1189  for ijtwc in xrange(jtwc_nstorms):
1190  filename=conf.strinterp(sh,'{jtwc_input}',istorm=ijtwc+1)
1191  si=read_message(logger,'JTWC',YMDH,filename,ijtwc+1)
1192  if si is not None:
1193  setattr(si,'source','jtwcstorm%d'%(1+ijtwc))
1194  setattr(si,'sourcefile',filename)
1195  rv.append(si)
1196  rv2=hwrf.revital.Revital()
1197  vitfile=conf.strinterp(sh,'{tcvitals}')
1198  rv2.readfiles([vitfile],raise_all=False)
1199  logger.info('Have %d tcvitals vitals. Clean them up...'
1200  %(len(rv2.vitals),))
1201  #rv2.clean_up_vitals(basin_center_checker=basin_center_checker,
1202  # name_number_checker=name_number_checker)
1203  logger.info('Have %d tcvitals vitals after cleaning.'
1204  %(len(rv2.vitals),))
1205  tcprio=10
1206  for vit in reversed(rv2.vitals):
1207  if vit.center!='JTWC': continue
1208  if vit.YMDH!=YMDH: continue
1209  have=False
1210  for myvit in rv:
1211  if myvit.stormid3==vit.stormid3 and \
1212  conf.cycle==myvit.when:
1213  logger.info('Not using tcvitals "%s" because of "%s"'%(
1214  vit.as_message(),myvit.as_message()))
1215  have=True
1216  break
1217  if not have:
1218  vit2=vit.copy()
1219  setattr(vit2,'priority',tcprio)
1220  setattr(vit2,'source','tcvitals')
1221  rv.append(vit2)
1222  logger.warning('Adding vital from tcvitals: "%s"'%(
1223  vit2.as_message()))
1224 
1225  #rv2.renumber(threshold=14)
1226  for vit in reversed(rv2.vitals):
1227  if vit.center!='JTWC': continue
1228  if vit.YMDH!=YMDHm6: continue
1229  if vit.stnum>=50: continue
1230  have=False
1231  for myvit in rv:
1232  if myvit.stormid3==vit.stormid3 and \
1233  conf.cycle==myvit.when:
1234  logger.info('Not extrapolating "%s" because of "%s"'%(
1235  vit.as_message(),myvit.as_message()))
1236  have=True
1237  break
1238  if not have:
1239  assert(vit.when==cycm6)
1240  vit2=vit+6 # extrapolate six hours
1241  assert(vit2.when==cyc)
1242  setattr(vit2,'priority',tcprio)
1243  setattr(vit2,'source','extrapolated tcvitals')
1244  rv.append(vit2)
1245  logger.warning('Extrapolating "%s" +6hrs => "%s"'%(
1246  vit.as_message(),vit2.as_message()))
1247 
1248  return rv
1249 
1250 def addlog(loghere,logger):
1251  try:
1252  thedir=os.path.dirname(loghere)
1253  produtil.fileop.makedirs(thedir,logger=logger)
1254  logstream=open(loghere,'at')
1255  loghandler=logging.StreamHandler(logstream)
1256  loghandler.setLevel(logging.DEBUG)
1257  logformat=logging.Formatter(
1258  "%(asctime)s.%(msecs)03d %(name)s (%(filename)s:%(lineno)d) "
1259  "%(levelname)s: %(message)s", "%m/%d %H:%M:%S")
1260  loghandler.setFormatter(logformat)
1261  logging.getLogger().addHandler(loghandler)
1262  except Exception as e:
1263  logger.error('%s: cannot set up logging: %s'%(
1264  loghere,str(e)),exc_info=True)
1265 
1266 def main():
1267  """!Main program for setup_hurricane"""
1268 
1269  # ------------------------------------------------------------------
1270  # Setup the produtil package and get a logger object:
1271  produtil.setup.setup(jobname='setup_hurricane')
1272  logger=logging.getLogger('setup_hurricane')
1273 
1274  # ------------------------------------------------------------------
1275  # Add logging to another file if requested
1276  loghere=os.environ.get('SETUP_HURRICANE_LOG','')
1277  if loghere:
1278  addlog(loghere,logger)
1279 
1280  # ------------------------------------------------------------------
1281  # Figure out where we are and who we are. Make sure SDM does not
1282  # run a test version.
1283  hwrf_make_jobs_py=os.path.realpath(__file__)
1284  HOMEhwrf=os.path.dirname(os.path.dirname(hwrf_make_jobs_py))
1285  PARMhwrf=os.path.join(HOMEhwrf,'parm')
1286  user=os.environ['USER']
1287 
1288  # Make sure the SDM does not run a test version of this script:
1289  #if user=='SDM':
1290  # # Make sure we're using the operational version.
1291  # if re.match('/+nwprod',HOMEhwrf):
1292  # logger.info('You are SDM, running from an /nwprod copy of HWRF.')
1293  # logger.info('I will deliver messages to operational locations.')
1294  # logger.info('Rerun as another user for a test run instead.')
1295  # else:
1296  # logger.error('Safeguard activated: SDM must use setup_hurricane from /nwprod or /nwprod2 to generate tracks.')
1297  # logger.error('Rerun as another user (not SDM) to run a test.')
1298  # sys.exit(1)
1299 
1300  # ------------------------------------------------------------------
1301  # Read the configuration file
1302  if 'SETUP_HURRICANE_CONF' in os.environ and os.environ['SETUP_HURRICANE_CONF']:
1303  conffile=os.environ['SETUP_HURRICANE_CONF']
1304  else:
1305  confu=os.path.join(PARMhwrf,'setup_hurricane_'+user+'.conf')
1306  confa=os.path.join(PARMhwrf,'setup_hurricane.conf')
1307  if os.path.exists(confu):
1308  conffile=confu
1309  elif os.path.exists(confa):
1310  conffile=confa
1311  else:
1312  logger.error('%s: does not exist'%(confa,))
1313  logger.error('%s: does not exist'%(confu,))
1314  logger.error('Please make one of them and rerun.')
1315  sys.exit(1)
1316  conf=hwrf.config.HWRFConfig()
1317  conf.readstr(conf_defaults)
1318  assert(conf.has_section('setup_hurricane'))
1319  # Default values:
1320  conf.set_options('setup_hurricane',
1321  deliver='no',envir='prod',maxgfdl='5',maxhwrf='8',
1322  gfdl_output='/com2/hur/{envir}/inpdata',
1323  hwrf_output='/com2/hwrf/{envir}/inphwrf',
1324  tcvitals='/com/arch/prod/syndat/syndat_tcvitals.{year}')
1325  with open(conffile) as f:
1326  conf.readfp(f)
1327 
1328  # ------------------------------------------------------------------
1329  # Get the requested time and input files (if relevant):
1330  files=()
1331  if len(sys.argv)<2:
1332  n=datetime.datetime.now() # current time
1333  else:
1334  if len(sys.argv)>2:
1335  files=sys.argv[2:]
1336  n=hwrf.numerics.to_datetime(sys.argv[1])
1337  cyc=datetime.datetime( # round down to 6-hourly synoptic time
1338  year=n.year,month=n.month,day=n.day,hour=int(n.hour/6)*6)
1339  YMDH=cyc.strftime('%Y%m%d%H')
1340  conf.cycle=cyc
1341 
1342  if not loghere:
1343  envir=os.environ.get('envir','prod')
1344  addlog('/com2/hur/%s/inphwrf/setup_hurricane.log'%(
1345  envir),logger)
1346 
1347  # ------------------------------------------------------------------
1348  # Read the vitals and clean them up.
1349  source=conf.getstr('setup_hurricane','source','stormfiles')
1350  if source=='tcvitals':
1351  rv=read_tcvitals(logger,files,cyc)
1352  assert(rv)
1353  elif source=='stormfiles':
1354  rv=read_nhc_and_jtwc_inputs(logger,conf)
1355  assert(rv)
1356  else:
1357  logger.error('Unrecognized option \"%s\" to [setup_hurricane] source. Please specify tcvitals or stormfiles.'%(source,))
1358  sys.exit(1)
1359 
1360  # ------------------------------------------------------------------
1361  # Pass control to the StormCurses
1362  logger.info('Vitals prepared. Start StormCurses quasi-GUI.')
1363  sc=StormCurses(rv.vitals,YMDH,logger,
1364  fake_sources=(source=='tcvitals'))
1365  with sc:
1366  setup=sc.event_loop()
1367  if setup:
1368  logger.info('User pressed [S] and typed Y E S - setting up models.')
1369  sc.setup(conf)
1370  else:
1371  logger.info('User pressed [Q] - will NOT setup hurricane models.')
1372 
1373 if __name__ == '__main__':
1374  main()
Contains setup(), which initializes the produtil package.
Definition: setup.py:1
def __exit__(self, type, value, tb)
Ends the curses library and restores standard terminal functions.
fake_sources
True=tcvitals in use, False=storm files.
YMDH
the cycle of interst
def hasmessage(self, i, field)
Returns True if there are warnings or errors for storm i, and False otherwise.
def to_datetime_rel(d, rel)
Converts objects to a datetime relative to another datetime.
Definition: numerics.py:319
def show_message_queue(self)
If the message queue is not empty, clears the screen and shows the contents of the message queue...
stdscr
the curses screen used for display of text
def test_screen(self)
This routine is for testing only.
def show_storm_screen
Prints the storm selection screen starting at line 0, and returns the number of lines printed...
def adderr
Records that vitals at index i cannot be used by either model due to an error in the specified field...
C_OCEAN
Unused: font for ocean locations on the map.
Defines the Revital class which manipulates tcvitals files.
Definition: revital.py:1
def event_loop
The main event loop for the storm selection and setup confirmation screens.
maxhwrf
Maximum number of HWRF storms allowed.
def __init__
Creates a StormCurses object that will assist the SDM in choosing between the storms in the listed vi...
warnings
A mapping from storm to list of warning messages for that storm.
Defines StormInfo and related functions for interacting with vitals ATCF data.
Definition: storminfo.py:1
hwrfwill
Array of logical telling whether HWRF will be run by each storm.
def resort(self)
Re-sorts the vitals into priority order.
def setup(ignore_hup=False, dbnalert_logger=None, jobname=None, cluster=None, send_dbn=None, thread_logger=False, thread_stack=2 **24, kwargs)
Initializes the produtil package.
Definition: setup.py:15
C_LAND
Unused: font for land locations on the map.
def setup_confirmation(self)
Clears the screen and shows a setup confirmation screen, displaying what models will run what storms...
def to_datetime(d)
Converts the argument to a datetime.
Definition: numerics.py:346
a class that contains configuration information
Definition: config.py:396
def quit_confirmation(self)
Clears the screen and informs the user that they asked to quit.
def makedirs
Make a directory tree, working around filesystem bugs.
Definition: fileop.py:224
Time manipulation and other numerical routines.
Definition: numerics.py:1
C_WARN_SELECT
Font for text of storms that are selected AND have warning messages.
maxgfdl
Maximum number of GFDL storms allowed.
def __enter__(self)
Sets up the curses library.
logger
a logging.Logger for log messages
def show_storm_table
Fills self.stdscr with a list of storms starting at line iline on the screen.
def fill_source_and_priority(self)
Sets the "priority" attribute in all vitals to storm1...stormN.
def _setup_one(self, conf, sh, gh, deliver)
Internal function that sets up one storm.
This class implements a user interface for selecting which storms GFDL and HWRF should run...
parses UNIX conf files and makes the result readily available
Definition: config.py:1
def addwarn(self, i, field, reason, details)
def setup(self, conf)
Either setup the models, or print what would be done.
def get_curses_mouse(self)
Gets a curses mouse event's details.
gfdlwill
Array of logical telling whether GFDL will be run by each storm.
Configures logging.
Definition: log.py:1
def show_storm_table_line
Prints one line of the storm list table, for storm self.vitals[istorm], at line iline on the console...
hwrfcannot
Array of logical telling whether each storm cannot be run by HWRF.
def show_storm_details(self, iline, istorm)
Shows details about a storm self.vitals[istorm] starting on the given line number.
def init_hwrf_gfdl(self)
Decides if HWRF and GFDL can or should run each storm listed in the vitals.
C_SELECT
Font for selected text.
vitals
a list of hwrf.storminfo.StormInfo to select from
C_WARN
Font for text of storms that have warning messages.
def toggle_run
Turns on or off the GFDL and/or HWRF model for the storm at index istorm of self.vitals.
def show_storm_heading(self, iline)
Prints the storm selection table header starting at the specified line, and returns iline+2...
YMDHm6
the cycle before the cycle of interest
def make_storm_indices(self)
Sets the "hwrfmessage" and "gfdlmessage" attributes in all of self.vitals[*] to "messageN" (for an in...
def addstr
Puts a string s on the screen at the given y column and x row.
messagequeue
a list of messages to display
gfdlcannot
Array of logical telling whether each storm cannot be run by GFDL.
Storm vitals information from ATCF, B-deck, tcvitals or message files.
Definition: storminfo.py:411
This class reads one or more tcvitals files and rewrites them as requested.
Definition: revital.py:38