#! /usr/bin/env python3 """!Creates the initial HAFS directory structure, loads information into each job. This module is used to create the initial HAFS conf file in the first HAFS job via the hafs.launcher.launch(). The hafs.launcher.load() then reloads that configuration. The launch() function does more than just create the conf file though. It parses the tcvitals, creates several initial files and directories and runs a sanity check on the whole setup. The HAFSLauncher class is used in place of an hafs.config.HAFSConfig throughout the HAFS system. It can be used as a drop-in replacement for an hafs.config.HAFSConfig, but has additional features needed to support sanity checks, initial creation of the HAFS system and tcvitals generation.""" ##@var __all__ # All symbols exported by "from hafs.launcher import *" __all__=['load','launch','HAFSLauncher','parse_launch_args','multistorm_parse_args'] import os, re, sys, collections, random import numpy as np import xarray as xr import produtil.fileop, produtil.run, produtil.log import tcutil.revital, tcutil.storminfo, tcutil.numerics import hafs.config import hafs.prelaunch from random import Random from produtil.fileop import isnonempty, deliver_file from produtil.cd import NamedDir, TempDir from produtil.run import mpi, mpirun, run, runstr, checkrun, exe, bigexe, alias from produtil.log import jlogger from tcutil.numerics import to_datetime_rel, to_datetime, to_fraction from hafs.config import HAFSConfig from hafs.exceptions import HAFSDirInsane,HAFSStormInsane,HAFSCycleInsane, \ HAFSVariableInsane,HAFSInputInsane,HAFSScriptInsane,HAFSExecutableInsane,\ HAFSFixInsane,HAFSArchiveInsane,HAFSConfigInsane def multistorm_parse_args(msids, args, logger, usage, PARMhafs=None, wrapper=False): """This is the multistorm argument parser. It is really just a wrapper around parse_launch_args(). The last Element of the returned list is the launch args for the Fake storm. From the original arguments, returns a new list of launch args for all the storms in a multistorm run. The SID and optional config.startfile from the original sys.argv[1:] list are replaced with a storm id and a config.startfile (if present) from the MULTISTORM_SIDS. The following multistorm conf options are also added to each storm. config.fakestormid=, config.multistorm_sids=,config.multistorm_priority_sid=, config.multistorm_sids=, General structure of the returned list. [[storm1, arg1, ..argN], ..[stormN, arg1, ..argN], [storm00, arg1, ..argN]] INPUT: args -- a copy of the initial command line args, excluding sys.argv[0] RETURNS: case_root,parm,infiles,stids,fake_stid,multistorm_priority_sid,moreopts[] """ # See if the optional config.startfile argument is present and get its index. # startfile_idx is a list of indexes in the args_multistorm list that have # a 'config.startfile' substring. There should only be one or none. # if there are none, then startfile_idx = [], an empty list. startfile_idx = [args.index(arg) for arg in args if 'config.startfile' in arg] if len(startfile_idx) > 1: logger.error('FATAL ERROR: Exiting, More than 1 config.startfile= parameter in the argument list.') sys.exit(2) # MULTISTORM Requirement-The fakestorm will be defined as "00L". fake_stid = '00L' assert(msids is not None) # Best guess at priority storm id if not msids: msids=list() msids=[fake_stid] if fake_stid != msids[0]: multistorm_priority_sid = msids[0] elif len(msids) > 1: multistorm_priority_sid = msids[1] else: #Else, running multistorm with no storm, only the fake storm. multistorm_priority_sid = msids[0] # THIS IS Required: multistorm_all_sids is list of ALL storm ids, which # means it includes the fakestorm. The fakestorm sid MUST be appended # at the end of multistorm_all_sids list. The call in # exhafs_launch.py:main().fakestorm_conf=hafs.launcher.launch( # ... moreopts[-1]...) relies on the fake storm being the last in this list. # Ultimately this allows for the creation of a start file of the fakestorm, # in addition to all the realstorms. # This just makes certain the fake storm is at the end of the list. # Also, OK if msids has only the fakestorm in its list. if fake_stid in msids: msids.remove(fake_stid) msids.append(fake_stid) multistorm_all_sids = list(msids) else: multistorm_all_sids = list(msids) multistorm_all_sids.append(fake_stid) args.append('config.fakestormid=' + fake_stid) args.append('config.multistorm_priority_sid=' + multistorm_priority_sid) args.append('config.multistorm_sids=' + ' '.join(msids)) logger.info('Setting up hafs to run as a multi storm with sids: %s' %(msids)) logger.info('HAFS multistorm: The priority sid is: %s'%(multistorm_priority_sid)) logger.info('HAFS multistorm: The multistorm fake storm id is: %s' %(fake_stid)) # Setup arguments for each storm, as if this script was called individually for each storm. # Update the storm id and startfile arguments for each storm. # [[storm1, arg1, ..argN], ..[stormN, arg1, ..argN], [storm00, arg1, ..argN]] multistorms = [] stids = [] moreopts = [] # Used to build the start files for a multistorm when called from the wrappers. # ie. if "00L." passed in, it is replace in the startfile name in the loop below # for each storm. sid_passedin = args[0] for i, stormid in enumerate(multistorm_all_sids): multistorms.append(args[:]) multistorms[i][0] = stormid if startfile_idx: if sid_passedin in multistorms[i][startfile_idx[0]]: multistorms[i][startfile_idx[0]]= \ args[startfile_idx[0]].replace(sid_passedin,stormid) else: multistorms[i][startfile_idx[0]]= args[startfile_idx[0]] + str(stormid) # The code block below inserts standard hafs_multistorm conf files to # the existing list of ordered hafs conf files and ensures the required # multistorm order of conf files. idx_system_conf=None for i, storm_args in enumerate(multistorms): (case_root,parm,infiles,stid,moreopt) = \ parse_launch_args(storm_args,logger,usage,PARMhafs) for idx,str in enumerate(infiles): if 'system.conf' in str: idx_system_conf=idx stids.append(stid) moreopts.append(moreopt) for confbn in [ 'hafs_multistorm.conf' ]: confy= os.path.join(parm, confbn) if not os.path.exists(confy): logger.error('FATAL ERROR: '+confy+': conf file does not exist.') sys.exit(2) elif not os.path.isfile(confy): logger.error('FATAL ERROR: '+confy+': conf file is not a regular file.') sys.exit(2) elif not produtil.fileop.isnonempty(confy): logger.warning( confy+': conf file is empty. Will continue anyway.') logger.info('Conf input: '+repr(confy)) if idx_system_conf: infiles.insert(idx_system_conf+1,confy) idx_system_conf += 1 else: infiles.append(confy) logger.info('MULTISTORM Conf input ORDER:') for conffile in infiles: logger.info('Conf input: '+repr(conffile)) return (case_root,parm,infiles,stids,fake_stid,multistorm_priority_sid,moreopts) def multistorm_priority(args, basins, logger, usage, PARMhafs=None, prelaunch=None,renumber=True): storms = list() strcycle=args[0] cyc=to_datetime(strcycle) YMDH=cyc.strftime('%Y%m%d%H') (case_root,parm,infiles,stid,moreopt) = \ parse_launch_args(args[1:],logger,usage,PARMhafs) print('INFILES: ', infiles) conf = launch(infiles,cyc,stid,moreopt,case_root, init_dirs=False,prelaunch=prelaunch, fakestorm=True) syndatdir=conf.getdir('syndat') vitpattern=conf.getstr('config','vitpattern','syndat_tcvitals.%Y') vitfile=os.path.join(syndatdir,cyc.strftime(vitpattern)) multistorm=conf.getbool('config','run_multistorm',False) #ADDED BY THIAGO TO DETERMINE IF "run_multistorm=true". rv=tcutil.revital.Revital(logger=logger) rv.readfiles(vitfile, raise_all=False) if renumber: rv.renumber() rv.delete_invest_duplicates() rv.clean_up_vitals() rv.discard_except(lambda v: v.YMDH==YMDH) rv.discard_except(lambda v: v.basin1 in basins) # ADDED BY THIAGO: HRD's new rule for East-pac storms only. # EDIT - GJA - 08/13/2017: Western threshold for EPAC storms is -135 # and Eastern threshold for LANT storms is -25 # Temp fix so relocation does not fail if multistorm: rv.discard_except(lambda v: v.basin1!='E' or (v.basin1=='E' and v.lon>=-140)) rv.clean_up_vitals() rv.sort_by_function(rv.hrd_multistorm_sorter) for v in rv: sid = v.as_tcvitals().split()[1] storms.append(sid) # if len(storms) == 0: # logger.info('No storms for cycle: '+cyc.strftime('%Y%m%d%H')) # produtil.fileop.touch(os.path.join(conf.getdir('com'), # 'no_storms.txt')) return(storms) def parse_launch_args(args,logger,usage,PARMhafs=None): """!Parsed arguments to scripts that launch the HAFS system. This is the argument parser for the exhafs_launch.py and hafs_driver.py scripts. It parses the storm ID and later arguments (in args). Earlier arguments are parsed by the scripts themselves. If something goes wrong, this function calls sys.exit(1) or sys.exit(2). The arguments depend on if PARMhafs=None or not. @code{.py} If PARMhafs is None: StormID CASE_ROOT /path/to/parm [options] Otherwise: StormID CASE_ROOT [options] @endcode * StormID --- three character storm identifier (ie.: 12L for Katrina) * CASE_ROOT -- HISTORY or FORECAST * /path/to/parm - path to the parm directory, which contains the default conf files. Options: * section.variable=value --- set this value in this section, no matter what * /path/to/file.conf --- read this conf file after the default conf files. Later conf files override earlier ones. The conf files read in are: * parm/hafs_input.conf * parm/hafs.conf * parm/hafs_holdvars.conf * parm/hafs_basic.conf * parm/system.conf @param args the script arguments, after script-specific ones are removed @param logger a logging.Logger for log messages @param usage a function called to provide a usage message @param PARMhafs the directory with *.conf files""" if len(args)<2 or ( PARMhafs is None and len(args)<3): usage(logger=logger) logger.error('FATAL ERROR: Wrong usage, exiting.') sys.exit(2) # Get the storm ID: stid=args[0].upper() if not re.match('^[0-9][0-9][ABCELPQSW]$',stid): logger.error('FATAL ERROR: %s: invalid storm id. Must be a three character ' 'storm ID such as 90L or 13W'%(stid,)) sys.exit(2) logger.info('Running Storm ID is '+repr(stid)) # Get the case root (real-time vs. retrospective): case_root=args[1].upper() if case_root=='HISTORY': real_time=False elif case_root=='FORECAST': real_time=True else: logger.error('FATAL ERROR: %s: invalid case root. Must be HISTORY for ' 'retrospective runs or FORECAST for real-time runs.' %(case_root,)) sys.exit(2) logger.info('Case root is '+repr(case_root)) # Find the parm directory if PARMhafs is None: parm=args[2] if not os.path.exists(parm): logger.error('FATAL ERROR: '+parm+': parm directory does not exist') sys.exit(2) elif not os.path.isdir(parm): logger.error('FATAL ERROR: '+parm+': parm directory is not a directory') sys.exit(2) logger.info('Scan %d optional arguments.'%(len(args)-3)) args=args[3:] else: parm=PARMhafs logger.info('Scan %d optional arguments.'%(len(args)-1)) args=args[2:] parm=os.path.realpath(parm) # Standard conf files: infiles=[ os.path.join(parm,'hafs_input.conf'), os.path.join(parm,'hafs.conf'), os.path.join(parm,'hafs_holdvars.conf'), os.path.join(parm,'hafs_basic.conf'), os.path.join(parm,'system.conf') ] # Now look for any option and conf file arguments: bad=False moreopt=collections.defaultdict(dict) for iarg in range(len(args)): logger.info(args[iarg]) m=re.match('''(?x) (?P
[a-zA-Z][a-zA-Z0-9_]*) \.(?P