From 45fc9b4fd620d3706bf01709e828b293319d05f3 Mon Sep 17 00:00:00 2001
From: Anne Philipp <anne.philipp@univie.ac.at>
Date: Fri, 14 Dec 2018 00:44:05 +0100
Subject: [PATCH] outsourced all of the necessary checks on control variables
 into own module functions

---
 source/python/mods/checks.py | 595 ++++++++++++++++++++++++++++++++++-
 1 file changed, 578 insertions(+), 17 deletions(-)

diff --git a/source/python/mods/checks.py b/source/python/mods/checks.py
index 298a714..7e7d91f 100644
--- a/source/python/mods/checks.py
+++ b/source/python/mods/checks.py
@@ -25,11 +25,38 @@
 # MODULES
 # ------------------------------------------------------------------------------
 
+import os
 import _config
+import exceptions
+from tools import my_error, silent_remove
+from datetime import datetime
 # ------------------------------------------------------------------------------
 # FUNCTIONS
 # ------------------------------------------------------------------------------
 
+def check_logicals_type(c, logicals):
+    '''Check that the logical variables have correct type integer.
+
+    Parameters
+    ----------
+    c : :obj:`ControlFile`
+        Contains all the parameters of CONTROL file and
+        command line.
+
+    logicals : :obj:`list` of (:obj:`string` or :obj:`integer`)
+        Names of the switches that are used to control the flow of the
+        program.
+
+    Return
+    ------
+
+    '''
+
+    for var in logicals:
+        if not isinstance(getattr(c, var), int):
+            setattr(c, var, int(getattr(c, var)))
+
+    return
 
 def check_grid(grid):
     '''Convert grid into correct Lat/Lon format. E.g. '0.5/0.5'
@@ -81,7 +108,22 @@ def check_area(grid, area, upper, lower, left , right):
     Parameters
     ----------
     grid : :obj:`string`
-        Contains grid information
+        Contains grid information.
+
+    area : :obj:`string`
+        Contains area informtion.
+
+    upper : :obj:`string`
+        The northern most latitude.
+
+    lower : :obj:`string`
+        The souther most latitude.
+
+    left : :obj:`string`
+        The western most longitude.
+
+    right : :obj:`string`
+        The eastern most longiude.
 
     Return
     ------
@@ -98,19 +140,19 @@ def check_area(grid, area, upper, lower, left , right):
         upper, left, lower, right = components
 
     # determine area format
-    if (abs(float(upper) / 1000.) >= 0.05 and
-        abs(float(lower) / 1000.) >= 0.05 and
-        abs(float(left) / 1000.) >= 0.05 and
-        abs(float(right) / 1000.) >= 0.05):
+    if ((abs(float(upper) / 10000.) >= 0.01 or float(upper) / 1000. == 0. ) and
+        (abs(float(lower) / 10000.) >= 0.01 or float(lower) / 1000. == 0. ) and
+        (abs(float(left) / 10000.) >= 0.01 or float(left) / 1000. == 0. ) and
+        (abs(float(right) / 10000.) >= 0.01 or float(right) / 1000. == 0.)):
         # area is defined in 1/1000 degrees; old format
         area = '{}/{}/{}/{}'.format(float(upper) / 1000.,
                                     float(left) / 1000.,
                                     float(lower) / 1000.,
                                     float(right) / 1000.)
-    elif (abs(float(upper) / 1000.) < 0.05 and
-          abs(float(lower) / 1000.) < 0.05 and
-          abs(float(left) / 1000.) < 0.05 and
-          abs(float(right) / 1000.) < 0.05):
+    elif (abs(float(upper) / 10000.) < 0.05 and
+          abs(float(lower) / 10000.) < 0.05 and
+          abs(float(left) / 10000.) < 0.05 and
+          abs(float(right) / 10000.) < 0.05):
         # area is already in new format
         area = '{}/{}/{}/{}'.format(float(upper),
                                     float(left),
@@ -199,12 +241,12 @@ def check_ppid(c, ppid):
     return
 
 
-def check_purefc(type):
+def check_purefc(ftype):
     '''Check for a pure forecast mode.
 
     Parameters
     ----------
-    type : :obj:`list` of :obj:`string`
+    ftype : :obj:`list` of :obj:`string`
         List of field types.
 
     Return
