source: flex_extract.git/source/python/classes/ControlFile.py @ 96e1533

ctbtodev
Last change on this file since 96e1533 was 96e1533, checked in by Anne Philipp <anne.philipp@…>, 5 years ago

redefined test data dir and completed unittests for tools module

  • Property mode set to 100644
File size: 19.8 KB
RevLine 
[812283d]1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#*******************************************************************************
4# @Author: Leopold Haimberger (University of Vienna)
5#
6# @Date: November 2015
7#
8# @Change History:
9#
10#   February 2018 - Anne Philipp (University of Vienna):
11#        - applied PEP8 style guide
12#        - added documentation
13#        - applied some minor modifications in programming style/structure
14#        - changed name of class Control to ControlFile for more
15#          self-explanation naming
16#        - outsource of class ControlFile
[54a8a01]17#        - initialisation of class attributes ( to avoid high number of
18#          conditional statements and set default values )
19#        - divided assignment of attributes and the check of conditions
20#        - outsourced the commandline argument assignments to control attributes
[812283d]21#
22# @License:
23#    (C) Copyright 2015-2018.
24#
25#    This software is licensed under the terms of the Apache Licence Version 2.0
26#    which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
27#
28# @Class Description:
29#    The CONTROL file is the steering part of the FLEXPART extraction
30#    software. All necessary parameters needed to retrieve the data fields
31#    from the MARS archive for driving FLEXPART are set in a CONTROL file.
32#    Some specific parameters like the start and end dates can be overwritten
33#    by the command line parameters, but in generel all parameters needed
34#    for a complete set of fields for FLEXPART can be set in the CONTROL file.
35#
36# @Class Content:
37#    - __init__
[54a8a01]38#    - __read_controlfile__
[812283d]39#    - __str__
[54a8a01]40#    - assign_args_to_control
41#    - assign_envs_to_control
42#    - check_conditions
43#    - check_install_conditions
[ff99eae]44#    - to_list
45#
46# @Class Attributes:
[54a8a01]47#
[812283d]48#
49#*******************************************************************************
50
51# ------------------------------------------------------------------------------
52# MODULES
53# ------------------------------------------------------------------------------
54import os
[2fb99de]55import re
[54a8a01]56import sys
[812283d]57import inspect
[ff99eae]58
[ca867de]59# software specific classes and modules from flex_extract
60sys.path.append('../')
[2fb99de]61import _config
[70fee58]62from mods.tools import my_error, silent_remove
[2fb99de]63
[812283d]64# ------------------------------------------------------------------------------
65# CLASS
66# ------------------------------------------------------------------------------
[ff99eae]67class ControlFile(object):
[812283d]68    '''
[708c667]69    Contains the information which are stored in the CONTROL files.
[812283d]70    '''
71
72    def __init__(self, filename):
[274f9ef]73        '''Initialises the instance of ControlFile class and defines
74        all class attributes with default values. Afterwards calls
75        function __read_controlfile__ to read parameter from Control file.
[812283d]76
[274f9ef]77        Parameters
78        ----------
79        filename : :obj:`string`
80            Name of CONTROL file.
[812283d]81
[274f9ef]82        Return
83        ------
[812283d]84
85        '''
[54a8a01]86
87        # list of all possible class attributes and their default values
88        self.controlfile = filename
89        self.start_date = None
90        self.end_date = None
91        self.date_chunk = 3
92        self.dtime = None
93        self.basetime = None
94        self.maxstep = None
95        self.type = None
96        self.time = None
97        self.step = None
[0e576fc]98        self.acctype = None
99        self.acctime = None
100        self.accmaxstep = None
[54a8a01]101        self.marsclass = None
[5bad6ec]102        self.dataset = None
[54a8a01]103        self.stream = None
104        self.number = 'OFF'
[2fb99de]105        self.expver = '1'
[0e576fc]106        self.gaussian = ''
[54a8a01]107        self.grid = None
108        self.area = ''
109        self.left = None
110        self.lower = None
111        self.upper = None
112        self.right = None
113        self.level = None
114        self.levelist = None
115        self.resol = None
116        self.gauss = 0
117        self.accuracy = 24
118        self.omega = 0
119        self.omegadiff = 0
120        self.eta = 0
121        self.etadiff = 0
122        self.etapar = 77
123        self.dpdeta = 1
124        self.smooth = 0
125        self.format = 'GRIB1'
126        self.addpar = None
127        self.prefix = 'EN'
128        self.cwc = 0
129        self.wrf = 0
130        self.ecfsdir = 'ectmp:/${USER}/econdemand/'
131        self.mailfail = ['${USER}']
132        self.mailops = ['${USER}']
133        self.grib2flexpart = 0
134        self.ecstorage = 0
135        self.ectrans = 0
[25b14be]136        self.inputdir = _config.PATH_INPUT_DIR
[ca867de]137        self.outputdir = None
[2fb99de]138        self.ecmwfdatadir = _config.PATH_FLEXEXTRACT_DIR
139        self.exedir = _config.PATH_FORTRAN_SRC
[54a8a01]140        self.flexpart_root_scripts = None
[2fb99de]141        self.makefile = 'Makefile.gfortran'
[54a8a01]142        self.destination = None
143        self.gateway = None
144        self.ecuid = None
145        self.ecgid = None
146        self.install_target = None
147        self.debug = 0
[2fb99de]148        self.request = 0
[5bad6ec]149        self.public = 0
[96e1533]150        self.ecapi = None
[2fb99de]151
152        self.logicals = ['gauss', 'omega', 'omegadiff', 'eta', 'etadiff',
153                         'dpdeta', 'cwc', 'wrf', 'grib2flexpart', 'ecstorage',
[5bad6ec]154                         'ectrans', 'debug', 'request', 'public']
[54a8a01]155
156        self.__read_controlfile__()
157
158        return
159
160    def __read_controlfile__(self):
[274f9ef]161        '''Read CONTROL file and assign all CONTROL file variables.
[54a8a01]162
[274f9ef]163        Parameters
164        ----------
165
166        Return
167        ------
[54a8a01]168
169        '''
[4971f63]170
171        try:
[70fee58]172            cfile = os.path.join(_config.PATH_CONTROLFILES, self.controlfile)
173            with open(cfile) as f:
[4971f63]174                fdata = f.read().split('\n')
175        except IOError:
[70fee58]176            print('Could not read CONTROL file "' + cfile + '"')
[4971f63]177            print('Either it does not exist or its syntax is wrong.')
178            print('Try "' + sys.argv[0].split('/')[-1] + \
179                      ' -h" to print usage information')
180            sys.exit(1)
[812283d]181
182        # go through every line and store parameter
183        for ldata in fdata:
184            data = ldata.split()
185            if len(data) > 1:
186                if 'm_' in data[0].lower():
187                    data[0] = data[0][2:]
188                if data[0].lower() == 'class':
189                    data[0] = 'marsclass'
190                if data[0].lower() == 'day1':
191                    data[0] = 'start_date'
192                if data[0].lower() == 'day2':
193                    data[0] = 'end_date'
194                if data[0].lower() == 'addpar':
195                    if '/' in data[1]:
196                        # remove leading '/' sign from addpar content
197                        if data[1][0] == '/':
198                            data[1] = data[1][1:]
199                        dd = data[1].split('/')
200                        data = [data[0]]
201                        for d in dd:
202                            data.append(d)
203                if len(data) == 2:
204                    if '$' in data[1]:
205                        setattr(self, data[0].lower(), data[1])
206                        while '$' in data[1]:
207                            i = data[1].index('$')
208                            j = data[1].find('{')
209                            k = data[1].find('}')
210                            var = os.getenv(data[1][j+1:k])
211                            if var is not None:
212                                data[1] = data[1][:i] + var + data[1][k+1:]
213                            else:
[54a8a01]214                                my_error(self.mailfail,
215                                         'Could not find variable '
216                                         + data[1][j+1:k] + ' while reading ' +
217                                         self.controlfile)
[812283d]218                        setattr(self, data[0].lower() + '_expanded', data[1])
219                    else:
220                        if data[1].lower() != 'none':
221                            setattr(self, data[0].lower(), data[1])
222                        else:
223                            setattr(self, data[0].lower(), None)
224                elif len(data) > 2:
225                    setattr(self, data[0].lower(), (data[1:]))
226            else:
227                pass
228
[54a8a01]229        return
230
231    def __str__(self):
[274f9ef]232        '''Prepares a string which have all the ControlFile class attributes
233        with its associated values. Each attribute is printed in one line and
234        in alphabetical order.
235
236        Example
237        -------
238        'age': 10
239        'color': 'Spotted'
240        'kids': 0
241        'legs': 2
242        'name': 'Dog'
243        'smell': 'Alot'
244
245        Parameters
246        ----------
247
248        Return
249        ------
250        string
251            Single string of concatenated ControlFile class attributes
252            with their values
[54a8a01]253        '''
254        import collections
255
[5bad6ec]256        attrs = vars(self).copy()
[54a8a01]257        attrs = collections.OrderedDict(sorted(attrs.items()))
258
259        return '\n'.join("%s: %s" % item for item in attrs.items())
260
261    def assign_args_to_control(self, args):
[274f9ef]262        '''Overwrites the existing ControlFile instance attributes with
263        the command line arguments.
[54a8a01]264
[274f9ef]265        Parameters
266        ----------
267        args : :obj:`Namespace`
268            Contains the commandline arguments from script/program call.
[54a8a01]269
[274f9ef]270        Return
271        ------
[54a8a01]272
273        '''
274
275        # get dictionary of command line parameters and eliminate all
276        # parameters which are None (were not specified)
277        args_dict = vars(args)
278        arguments = {k : args_dict[k] for k in args_dict
279                     if args_dict[k] != None}
280
281        # assign all passed command line arguments to ControlFile instance
282        for k, v in arguments.iteritems():
283            setattr(self, str(k), v)
284
285        return
286
287    def assign_envs_to_control(self, envs):
[274f9ef]288        '''Assigns the ECMWF environment parameter.
[54a8a01]289
[274f9ef]290        Parameters
291        ----------
292        envs : :obj:`dictionary` of :obj:`strings`
293            Contains the ECMWF environment parameternames "ECUID", "ECGID",
294            "DESTINATION" and "GATEWAY" with its corresponding values.
295            They were read from the file "ECMWF_ENV".
296
297        Return
298        ------
[54a8a01]299
300        '''
301
302        for k, v in envs.iteritems():
303            setattr(self, str(k).lower(), str(v))
304
305        return
306
[2fb99de]307    def check_conditions(self, queue):
[274f9ef]308        '''Checks a couple of necessary attributes and conditions,
309        such as if they exist and contain values.
310        Otherwise set default values.
311
312        Parameters
313        ----------
314        queue : :obj:`string`
315            Name of the queue if submitted to the ECMWF servers.
316            Used to check if ecuid, ecgid, gateway and destination
317            are set correctly and are not empty.
318
319        Return
320        ------
321
[54a8a01]322        '''
[25b14be]323        from mods.tools import my_error
[54a8a01]324        import numpy as np
325
326        # check for having at least a starting date
327        # otherwise program is not allowed to run
[0e576fc]328        if not self.start_date:
[2fb99de]329            print('start_date specified neither in command line nor \
330                   in CONTROL file ' +  self.controlfile)
331            print('Try "' + sys.argv[0].split('/')[-1] +
332                  ' -h" to print usage information')
[54a8a01]333            sys.exit(1)
334
335        # retrieve just one day if end_date isn't set
[0e576fc]336        if not self.end_date:
[812283d]337            self.end_date = self.start_date
[54a8a01]338
[ca867de]339        # basetime has only two possible values
340        if self.basetime:
341            if int(self.basetime) != 0 and int(self.basetime) != 12:
342                print('Basetime has an invalid value!')
343                print('Basetime = ' + str(self.basetime))
344                sys.exit(1)
345
[54a8a01]346        # assure consistency of levelist and level
[0e576fc]347        # up-to-date available maximum level numbers at ECMWF, 05.10.2018
348        max_level_list = [16, 19, 31, 40, 50, 60, 62, 91, 137]
349        if not self.levelist and not self.level:
[2fb99de]350            print('Warning: neither levelist nor level \
351                               specified in CONTROL file')
352            sys.exit(1)
[0e576fc]353        elif not self.levelist and self.level:
[2fb99de]354            self.levelist = '1/to/' + self.level
[0e576fc]355        elif (self.levelist and not self.level) or \
[2fb99de]356             (self.levelist[-1] != self.level[-1]):
357            self.level = self.levelist.split('/')[-1]
[812283d]358        else:
[2fb99de]359            pass
[812283d]360
[0e576fc]361        # check if max level is a valid level
362        if int(self.level) not in max_level_list:
363            print('ERROR: ')
364            print('LEVEL must be the maximum level of a specified '
365                  'level list from ECMWF, e.g.')
366            print('[16, 19, 31, 40, 50, 60, 62, 91 or 137]')
367            print('Check parameter "LEVEL" or the max level of "LEVELIST"!')
368            sys.exit(1)
369
[2fb99de]370        # if area was provided (only from commandline)
371        # decompose area into its 4 components
[54a8a01]372        if self.area:
[2fb99de]373            components = self.area.split('/')
374            # convert float to integer coordinates
375            if '.' in self.area:
376                components = [str(int(float(item) * 1000))
377                              for i, item in enumerate(components)]
378            self.upper, self.left, self.lower, self.right = components
379
380        # prepare step list if "/" signs are found
[54a8a01]381        if '/' in self.step:
[2fb99de]382            steps = self.step.split('/')
383            if 'to' in self.step.lower() and 'by' in self.step.lower():
384                ilist = np.arange(int(steps[0]),
385                                  int(steps[2]) + 1,
386                                  int(steps[4]))
387                self.step = ['{:0>3}'.format(i) for i in ilist]
388            elif 'to' in self.step.lower() and 'by' not in self.step.lower():
389                my_error(self.mailfail, self.step + ':\n' +
390                         'if "to" is used in steps parameter, \
391                         please use "by" as well')
[54a8a01]392            else:
[2fb99de]393                self.step = steps
[54a8a01]394
395        # if maxstep wasn't provided
396        # search for it in the "step" parameter
[0e576fc]397        if not self.maxstep:
[812283d]398            self.maxstep = 0
399            for s in self.step:
400                if int(s) > self.maxstep:
401                    self.maxstep = int(s)
402        else:
403            self.maxstep = int(self.maxstep)
404
[54a8a01]405        # set root scripts since it is needed later on
406        if not self.flexpart_root_scripts:
407            self.flexpart_root_scripts = self.ecmwfdatadir
[812283d]408
[ca867de]409        if not self.outputdir:
410            self.outputdir = self.inputdir
411
[54a8a01]412        if not isinstance(self.mailfail, list):
413            if ',' in self.mailfail:
414                self.mailfail = self.mailfail.split(',')
415            elif ' ' in self.mailfail:
416                self.mailfail = self.mailfail.split()
417            else:
418                self.mailfail = [self.mailfail]
[812283d]419
[54a8a01]420        if not isinstance(self.mailops, list):
421            if ',' in self.mailops:
422                self.mailops = self.mailops.split(',')
423            elif ' ' in self.mailops:
424                self.mailops = self.mailops.split()
425            else:
426                self.mailops = [self.mailops]
427
[70fee58]428        if queue in _config.QUEUES_LIST and \
[2fb99de]429           not self.gateway or not self.destination or \
[54a8a01]430           not self.ecuid or not self.ecgid:
[2fb99de]431            print('\nEnvironment variables GATEWAY, DESTINATION, ECUID and \
432                   ECGID were not set properly!')
433            print('Please check for existence of file "ECMWF_ENV" in the \
434                   python directory!')
[54a8a01]435            sys.exit(1)
[812283d]436
[2fb99de]437        if self.request != 0:
[5d42acd]438            marsfile = os.path.join(self.inputdir,
[2fb99de]439                                    _config.FILE_MARS_REQUESTS)
440            if os.path.isfile(marsfile):
[70fee58]441                silent_remove(marsfile)
[2fb99de]442
443        # check all logical variables for data type
444        # if its a string change to integer
445        for var in self.logicals:
446            if not isinstance(getattr(self, var), int):
447                setattr(self, var, int(getattr(self, var)))
448
[5bad6ec]449        if self.public and not self.dataset:
450            print('ERROR: ')
451            print('If public mars data wants to be retrieved, '
452                  'the "dataset"-parameter has to be set in the control file!')
453            sys.exit(1)
454
[0e576fc]455        if not isinstance(self.type, list):
456            self.type = [self.type]
457
458        for i, val in enumerate(self.type):
459            if self.type[i] == 'AN' and int(self.step[i]) != 0:
460                print('Analysis retrievals must have STEP = 0 (is set to 0)')
461                self.type[i] = 0
462
463        if not isinstance(self.time, list):
464            self.time = [self.time]
465
466        if not isinstance(self.step, list):
467            self.step = [self.step]
468
469        if not self.acctype:
470            print('... Control paramter ACCTYPE was not defined.')
471            try:
472                if len(self.type) > 1 and self.type[1] != 'AN':
473                    print('Use old setting by using TYPE[1] for flux forecast!')
474                    self.acctype = self.type[1]
475            except:
476                print('Use default value "FC" for flux forecast!')
477                self.acctype='FC'
478
479        if not self.acctime:
480            print('... Control paramter ACCTIME was not defined.')
481            print('Use default value "00/12" for flux forecast!')
482            self.acctime='00/12'
483
484        if not self.accmaxstep:
485            print('... Control paramter ACCMAXSTEP was not defined.')
486            print('Use default value "12" for flux forecast!')
487            self.accmaxstep='12'
488
[812283d]489        return
490
[54a8a01]491    def check_install_conditions(self):
[274f9ef]492        '''Checks a couple of necessary attributes and conditions
493        for the installation such as if they exist and contain values.
494        Otherwise set default values.
[812283d]495
[274f9ef]496        Parameters
497        ----------
498
499        Return
500        ------
[812283d]501
502        '''
503
[54a8a01]504        if self.install_target and \
505           self.install_target not in ['local', 'ecgate', 'cca']:
[2fb99de]506            print('ERROR: unknown or missing installation target ')
507            print('target: ', self.install_target)
508            print('please specify correct installation target ' +
509                  '(local | ecgate | cca)')
510            print('use -h or --help for help')
[54a8a01]511            sys.exit(1)
512
513        if self.install_target and self.install_target != 'local':
514            if not self.ecgid or not self.ecuid or \
515               not self.gateway or not self.destination:
[2fb99de]516                print('Please enter your ECMWF user id and group id as well ' +
517                      'as the \nname of the local gateway and the ectrans ' +
518                      'destination ')
519                print('with command line options --ecuid --ecgid \
520                       --gateway --destination')
521                print('Try "' + sys.argv[0].split('/')[-1] + \
522                      ' -h" to print usage information')
523                print('Please consult ecaccess documentation or ECMWF user \
524                       support for further details')
[54a8a01]525                sys.exit(1)
526
527            if not self.flexpart_root_scripts:
528                self.flexpart_root_scripts = '${HOME}'
529            else:
530                self.flexpart_root_scripts = self.flexpart_root_scripts
[2fb99de]531        else: # local
[54a8a01]532            if not self.flexpart_root_scripts:
[2fb99de]533                self.flexpart_root_scripts = _config.PATH_FLEXEXTRACT_DIR
[54a8a01]534
535        return
[812283d]536
[ff99eae]537    def to_list(self):
[274f9ef]538        '''Just generates a list of strings containing the attributes and
539        assigned values except the attributes "_expanded", "exedir",
540        "ecmwfdatadir" and "flexpart_root_scripts".
541
542        Parameters
543        ----------
544
545        Return
546        ------
547        l : :obj:`list`
548            A sorted list of the all ControlFile class attributes with
549            their values except the attributes "_expanded", "exedir",
[812283d]550            "ecmwfdatadir" and "flexpart_root_scripts".
551        '''
552
[54a8a01]553        import collections
554
[5bad6ec]555        attrs = collections.OrderedDict(sorted(vars(self).copy().items()))
[54a8a01]556
[812283d]557        l = list()
558
559        for item in attrs.items():
560            if '_expanded' in item[0]:
561                pass
562            elif 'exedir' in item[0]:
563                pass
564            elif 'flexpart_root_scripts' in item[0]:
565                pass
566            elif 'ecmwfdatadir' in item[0]:
567                pass
568            else:
[ff99eae]569                if isinstance(item[1], list):
[812283d]570                    stot = ''
571                    for s in item[1]:
572                        stot += s + ' '
573
574                    l.append("%s %s" % (item[0], stot))
575                else:
576                    l.append("%s %s" % item)
577
578        return sorted(l)
[ff99eae]579
Note: See TracBrowser for help on using the repository browser.
hosted by ZAMG