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
Line 
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
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
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__
38#    - __read_controlfile__
39#    - __str__
40#    - assign_args_to_control
41#    - assign_envs_to_control
42#    - check_conditions
43#    - check_install_conditions
44#    - to_list
45#
46# @Class Attributes:
47#
48#
49#*******************************************************************************
50
51# ------------------------------------------------------------------------------
52# MODULES
53# ------------------------------------------------------------------------------
54import os
55import re
56import sys
57import inspect
58
59# software specific classes and modules from flex_extract
60sys.path.append('../')
61import _config
62from mods.tools import my_error, silent_remove
63
64# ------------------------------------------------------------------------------
65# CLASS
66# ------------------------------------------------------------------------------
67class ControlFile(object):
68    '''
69    Contains the information which are stored in the CONTROL files.
70    '''
71
72    def __init__(self, filename):
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.
76
77        Parameters
78        ----------
79        filename : :obj:`string`
80            Name of CONTROL file.
81
82        Return
83        ------
84
85        '''
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
98        self.acctype = None
99        self.acctime = None
100        self.accmaxstep = None
101        self.marsclass = None
102        self.dataset = None
103        self.stream = None
104        self.number = 'OFF'
105        self.expver = '1'
106        self.gaussian = ''
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
136        self.inputdir = _config.PATH_INPUT_DIR
137        self.outputdir = None
138        self.ecmwfdatadir = _config.PATH_FLEXEXTRACT_DIR
139        self.exedir = _config.PATH_FORTRAN_SRC
140        self.flexpart_root_scripts = None
141        self.makefile = 'Makefile.gfortran'
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
148        self.request = 0
149        self.public = 0
150        self.ecapi = None
151
152        self.logicals = ['gauss', 'omega', 'omegadiff', 'eta', 'etadiff',
153                         'dpdeta', 'cwc', 'wrf', 'grib2flexpart', 'ecstorage',
154                         'ectrans', 'debug', 'request', 'public']
155
156        self.__read_controlfile__()
157
158        return
159
160    def __read_controlfile__(self):
161        '''Read CONTROL file and assign all CONTROL file variables.
162
163        Parameters
164        ----------
165
166        Return
167        ------
168
169        '''
170
171        try:
172            cfile = os.path.join(_config.PATH_CONTROLFILES, self.controlfile)
173            with open(cfile) as f:
174                fdata = f.read().split('\n')
175        except IOError:
176            print('Could not read CONTROL file "' + cfile + '"')
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)
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:
214                                my_error(self.mailfail,
215                                         'Could not find variable '
216                                         + data[1][j+1:k] + ' while reading ' +
217                                         self.controlfile)
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
229        return
230
231    def __str__(self):
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
253        '''
254        import collections
255
256        attrs = vars(self).copy()
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):
262        '''Overwrites the existing ControlFile instance attributes with
263        the command line arguments.
264
265        Parameters
266        ----------
267        args : :obj:`Namespace`
268            Contains the commandline arguments from script/program call.
269
270        Return
271        ------
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):
288        '''Assigns the ECMWF environment parameter.
289
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        ------
299
300        '''
301
302        for k, v in envs.iteritems():
303            setattr(self, str(k).lower(), str(v))
304
305        return
306
307    def check_conditions(self, queue):
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
322        '''
323        from mods.tools import my_error
324        import numpy as np
325
326        # check for having at least a starting date
327        # otherwise program is not allowed to run
328        if not self.start_date:
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')
333            sys.exit(1)
334
335        # retrieve just one day if end_date isn't set
336        if not self.end_date:
337            self.end_date = self.start_date
338
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
346        # assure consistency of levelist and level
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:
350            print('Warning: neither levelist nor level \
351                               specified in CONTROL file')
352            sys.exit(1)
353        elif not self.levelist and self.level:
354            self.levelist = '1/to/' + self.level
355        elif (self.levelist and not self.level) or \
356             (self.levelist[-1] != self.level[-1]):
357            self.level = self.levelist.split('/')[-1]
358        else:
359            pass
360
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
370        # if area was provided (only from commandline)
371        # decompose area into its 4 components
372        if self.area:
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
381        if '/' in self.step:
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')
392            else:
393                self.step = steps
394
395        # if maxstep wasn't provided
396        # search for it in the "step" parameter
397        if not self.maxstep:
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
405        # set root scripts since it is needed later on
406        if not self.flexpart_root_scripts:
407            self.flexpart_root_scripts = self.ecmwfdatadir
408
409        if not self.outputdir:
410            self.outputdir = self.inputdir
411
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]
419
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
428        if queue in _config.QUEUES_LIST and \
429           not self.gateway or not self.destination or \
430           not self.ecuid or not self.ecgid:
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!')
435            sys.exit(1)
436
437        if self.request != 0:
438            marsfile = os.path.join(self.inputdir,
439                                    _config.FILE_MARS_REQUESTS)
440            if os.path.isfile(marsfile):
441                silent_remove(marsfile)
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
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
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
489        return
490
491    def check_install_conditions(self):
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.
495
496        Parameters
497        ----------
498
499        Return
500        ------
501
502        '''
503
504        if self.install_target and \
505           self.install_target not in ['local', 'ecgate', 'cca']:
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')
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:
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')
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
531        else: # local
532            if not self.flexpart_root_scripts:
533                self.flexpart_root_scripts = _config.PATH_FLEXEXTRACT_DIR
534
535        return
536
537    def to_list(self):
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",
550            "ecmwfdatadir" and "flexpart_root_scripts".
551        '''
552
553        import collections
554
555        attrs = collections.OrderedDict(sorted(vars(self).copy().items()))
556
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:
569                if isinstance(item[1], list):
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)
579
Note: See TracBrowser for help on using the repository browser.
hosted by ZAMG