@@ -214,23 +256,542 @@ def check_purefc(type):
         analysis fields in between.
     '''
 
-    if 'AN' not in type and '4V' not in type:
+    if 'AN' not in ftype and '4V' not in ftype:
         # pure forecast
-        return True
+        return 1
+
+    return 0
+
+
+def check_step(step, mailfail):
+    '''Checks on step format and convert into a list of steps.
+
+    If the steps were defined with "to" and "by" they are converted into
+    a list of steps. If the steps were set in a string, it is
+    converted into a list.
+
+    Parameters
+    ----------
+    step : :obj:`list` of :obj:`string` or :obj:`string`
+        Specifies the forecast time step from forecast base time.
+        Valid values are hours (HH) from forecast base time.
+
+    mailfail : :obj:`list` of :obj:``string`
+        Contains all email addresses which should be notified.
+        It might also contain just the ecmwf user name which will trigger
+        mailing to the associated email address for this user.
+
+    Return
+    ------
+    step : :obj:`list` of :obj:`string`
+        List of forecast steps in format e.g. [001, 002, ...]
+    '''
+
+    if '/' in step:
+        steps = step.split('/')
+        if 'to' in step.lower() and 'by' in step.lower():
+            ilist = np.arange(int(steps[0]),
+                              int(steps[2]) + 1,
+                              int(steps[4]))
+            step = ['{:0>3}'.format(i) for i in ilist]
+        elif 'to' in step.lower() and 'by' not in step.lower():
+            my_error(mailfail, step + ':\n' +
+                     'if "to" is used in steps parameter, '
+                     'please use "by" as well')
+        else:
+            step = steps
+
+    if not isinstance(step, list):
+        step = [step]
+
+    return step
+
+def check_type(ftype, steps):
+    '''Check if type variable is of type list and if analysis field has
+    forecast step 0.
+
+    Parameters
+    ----------
+    ftype : :obj:`list` of :obj:`string` or :obj:`string`
+        List of field types.
+
+    steps : :obj:`string`
+        Specifies the forecast time step from forecast base time.
+        Valid values are hours (HH) from forecast base time.
+
+    Return
+    ------
+    ftype : :obj:`list` of :obj:`string`
+        List of field types.
+    '''
+    if not isinstance(ftype, list):
+        ftype = [ftype]
+
+    for i, val in enumerate(ftype):
+        if ftype[i] == 'AN' and int(steps[i]) != 0:
+            print('Analysis retrievals must have STEP = 0 (now set to 0)')
+            ftype[i] = 0
+
+    return ftype
+
+def check_time(ftime):
+    '''Check if time variable is of type list. Otherwise convert to list.
+
+    Parameters
+    ----------
+    ftime : :obj:`list` of :obj:`string` or :obj:`string`
+        The time in hours of the field.
+
+    Return
+    ------
+    ftime : :obj:`list` of :obj:`string`
+        The time in hours of the field.
+    '''
+    if not isinstance(ftime, list):
+        ftime = [ftime]
+
+    return ftime
+
+def check_len_type_time_step(ftype, ftime, steps, maxstep, purefc):
+    '''Check if
+
+    Parameters
+    ----------
+    ftype : :obj:`list` of :obj:`string`
+        List of field types.
+
+    ftime : :obj:`list` of :obj:`string` or :obj:`string`
+        The time in hours of the field.
+
+    steps : :obj:`string`
+        Specifies the forecast time step from forecast base time.
+        Valid values are hours (HH) from forecast base time.
+
+    maxstep : :obj:`integer`
+        The maximum forecast time step in hours from the forecast base time.
+        This is the maximum step for non flux (accumulated) forecast data.
+
+    purefc : :obj:`integer`
+        Switch for definition of pure forecast mode or not.
+
+    Return
+    ------
+    ftype : :obj:`list` of :obj:`string`
+        List of field types.
+
+    ftime : :obj:`list` of :obj:`string`
+        The time in hours of the field.
+
+    steps : :obj:`string`
+        Specifies the forecast time step from forecast base time.
+        Valid values are hours (HH) from forecast base time.
+    '''
+    if not (len(ftype) == len(ftime) == len(steps)):
+        raise ValueError('ERROR: The number of field types, times and steps '
+                         'are not the same! Please check the setting in the '
+                         'CONTROL file!')
+
+    # if pure forecast is selected and only one field type/time is set
+    # prepare a complete list of type/time/step combination upto maxstep
+    if len(ftype) == 1 and purefc:
+        ftype = []
+        steps = []
+        ftime = []
+        for i in range(0, maxstep + 1):
+            ftype.append(ftype[0])
+            steps.append('{:0>3}'.format(i))
+            ftime.append(ftime[0])
+
+    return ftype, ftime, steps
+
+def check_mail(mail):
+    '''Check the string of mail addresses, seperate them and convert to a list.
+
+    Parameters
+    ----------
+    mail : :obj:`list` of :obj:`string` or :obj:`string`
+        Contains email addresses for notifications.
+        It might also contain just the ecmwf user name which will trigger
+        mailing to the associated email address for this user.
+
+    Return
+    ------
+    mail : :obj:`list` of :obj:``string`
+        Contains email addresses for notifications.
+        It might also contain just the ecmwf user name which will trigger
+        mailing to the associated email address for this user.
+
+    '''
+    if not isinstance(mail, list):
+        if ',' in mail:
+            mail = mail.split(',')
+        elif ' ' in mail:
+            mail = mail.split()
+        else:
+            mail = [mail]
+
+    return mail
+
+def check_queue(queue, gateway, destination, ecuid, ecgid):
+    '''Check if the necessary ECMWF parameters are set if the queue is
+    one of the QUEUES_LIST (in _config).
+
+    Parameters
+    ----------
+    queue : :obj:`string`
+        Name of the queue if submitted to the ECMWF servers.
+        Used to check if ecuid, ecgid, gateway and destination
+        are set correctly and are not empty.
+
+    gateway : :obj:`string`
+        The address of the gateway server.
+
+    destination : :obj:`string`
+        The name of the destination of the gateway server for data
+        transfer through ectrans. E.g. name@genericSftp
+
+    ecuid : :obj:`string`
+        ECMWF user id.
+
+    ecgid : :obj:`string`
+        ECMWF group id.
+
+    Return
+    ------
+
+    '''
+    if queue in _config.QUEUES_LIST and \
+        not gateway or not destination or \
+        not ecuid or not ecgid:
+        raise ValueError('\nEnvironment variables GATEWAY, DESTINATION, ECUID '
+                         'and ECGID were not set properly! \n '
+                         'Please check for existence of file "ECMWF_ENV" '
+                         'in the run directory!')
+    return
+
+def check_pathes(idir, odir, fpdir, fedir):
+    '''Check if output and flexpart pathes are set.
+
+    Parameters
+    ----------
+    idir : :obj:`string`
+        Path to the temporary directory for MARS retrieval data.
+
+    odir : :obj:`string`
+        Path to the final output directory where the FLEXPART input files
+        will be stored.
 
