42 import logging, sys, os, curses, collections, time, datetime, re, StringIO
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"""
56 if center
not in (
'JTWC',
"NHC"):
continue
57 if vital.basin1
not in 'ABWSPECQXL':
continue
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
65 @param vl a list of hwrf.storminfo.StormInfo objects"""
67 if vital.stormname==
'UNKNOWN' or \
68 (vital.stnum>50
and vital.stnum<90):
70 if vital.stormname==
'TEST' and (vital.stnum<50
or vital.stnum>90):
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':
82 elif a.center==
'JTWC' and b.center==
'NHC':
85 elif hasattr(a,
'priority')
and hasattr(b,
'priority')
and \
86 a.priority!=b.priority:
88 return cmp(a.priority,b.priority)
89 if hasattr(a,
'source')
and hasattr(b,
'source')
and \
91 return -cmp(a.source,b.source)
92 return -cmp(a.wmax,b.wmax)
or cmp(a.stnum,b.stnum)
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
100 @param a,b hwrf.storminfo.StormInfo objects to order"""
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
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:
121 firstprio=int(firstprio)-1
122 d=collections.defaultdict(
lambda: firstprio)
124 if not hasattr(v,
'priority'):
126 setattr(v,
'priority',prio)
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,
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"""
144 self.
YMDH=when.strftime(
'%Y%m%d%H')
246 """!Sets up the curses library. Use in a Python "with" block."""
247 self.
stdscr=curses.initscr()
249 curses.use_default_colors()
253 self.stdscr.keypad(1)
255 curses.init_pair(1,-1,-1)
258 curses.init_pair(2,curses.COLOR_RED,-1)
259 self.
C_WARN=curses.color_pair(2)
261 curses.init_pair(3,curses.COLOR_WHITE,curses.COLOR_RED)
264 curses.init_pair(4,curses.COLOR_WHITE,curses.COLOR_BLUE)
265 self.
C_OCEAN=curses.color_pair(4)|curses.A_BOLD
267 curses.init_pair(5,curses.COLOR_WHITE,curses.COLOR_GREEN)
268 self.
C_LAND=curses.color_pair(5)|curses.A_BOLD
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)
283 """!This routine is for testing only. It displays text with
284 all color combinations used by this class."""
288 self.
addstr(7,2,
'TEST HIGHLIGHTED',
290 self.
addstr(9,2,
'TEST HIGHLIGHTED WARN',
291 curses.A_STANDOUT|self.
C_WARN)
294 self.stdscr.refresh()
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
304 for i
in xrange(len(self.
vitals)):
307 setattr(self.
vitals[i],
'hwrfmessage',
'message%d'%ihwrf)
309 setattr(self.
vitals[i],
'hwrfmessage',
'-CANNOT-')
311 setattr(self.
vitals[i],
'hwrfmessage',
'---NO---')
314 setattr(self.
vitals[i],
'gfdlmessage',
'message%d'%igfdl)
316 setattr(self.
vitals[i],
'gfdlmessage',
'-CANNOT-')
318 setattr(self.
vitals[i],
'gfdlmessage',
'---NO---')
321 """!Decides if HWRF and GFDL can or should run each storm
322 listed in the vitals. Sets any warning or error flags for
327 hwrfdisable=[
False] * len(self.
hwrfwill)
330 if v.YMDH!=self.
YMDH:
331 self.
adderr(i,
'source',
'wrong cycle',
332 'This data is for the wrong cycle: %s.'%(v.YMDH,))
335 if v.basin1
not in 'LEC':
337 if getattr(v,
'invalid',
False)
is True:
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.')
347 self.
addwarn(i,
'wmax',
'missing',
348 'Strong storm was not requested (missing bulletin?)')
350 'PROBABLE COMMUNICATION ERROR BETWEEN JTWC AND NCEP.')
352 'SUGGEST CALLING JTWC, RUN THIS STORM IF IT IS REAL.')
359 self.
gfdlcannot[i]=
'this is extrapolated tcvitals data.'
361 if source==
'tcvitals':
363 'PROBABLE COMMUNICATION ERROR BETWEEN JTWC AND NCEP.')
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: '
373 self.
addwarn(i,
'wmax',
'Vmax<10m/s',
374 'Wind is very weak.')
376 self.
addwarn(i,
'wmax',
'Vmax>80m/s',
377 'Wind is very strong.')
379 self.
addwarn(i,
'pmin',
'Pmin<890',
380 'Extremely low pressure (<890 mbar)')
382 self.
addwarn(i,
'pmin',
'Pmin>1012',
383 'Extremely high pressure (>1012 mbar)')
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:
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:
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')
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
424 for i
in xrange(len(self.
vitals)):
430 self.messagequeue.append(
'%s: HWRF cannot run: %s'%(self.
vitals[i].stormid3,self.
hwrfcannot[i]))
435 self.messagequeue.append(
'Too many HWRF storms.')
440 self.messagequeue.append(
'%s: GFDL cannot run: %s'%(self.
vitals[i].stormid3,self.
gfdlcannot[i]))
445 self.messagequeue.append(
'Too many GFDL storms.')
451 """!Returns True if there are warnings or errors for storm i,
453 @param i index of the storm in self.vitals
454 @param field the field of interest"""
458 return len(warnings)>0
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)
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])
485 """!Clears the screen and informs the user that they asked to
486 quit. Waits for a keypress and then returns True."""
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()
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
502 'You have asked to setup the following simulations:')
507 for istorm
in xrange(len(self.
vitals)):
508 if not self.
hwrfwill[istorm]:
continue
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(
520 self.
addstr(1,6,
'NO STORMS!')
522 self.
addstr(1,6,
'%d storms:'%nhwrf)
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(
537 self.
addstr(i0,6,
'NO STORMS!')
539 self.
addstr(i0,6,
'%d storms:'%ngfdl)
541 self.
addstr(i,0,
'Type YES to confirm, or press any key to go back...')
543 self.
addstr(i,0,
' Y E S ')
545 self.stdscr.refresh()
548 k=self.stdscr.getch()
549 if ikey==0
and k
in (ord(
'y'),ord(
'Y')):
552 self.stdscr.refresh()
553 elif ikey==1
and k
in (ord(
'e'),ord(
'E')):
556 self.stdscr.refresh()
557 elif ikey==2
and k
in (ord(
's'),ord(
'S')):
561 self.stdscr.refresh()
563 self.
addstr(i,0,
'--- CONFIRMED - SETUP - SEQUENCE ---')
564 self.stdscr.refresh()
566 self.
addstr(i,0,
'--- CONFIRMED - SETUP - SEQUENCE ---',
570 self.
addstr(i,0,
' CANCEL - GOING BACK ')
571 self.stdscr.refresh()
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."""
585 self.
addstr(i,0,
'Controls: [N]ext [P]rev, toggle [H]WRF [G]FDL [B]oth')
587 self.
addstr(i,0,
'When done: [S]etup models or [Q]uit without doing anything')
591 if ihighlight
is not None and len(self.
vitals)>=ihighlight:
593 self.stdscr.refresh()
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 =
'-------------------------------------------------------------------------'
604 self.
addstr(iline,0,title) ; iline+=1
605 self.
addstr(iline,0,bar) ; iline+=1
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
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'),]
630 for (k,f)
in formats:
638 if k==
'i': d=fmt.format(i+1,)
641 if v.source==
'extrapolated tcvitals':
643 elif v.source==
'tcvitals':
647 elif v.YMDH!=self.
YMDH:
648 strprio=
'storm%d'%v.priority
650 strprio=
'storm%d'%v.priority
651 d=fmt.format(strprio)
656 m=self.
C_WARN|curses.A_BOLD
663 m=self.
C_WARN|curses.A_BOLD
667 d=fmt.format(abs(val),)+(
'N' if(val>=0)
else 'S' )
669 d=fmt.format(abs(val),)+(
'E' if(val>=0)
else 'W' )
682 except curses.error
as e:
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
692 @param iline first line
693 @param ihighlight index of the storm in self.vitals to highlight, or None to
694 highlight no storms"""
696 for i
in xrange(len(self.
vitals)):
698 ihighlight
is not None and i==ihighlight)
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))"""
714 self.stdscr.addstr(y,x,s)
716 self.stdscr.addstr(y,x,s,a)
717 except curses.error
as e:
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
731 priority=getattr(v,
'priority',-999)
732 if priority
is None or priority<0:
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)
743 abs(v.lat),
'N' if(v.lat>=0)
else 'S')
745 abs(v.lon),
'E' if(v.lon>=0)
else 'W')
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)
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)
758 line=
' Pmin={v.pmin:.1f}mbar, outermost closed isobar '\
759 'P={v.poci:.1f} at {v.roci:.1f}km radius'.format(v=v)
765 for (field,warnings)
in self.
warnings[istorm].iteritems():
767 for (short,long)
in warnings:
770 self.
addstr(iline,0,
'%s(%d) - %s: %s'%(
771 field,iwarn,short,long),
772 self.
C_WARN|curses.A_BOLD)
776 'I see no obvious errors in the vitals data for '
777 '{v.stormname:s} {v.stormid3:3s}. '.format(v=v))
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."""
790 self.
addstr(imessage+2,5,
"... PRESS ANY KEY ...")
791 self.stdscr.refresh()
799 """!Re-sorts the vitals into priority order."""
804 self.logger.info(
'Sorting vitals.')
807 rv.sort_by_function(sort_vitals)
812 self.
vitals=[x
for x
in rv.vitals]
818 """!Sets the "priority" attribute in all vitals to storm1...stormN"""
824 v.source=
'storm%d'%ijtwc
825 setattr(v,
'priority',ijtwc)
826 elif v.center==
'NHC':
828 v.source=
'storm%d'%inhc
829 setattr(v,
'priority',inhc)
832 """!Gets a curses mouse event's details.
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()
843 if ivital<0
or ivital>=len(self.
vitals):
846 return (
'GFDL',ivital)
847 elif x>=65
and x<=72:
848 return (
'HWRF',ivital)
850 return (
'select',ivital)
853 """!The main event loop for the storm selection and setup
854 confirmation screens. Handles mouse and key events.
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."""
864 k=self.stdscr.getch()
869 if k==curses.KEY_MOUSE:
885 elif k
in ( ord(
'p'), ord(
'P'), ord(
'u'), ord('U'),
886 curses.KEY_UP, curses.KEY_LEFT ):
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 ):
892 inew=(iselect+istorms+1)%istorms
893 elif k
in ( ord(
'h'), ord(
'H') ):
897 elif k
in (ord(
'g'), ord(
'G')):
901 elif k
in (ord(
'b'), ord(
'B')):
905 elif k
in ( ord(
'q'), ord(
'Q') ):
910 elif k
in ( ord(
'S'), ord(
's') ):
921 """!Either setup the models, or print what would be done.
923 @param conf Uses the specified configuration object (ideally,
924 an HWRFConfig) to find output locations."""
928 deliver=conf.getbool(sh,
'deliver')
930 logger.warning(
'deliver=yes: will write message files')
932 logger.warning(
'deliver=no: will NOT write message files')
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.')
941 def _setup_one(self,conf,sh,gh,deliver):
942 """!Internal function that sets up one storm.
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"""
953 outdir=conf.getstr(sh,gh+
'_output')
955 maxstorm=conf.getstr(sh,
'max'+gh)
965 filename=getattr(v,gh+
'message')
966 if '-' in filename:
continue
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))
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)
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)
992 message=v.as_message()
994 logger.info(
'%sinclude vit: %s'%(would,message))
995 outstring+=message+
'\n'
997 logger.info(
'wrong cycle: %s'%(message,))
999 with open(os.path.join(outdir,
'storms.prev'),
'wt')
as f:
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)))
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:]))
1019 with open(nfilename,
'wt')
as f:
1021 with open(dfilename,
'wt')
as f:
1022 f.write(self.
YMDH[2:]+
'\n')
1027 '''[setup_hurricane]
1031 gfdl_output=/com/hur/{envir}/inpdata
1032 hwrf_output=/com/hur/{envir}/inphwrf
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}
1043 def read_tcvitals(logger,files,cyc):
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"""
1051 YMDH=cyc.strftime(
'%Y%m%d%H')
1054 rv.readfiles(f,raise_all=
False)
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)
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)
1066 def make_bad_message(center,priority,ymdh,stormname,explanation):
1067 """!Called when a storm's message cannot be
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"""
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]
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))
1091 setattr(si,
'priority',int(priority))
1092 setattr(si,
'invalid',
True)
1093 setattr(si,
'explanation',str(explanation))
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.
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,))
1113 with open(filename,
'rt')
as f:
1116 setattr(si,
'invalid',
False)
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)
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)
1142 with open(filename,
'rt')
as f:
1143 line=f.readline().strip()
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)))
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'
1158 YMDH=conf.getstr(
'config',
'YMDH')
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)
1166 logger.info(
'Current cycle is %s and previous is %s'%(YMDH,YMDHm6))
1168 jtwc_nstorms=maxjtwc
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)
1180 logger.warning(
'%s: could not read message'%(filename,))
1183 logger.warning(
'Ignoring old storm: %s'%(si.as_message(),))
1185 setattr(si,
'source',
'nhcstorm%d'%(1+inhc))
1186 setattr(si,
'sourcefile',filename)
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)
1193 setattr(si,
'source',
'jtwcstorm%d'%(1+ijtwc))
1194 setattr(si,
'sourcefile',filename)
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),))
1203 logger.info(
'Have %d tcvitals vitals after cleaning.'
1204 %(len(rv2.vitals),))
1206 for vit
in reversed(rv2.vitals):
1207 if vit.center!=
'JTWC':
continue
1208 if vit.YMDH!=YMDH:
continue
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()))
1219 setattr(vit2,
'priority',tcprio)
1220 setattr(vit2,
'source',
'tcvitals')
1222 logger.warning(
'Adding vital from tcvitals: "%s"'%(
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
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()))
1239 assert(vit.when==cycm6)
1241 assert(vit2.when==cyc)
1242 setattr(vit2,
'priority',tcprio)
1243 setattr(vit2,
'source',
'extrapolated tcvitals')
1245 logger.warning(
'Extrapolating "%s" +6hrs => "%s"'%(
1246 vit.as_message(),vit2.as_message()))
1250 def addlog(loghere,logger):
1252 thedir=os.path.dirname(loghere)
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)
1267 """!Main program for setup_hurricane"""
1272 logger=logging.getLogger(
'setup_hurricane')
1276 loghere=os.environ.get(
'SETUP_HURRICANE_LOG',
'')
1278 addlog(loghere,logger)
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']
1302 if 'SETUP_HURRICANE_CONF' in os.environ
and os.environ[
'SETUP_HURRICANE_CONF']:
1303 conffile=os.environ[
'SETUP_HURRICANE_CONF']
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):
1309 elif os.path.exists(confa):
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.')
1317 conf.readstr(conf_defaults)
1318 assert(conf.has_section(
'setup_hurricane'))
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:
1332 n=datetime.datetime.now()
1337 cyc=datetime.datetime(
1338 year=n.year,month=n.month,day=n.day,hour=int(n.hour/6)*6)
1339 YMDH=cyc.strftime(
'%Y%m%d%H')
1343 envir=os.environ.get(
'envir',
'prod')
1344 addlog(
'/com2/hur/%s/inphwrf/setup_hurricane.log'%(
1349 source=conf.getstr(
'setup_hurricane',
'source',
'stormfiles')
1350 if source==
'tcvitals':
1351 rv=read_tcvitals(logger,files,cyc)
1353 elif source==
'stormfiles':
1354 rv=read_nhc_and_jtwc_inputs(logger,conf)
1357 logger.error(
'Unrecognized option \"%s\" to [setup_hurricane] source. Please specify tcvitals or stormfiles.'%(source,))
1362 logger.info(
'Vitals prepared. Start StormCurses quasi-GUI.')
1364 fake_sources=(source==
'tcvitals'))
1366 setup=sc.event_loop()
1368 logger.info(
'User pressed [S] and typed Y E S - setting up models.')
1371 logger.info(
'User pressed [Q] - will NOT setup hurricane models.')
1373 if __name__ ==
'__main__':
Contains setup(), which initializes the produtil package.
def __exit__(self, type, value, tb)
Ends the curses library and restores standard terminal functions.
fake_sources
True=tcvitals in use, False=storm files.
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.
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.
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.
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.
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.
a class that contains configuration information
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.
Time manipulation and other numerical routines.
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
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.
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.
This class reads one or more tcvitals files and rewrites them as requested.