#! /usr/bin/env python3 ##@namespace run_hafs # @brief A wrapper around the Rocoto workflow system that knows how to run HAFS in Rocoto # # @anchor run_hafs_main # This is a Python program, run_hafs.py, that users run to launch and maintain an # HAFS workflow # # @code{.sh} # run_hafs.py [options] [ensids and cycles] 95E case_root [conf] # @endcode # # Arguments: # * 95E --- stormid # * case_root --- HISTORY for retrospective runs or FORECAST for real-time # # Options: # * -f --- Force a run even if the *.xml and *.db file already exist # * -w workflow.xml --- specify the Rocoto XML file # * -d workflow.db --- specify the Rocoto database file # * -s site-file.ent --- Specify the site file in the sites/ subdirectory # # Cycles: # * 2014081412 --- one cycle to run # * 2014081400-2014081618 --- a range of cycles to run # * 2014 --- run all cycles for this storm in this year # * -t --- include cycles even if they are not in the tcvitals # * -n --- disable renumbering of invests into non-invests # * -W N --- discard invests weaker than N m/s before renumbering # # Conf options: # * ../parm/hafs_more.conf --- read this configuration file # * config.run_gsi=yes --- specify the value of one configuration option ##@cond RUN_HAFS_PY import os, sys, re, logging, collections, io, getopt, itertools from os.path import realpath, normpath, dirname def ask(question): sys.stdout.write(question) itry=0 itrytoohard=100 go=True while go: itry+=1 x=sys.stdin.readline() if x.lower()=='y\n': return True elif x.lower()=='n\n': return False elif itry>=itrytoohard: sys.stderr.write('FATAL ERROR: Giving up after %d failed responses.'%itry) sys.exit(2) else: sys.stdout.write('Please answer y or n.') def usage(message=None,logger=None): """!Dumps a usage message and exits with status 2. @param message An extra message to send to stderr after the usage message @param logger Ignored.""" print(''' Usage: run_hafs.py [options] [cycles] 95E case_root [conf] Mandatory arguments: 95E -- the storm to run case_root -- FORECAST = real-time mode, HISTORY = retrospective mod Workflow options: -f -- Tells the run_hafs.py that you already ran it once for this storm, cycle list and experiment. It will use any existing *.xml or *.db file without asking for permission. Critical in crontabs. -w workflow-file.xml -- use this as the output XML file to send into rocotorun (rocotorun's -w option) -d workflow-db.db -- use this as the SQLite3 database file for Rocoto (rocotorun's -d option) Specifying a site: -s site-file -- path to a custom-made site file, rather than using one automatically chosen from sites/*.ent. Do not include any shell or XML metacharacters in the name. PATHS: This script should be run from the rocoto/ subdirectory of the HAFS installation location so it can guess the ush/ and parm/ locations (../ush and ../parm). You can override those guesses by providing the paths in the $USHhafs and $PARMhafs environment variables. SPECIFYING CYCLES: -c N -- number of hours between cycles. This ONLY affects cycle specifications after the -c option. [cycles] -- one or more cycle specifications: 2014091312-2014091712 - run this range of cycles 2014091312 - run this cycle 2014 - all cycles from 0Z January 1, 2014 to the end of that year. 2014091312-2014091712 2014091800 - run cycles from 2014091312 through 2014091712 AND run 2014091800 -t -- include cycles even if they are not in the tcvitals. This option is turned on automatically when H214 cycle lists are requested. -n -- disable renumbering of invests to non-invests. This is done automatically when an invest is requested. -W N -- discard invests weaker than N meters per second before renumbering. Default: -W 14 if a non-invest storm is requested, and -W 0 (don't discard) if an invest is requested. Configuration ([conf]): section.option=value -- override conf options on the command line /path/to/file.conf -- additional conf files to parse''', file=sys.stderr) if message is not None: print('FATAL ERROR: '+str(message).rstrip()+'\n', file=sys.stderr) sys.exit(2) ######################################################################## # Try to guess $USHhafs and $PARMhafs. The $HOMEhafs or current # working directory are used if $USHhafs and $PARMhafs are not set in # the environment. We also add the $USHhafs to the Python library # path. ##@var USHhafs # The ush/ subdirectory of the HAFS installation directory USHhafs=None ##@var HOMEhafs # The HAFS installation directory HOMEhafs=None ##@var PARMhafs # The parameter directory PARMhafs=None if os.environ.get('USHhafs',''): USHhafs=os.environ['USHhafs'] if os.environ.get('PARMhafs',''): PARMhafs=os.environ['PARMhafs'] if os.environ.get('HOMEhafs',''): HOMEhafs=os.environ['HOMEhafs'] if HOMEhafs is None and (USHhafs is None or PARMhafs is None): HOMEhafs=dirname(os.getcwd()) USHguess=os.path.join(HOMEhafs,'ush') PARMguess=os.path.join(HOMEhafs,'parm') if os.path.isdir(USHguess) and os.path.isdir(PARMguess): if USHhafs is None: USHhafs=USHguess if PARMhafs is None: PARMhafs=PARMguess if HOMEhafs is not None: if USHhafs is None: USHhafs=os.path.join(HOMEhafs,'ush') if PARMhafs is None: PARMhafs=os.path.join(HOMEhafs,'parm') if USHhafs is None: print("FATAL ERROR: Cannot guess $USHhafs. Please set $HOMEhafs or " \ "$USHhafs in environment.", file=sys.stderr) sys.exit(2) if PARMhafs is None: print("FATAL ERROR: Cannot guess $PARMhafs. Please set $HOMEhafs or " \ "$PARMhafs in environment.", file=sys.stderr) sys.exit(2) if HOMEhafs is None: print("FATAL ERROR: Cannot guess $HOMEhafs. Please set $HOMEhafs " \ "in the environment.", file=sys.stderr) sys.exit(2) sys.path.append(USHhafs) ######################################################################## # Load and set up the produtil package. import hafs.launcher, hafs.prelaunch import tcutil.revital, tcutil.numerics, tcutil.rocoto from tcutil.numerics import to_datetime, to_timedelta from tcutil.rocoto import entity_quote import produtil.setup, produtil.atparse, produtil.run, produtil.prog, \ produtil.fileop, produtil.batchsystem, produtil.cluster from produtil.fileop import remove_file, isnonempty from produtil.run import run, exe, runstr from produtil.prog import shbackslash #######import hafs.launcher #######import hafs_expt produtil.batchsystem.set_jobname('run_hafs') produtil.setup.setup(send_dbn=False) ######################################################################## # Global variables and constants logger=logging.getLogger('run_hafs') epsilon = to_timedelta(5) # five seconds six_hours = to_timedelta(6*3600) cycling_interval = six_hours cycleset = set() enset = set() mslist = list() mblist = list() benchmarkset = None parse_tcvitals = True renumber = True force = False site_file = '' outxml = '' outdb = '' dateargs = list() iarg = 0 firstarg = 0 weak_invest = None multistorm = False storms_opt = '' basins_opt = '' renumber_opt = '' def okpath(path): return produtil.fileop.norm_expand_path(path,fullnorm=True) ######################################################################## # Parse the options and arguments. short_opts = "c:d:fm:M:ns:tW:w:" long_opts = ["cycling=", "database=", "force", "multistorms=", "multibasins=", "renumber=", "site=", "tcvitals", "weak", "workflow=" ] try: opts, args = getopt.getopt(sys.argv[1:], short_opts, long_opts) except getopt.GetoptError as err: print(str(err)) usage('SCRIPT IS ABORTING DUE TO UNRECOGNIZED ARGUMENT') for k, v in opts: if k in ('-c', '--cycling'): cycling_interval = to_timedelta(int(v)*3600) elif k in ('-d', '--database'): outdb = v elif k in ('-f', '--force'): force = True elif k in ('-m', '--multistorms'): mslist.extend(v.split(",")) multistorm = True storms_opt=''.join(['-m ', ','.join(mslist)]) elif k in ('-M', '--multibasins'): mblist.extend(v.split(",")) multistorm = True basins_opt=''.join(['-M ', ','.join(mblist)]) elif k in ('-n', '--renumber'): renumber = False renumber_opt='-n' elif k in ('-s', '--site'): site_file = str(v) elif k in ('-t', '--tcvitals'): parse_tcvitals = False elif k in ('-W', '--weak'): weak_invest = int(v) elif k in ('-w', '--workflow'): outxml = v else: assert False, "UNHANDLED OPTION" # Make sure the workflow isn't the database if outxml[-3:]=='.db': usage('When using the -d option, the Rocoto XML filename must ' 'not end with ".db".') # Make sure the database isn't the workflow if outdb[-4:]=='.xml': usage('When using the -d option, the database filename must ' 'not end with ".xml".') for arg in args: if re.match('\A\d\d\Z',arg): logger.info('ensemble id') # Single ensemble ID enset.add('%02d'%int(arg,10)) elif re.match('\A\d\d-\d\d\Z',arg): logger.info('list of ensemble ids') # List of ensemble IDs en1=int(arg[0:2],10) en2=int(arg[3:],10) enset.update([ "%02d"%(x+en1) for x in range(en2-en1+1) ]) elif re.match('\A\d{10}\Z',arg): logger.info('single date/time') # Single date/time cycleset.add(arg) dateargs.append(arg) elif re.match('\A\d{4}\Z',arg): logger.info('year') # Year start=to_datetime(arg+'01010000') end=to_datetime(arg+'12312359') now=start while now[a-zA-Z][a-zA-Z0-9_]*)\.(?P