-    return False
+    fpdir : :obj:`string`
+        Path to FLEXPART root directory.
 
+    fedir : :obj:`string`
+        Path to flex_extract root directory.
+
+    Return
+    ------
+    odir : :obj:`string`
+        Path to the final output directory where the FLEXPART input files
+        will be stored.
+
+    fpdir : :obj:`string`
+        Path to FLEXPART root directory.
+
+    '''
+    if not fpdir:
+        fpdir = fedir
+
+    if not odir:
+        odir = idir
+
+    return odir, fpdir
+
+def check_dates(start, end):
+    '''Checks if there is at least a start date for a one day retrieval.
+
+    Checks if end date lies after start date and end date is set.
+
+    Parameters
+    ----------
+    start : :obj:`string`
+        The start date of the retrieval job.
+
+    end : :obj:`string`
+        The end date of the retrieval job.
+
+    Return
+    ------
+    start : :obj:`string`
+        The start date of the retrieval job.
+
+    end : :obj:`string`
+        The end date of the retrieval job.
 
-def check_():
     '''
+    # check for having at least a starting date
+    # otherwise program is not allowed to run
+    if not start:
+        raise ValueError('start_date was neither specified in command line nor '
+                         'in CONTROL file.\n'
+                         'Try "{} -h" to print usage information'
+                         .format(sys.argv[0].split('/')[-1]) )
+
+    # retrieve just one day if end_date isn't set
+    if not end:
+        end = start
+
+    dstart = datetime.strptime(start, '%Y%m%d')
+    dend = datetime.strptime(end, '%Y%m%d')
+    if dstart > dend:
+        raise ValueError('ERROR: Start date is after end date! \n'
+                         'Please adapt the dates in CONTROL file or '
+                         'command line! (start={}; end={})'.format(start, end))
+
+    return start, end
+
+def check_maxstep(maxstep, steps):
+    '''Convert maxstep into integer if it is already given. Otherwise, select
+    maxstep by going through the steps list.
+
+    Parameters
+    ----------
+    maxstep : :obj:`string`
+        The maximum forecast time step in hours from the forecast base time.
+        This is the maximum step for non flux (accumulated) forecast data.
+
+    steps : :obj:`string`
+        Specifies the forecast time step from forecast base time.
+        Valid values are hours (HH) from forecast base time.
+
+    Return
+    ------
+    maxstep : :obj:`integer`
+        The maximum forecast time step in hours from the forecast base time.
+        This is the maximum step for non flux (accumulated) forecast data.
+
+    '''
+    # if maxstep wasn't provided
+    # search for it in the "step" parameter
+    if not maxstep:
+        maxstep = 0
+        for s in steps:
+            if int(s) > maxstep:
+                maxstep = int(s)
+    else:
+        maxstep = int(maxstep)
+
+    return maxstep
+
+def check_basetime(basetime):
+    '''Check if basetime is set and contains one of the two
+    possible values (0, 12).
+
+    Parameters
+    ----------
+    basetime : :obj:``
+        The time for a half day retrieval. The 12 hours upfront are to be
+        retrieved.
+
+    Return
+    ------
+
+    '''
+    if basetime:
+        if int(basetime) != 0 and int(basetime) != 12:
+            raise ValueError('ERROR: Basetime has an invalid value '
+                             '-> {}'.format(str(basetime)))
+    return
+
+def check_request(request, marsfile):
+    '''Check if there is an old mars request file and remove it.
+
+    Parameters
+    ----------
+    request : :obj:`integer`
+        Selects the mode of retrieval.
+        0: Retrieves the data from ECMWF.
+        1: Prints the mars requests to an output file.
+        2: Retrieves the data and prints the mars request.
+
+    marsfile : :obj:`string`
+        Path to the mars request file.
+
+    Return
+    ------
+
+    '''
+    if request != 0:
+        if os.path.isfile(marsfile):
+            silent_remove(marsfile)
+    return
+
+def check_public(public, dataset):
+    '''Check wether the dataset parameter is set for a
+    public data set retrieval.
 
     Parameters
     ----------
-    par : :obj:``
-        ...
+    public : :obj:`ìnteger`
+        Specifies if public data are to be retrieved or not.
+
+    dataset : :obj:`string`
+        Specific name which identifies the public dataset.
 
     Return
     ------
 
     '''
+    if public and not dataset:
+        raise ValueError('ERROR: If public mars data wants to be retrieved, '
+                         'the "dataset"-parameter has to be set too!')
     return
+
+def check_acctype(acctype, ftype):
+    '''Guarantees that the accumulation field type is set.
+
+    If not set, it is derivated as in the old method (TYPE[1]).
+
+    Parameters
+    ----------
+    acctype : :obj:`string`
+        The field type for the accumulated forecast fields.
+
+    ftype : :obj:`list` of :obj:`string`
+        List of field types.
+
+    Return
+    ------
+    acctype : :obj:`string`
+        The field type for the accumulated forecast fields.
+    '''
+    if not acctype:
+        print('... Control parameter ACCTYPE was not defined.')
+        try:
+            if len(ftype) == 1 and ftype[0] != 'AN':
+                print('Use same field type as for the non-flux fields.')
+                acctype = ftype[0]
+            elif len(ftype) > 1 and ftype[1] != 'AN':
+                print('Use old setting by using TYPE[1] for flux forecast!')
+                acctype = ftype[1]
+        except:
+            raise ValueError('ERROR: Accumulation field type could not be set!')
+    else:
+        if acctype.upper() == 'AN':
+            raise ValueError('ERROR: Accumulation forecast fields can not be '
+                             'of type "analysis"!')
+    return acctype
+
+
+def check_acctime(acctime, acctype, purefc):
+    '''Guarantees that the accumulation forecast times were set.
+
+    If it is not set, it is tried to set the value fore some of the
+    most commonly used data sets. Otherwise it raises an error.
+
+    Parameters
+    ----------
+    acctime : :obj:`string`
+        The starting time from the accumulated forecasts.
+
+    acctype : :obj:`string`
+        The field type for the accumulated forecast fields.
+
+    purefc : :obj:`integer`
+        Switch for definition of pure forecast mode or not.
+
+    Return
+    ------
+    acctime : :obj:`string`
+        The starting time from the accumulated forecasts.
+    '''
+    if not acctime:
+        print('... Control parameter ACCTIME was not defined.')
+        print('... Value will be set depending on field type: '
+              '\t\t EA=06/18\n\t\t EI/OD=00/12\n\t\t  EP=18')
+        if acctype.upper() == 'EA': # Era 5
+            acctime = '06/18'
+        elif acctype.upper() == 'EI': # Era-Interim
+            acctime = '00/12'
+        elif acctype.upper() == 'EP': # CERA
+            acctime = '18'
+        elif acctype.upper() == 'OD' and not purefc: # On-demand operational
+            acctime = '00/12'
+        else:
+            raise ValueError('ERROR: Accumulation forecast time can not '
+                             'automatically be derived!')
+    return acctime
+
+def check_accmaxstep(accmaxstep, acctype, purefc, maxstep):
+    '''Guarantees that the accumulation forecast step were set.
+
+    Parameters
+    ----------
+    accmaxstep : :obj:`string`
+        The maximum forecast step for the accumulated forecast fields.
+
+    acctype : :obj:`string`
+        The field type for the accumulated forecast fields.
+
+    purefc : :obj:`integer`
+        Switch for definition of pure forecast mode or not.
+
+    maxstep : :obj:`string`
+        The maximum forecast time step in hours from the forecast base time.
+        This is the maximum step for non flux (accumulated) forecast data.
+
+    Return
+    ------
+    accmaxstep : :obj:`string`
+        The maximum forecast step for the accumulated forecast fields.
+    '''
+    if not accmaxstep:
+        print('... Control parameter ACCMAXSTEP was not defined.')
+        print('... Value will be set depending on field type/time: '
+              '\t\t EA/EI/OD=12\n\t\t  EP=24')
+        if acctype.upper() in ['EA', 'EI', 'OD'] and not purefc:
+            # Era 5, Era-Interim, On-demand operational
+            accmaxstep = '12'
+        elif acctype.upper() == 'EP': # CERA
+            accmaxstep = '18'
+        elif purefc and accmaxstep != maxstep:
+            accmaxstep = maxstep
+            print('... For pure forecast mode, the accumulated forecast must '
+                  'have the same maxstep as the normal forecast fields!\n'
+                  '\t\t Accmaxstep was set to maxstep!')
+        else:
+            raise ValueError('ERROR: Accumulation forecast step can not '
+                             'automatically be derived!')
+    else:
+        if purefc and int(accmaxstep) != int(maxstep):
+            accmaxstep = maxstep
+            print('... For pure forecast mode, the accumulated forecast must '
+                          'have the same maxstep as the normal forecast fields!\n'
+                          '\t\t Accmaxstep was set to maxstep!')
+    return accmaxstep
+
+def check_addpar(addpar):
+    '''Check that addpar has correct format of additional parameters in
+    a single string, so that it can be easily appended to the hard coded
+    parameters that are retrieved in any case.
+
+    Parameters
+    ----------
+    addpar : :obj:`string` or :obj:'list' of :obj:'string'
+        List of additional parameters to be retrieved.
+
+    Return
+    ------
+    addpar : :obj:'string'
+        List of additional parameters to be retrieved.
+    '''
+
+    if addpar and isinstance(addpar, str):
+        if '/' in addpar:
+            parlist = addpar.split('/')
+            parlist = [p for p in parlist if p is not '']
+        else:
+            parlist = [addpar]
+
+        addpar = '/' + '/'.join(parlist)
+
+    return addpar
+
-- 
GitLab