From ff99eae65dfaeec1221e06572d770ebd48feb3f4 Mon Sep 17 00:00:00 2001 From: Anne Philipp <anne.philipp@univie.ac.at> Date: Fri, 1 Jun 2018 22:34:59 +0200 Subject: [PATCH] completed application of pep8 style guide and pylint investigations. added documentation almost everywhere --- ECMWF_FPparameter.xlsx | Bin 20494 -> 14531 bytes python/CONTROL_OD | 35 + python/ControlFile.py | 68 +- python/{ECFlexpart.py => EcFlexpart.py} | 646 ++++++++++-------- python/GribTools.py | 99 ++- python/{MARSretrieval.py => MarsRetrieval.py} | 85 ++- python/{UIOFiles.py => UioFiles.py} | 47 +- python/{Disagg.py => disaggregation.py} | 20 +- python/{getMARSdata.py => get_mars_data.py} | 102 ++- python/install.py | 150 ++-- python/plot_retrieved.py | 395 ++++++----- ...prepareFLEXPART.py => prepare_flexpart.py} | 65 +- python/profiling.py | 4 +- python/submit.py | 58 +- python/{testsuite.py => test_suite.py} | 32 +- python/{Tools.py => tools.py} | 191 +++--- 16 files changed, 1048 insertions(+), 949 deletions(-) create mode 100644 python/CONTROL_OD rename python/{ECFlexpart.py => EcFlexpart.py} (69%) rename python/{MARSretrieval.py => MarsRetrieval.py} (90%) rename python/{UIOFiles.py => UioFiles.py} (79%) rename python/{Disagg.py => disaggregation.py} (93%) rename python/{getMARSdata.py => get_mars_data.py} (74%) rename python/{prepareFLEXPART.py => prepare_flexpart.py} (79%) rename python/{testsuite.py => test_suite.py} (86%) rename python/{Tools.py => tools.py} (78%) diff --git a/ECMWF_FPparameter.xlsx b/ECMWF_FPparameter.xlsx index 21bae48a2bef0dbb1dc5d02d0482921cb980a01e..20207d5a2ffdfa77f4f6686d6fc3332e284e3988 100644 GIT binary patch literal 14531 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 literal 20494 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 KFfjb`0`UOSKBaj8 diff --git a/python/CONTROL_OD b/python/CONTROL_OD new file mode 100644 index 0000000..f82261e --- /dev/null +++ b/python/CONTROL_OD @@ -0,0 +1,35 @@ +DAY1 20151007 +DAY2 20151007 +DTIME 3 +M_TYPE AN FC FC FC FC FC AN FC FC FC FC FC AN FC FC FC FC FC AN FC FC FC FC FC +M_TIME 00 00 00 00 00 00 06 00 00 00 00 00 12 12 12 12 12 12 18 12 12 12 12 12 +M_STEP 00 01 02 03 04 05 00 07 08 09 10 11 00 01 02 03 04 05 00 07 08 09 10 11 +M_CLASS OD +M_STREAM OPER +M_NUMBER OFF +M_EXPVER 1 +M_GRID 1000 +M_LEFT -10000 +M_LOWER 30000 +M_UPPER 60000 +M_RIGHT 30000 +M_LEVEL 137 +M_LEVELIST 130/to/137 +M_RESOL 399 +M_GAUSS 0 +M_ACCURACY 16 +M_OMEGA 0 +M_OMEGADIFF 0 +M_ETA 1 +M_ETADIFF 0 +M_DPDETA 1 +M_SMOOTH 0 +M_FORMAT GRIB1 +M_ADDPAR 186/187/188/235/139/39 +PREFIX EO +ECSTORAGE 0 +ECTRANS 1 +ECFSDIR ectmp:/${USER}/econdemand/ +MAILOPS ${USER} +MAILFAIL ${USER} +EOF diff --git a/python/ControlFile.py b/python/ControlFile.py index 8974757..2fd438d 100644 --- a/python/ControlFile.py +++ b/python/ControlFile.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP +# ToDo AP # - write a test class #************************************************************************ #******************************************************************************* @@ -36,7 +36,28 @@ # @Class Content: # - __init__ # - __str__ -# - tolist +# - to_list +# +# @Class Attributes: +# - start_date +# - end_date +# - accuracy +# - omega +# - cwc +# - omegadiff +# - etadiff +# - level +# - levelist +# - step +# - maxstep +# - prefix +# - makefile +# - basetime +# - date_chunk +# - grib2flexpart +# - exedir +# - flexpart_root_scripts +# - ecmwfdatadir # #******************************************************************************* @@ -45,13 +66,14 @@ # ------------------------------------------------------------------------------ import os import inspect + # software specific module from flex_extract -import Tools +from tools import get_list_as_string, my_error # ------------------------------------------------------------------------------ # CLASS # ------------------------------------------------------------------------------ -class ControlFile: +class ControlFile(object): ''' Class containing the information of the flex_extract CONTROL file. @@ -113,7 +135,6 @@ class ControlFile: data = [data[0]] for d in dd: data.append(d) - pass if len(data) == 2: if '$' in data[1]: setattr(self, data[0].lower(), data[1]) @@ -125,11 +146,9 @@ class ControlFile: if var is not None: data[1] = data[1][:i] + var + data[1][k+1:] else: - Tools.myerror(None, - 'Could not find variable ' + - data[1][j+1:k] + - ' while reading ' + - filename) + my_error(None, 'Could not find variable ' + + data[1][j+1:k] + ' while reading ' + + filename) setattr(self, data[0].lower() + '_expanded', data[1]) else: if data[1].lower() != 'none': @@ -159,8 +178,8 @@ class ControlFile: self.etadiff = '0' if not hasattr(self, 'levelist'): if not hasattr(self, 'level'): - print('Warning: neither levelist nor level \ - specified in CONTROL file') + print 'Warning: neither levelist nor level \ + specified in CONTROL file' else: self.levelist = '1/to/' + self.level else: @@ -223,7 +242,7 @@ class ControlFile: return ', '.join("%s: %s" % item for item in attrs.items()) - def tolist(self): + def to_list(self): ''' @Description: Just generates a list of strings containing the attributes and @@ -254,7 +273,7 @@ class ControlFile: elif 'ecmwfdatadir' in item[0]: pass else: - if type(item[1]) is list: + if isinstance(item[1], list): stot = '' for s in item[1]: stot += s + ' ' @@ -264,3 +283,24 @@ class ControlFile: l.append("%s %s" % item) return sorted(l) + + # def to_dict(self): + # ''' + + # ''' + # parameters_dict = vars(self) + + # # remove unneeded parameter + # parameters_dict.pop('_expanded', None) + # parameters_dict.pop('exedir', None) + # parameters_dict.pop('flexpart_root_scripts', None) + # parameters_dict.pop('ecmwfdatadir', None) + + # parameters_dict_str = {} + # for key, value in parameters_dict.iteritems(): + # if isinstance(value, list): + # parameters_dict_str[str(key)] = get_list_as_string(value, ' ') + # else: + # parameters_dict_str[str(key)] = str(value) + + # return parameters_dict_str diff --git a/python/ECFlexpart.py b/python/EcFlexpart.py similarity index 69% rename from python/ECFlexpart.py rename to python/EcFlexpart.py index 0537feb..4d53874 100644 --- a/python/ECFlexpart.py +++ b/python/EcFlexpart.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP +# ToDo AP # - specifiy file header documentation # - add class description in header information # - apply classtests @@ -20,8 +20,8 @@ # November 2015 - Leopold Haimberger (University of Vienna): # - extended with class Control # - removed functions mkdir_p, daterange, years_between, months_between -# - added functions darain, dapoly, toparamId, init128, normalexit, -# myerror, cleanup, install_args_and_control, +# - added functions darain, dapoly, to_param_id, init128, normal_exit, +# my_error, clean_up, install_args_and_control, # interpret_args_and_control, # - removed function __del__ in class EIFLexpart # - added the following functions in EIFlexpart: @@ -39,10 +39,10 @@ # February 2018 - Anne Philipp (University of Vienna): # - applied PEP8 style guide # - added documentation -# - removed function getFlexpartTime in class ECFlexpart +# - removed function getFlexpartTime in class EcFlexpart # - outsourced class ControlFile # - outsourced class MarsRetrieval -# - changed class name from EIFlexpart to ECFlexpart +# - changed class name from EIFlexpart to EcFlexpart # - applied minor code changes (style) # - removed "dead code" , e.g. retrieval of Q since it is not needed # - removed "times" parameter from retrieve-method since it is not used @@ -69,38 +69,57 @@ # - create # - deacc_fluxes # +# @Class Attributes: +# - dtime +# - basetime +# - server +# - marsclass +# - stream +# - resol +# - accuracy +# - number +# - expver +# - glevelist +# - area +# - grid +# - level +# - levelist +# - types +# - dates +# - area +# - gaussian +# - params +# - inputdir +# - outputfilelist +# #******************************************************************************* - +#pylint: disable=unsupported-assignment-operation +# this is disabled because its an error in pylint for this specific case +#pylint: disable=consider-using-enumerate +# this is not useful in this case # ------------------------------------------------------------------------------ # MODULES # ------------------------------------------------------------------------------ import subprocess import shutil import os -import sys -import inspect import glob -import datetime -from numpy import * -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter -ecapi = True -try: - import ecmwfapi -except ImportError: - ecapi = False -from gribapi import * +from datetime import datetime, timedelta +import numpy as np +from gribapi import grib_set, grib_index_select, grib_new_from_index, grib_get,\ + grib_write, grib_get_values, grib_set_values, grib_release,\ + grib_index_release, grib_index_get # software specific classes and modules from flex_extract -from GribTools import GribTools -from Tools import init128, toparamId, silentremove, product -from ControlFile import ControlFile -from MARSretrieval import MARSretrieval -import Disagg +from Gribtools import Gribtools +from tools import init128, to_param_id, silent_remove, product, my_error +from MarsRetrieval import MarsRetrieval +import disaggregation # ------------------------------------------------------------------------------ # CLASS # ------------------------------------------------------------------------------ -class ECFlexpart: +class EcFlexpart(object): ''' Class to retrieve FLEXPART specific ECMWF data. ''' @@ -110,11 +129,11 @@ class ECFlexpart: def __init__(self, c, fluxes=False): ''' @Description: - Creates an object/instance of ECFlexpart with the + Creates an object/instance of EcFlexpart with the associated settings of its attributes for the retrieval. @Input: - self: instance of ECFlexpart + self: instance of EcFlexpart The current object of the class. c: instance of class ControlFile @@ -141,17 +160,15 @@ class ECFlexpart: # different mars types for retrieving data for flexpart self.types = dict() - try: - if c.maxstep > len(c.type): # Pure forecast mode - c.type = [c.type[1]] - c.step = ['{:0>3}'.format(int(c.step[0]))] - c.time = [c.time[0]] - for i in range(1, c.maxstep + 1): - c.type.append(c.type[0]) - c.step.append('{:0>3}'.format(i)) - c.time.append(c.time[0]) - except: - pass + + if c.maxstep > len(c.type): # Pure forecast mode + c.type = [c.type[1]] + c.step = ['{:0>3}'.format(int(c.step[0]))] + c.time = [c.time[0]] + for i in range(1, c.maxstep + 1): + c.type.append(c.type[0]) + c.step.append('{:0>3}'.format(i)) + c.time.append(c.time[0]) self.inputdir = c.inputdir self.basetime = c.basetime @@ -174,57 +191,51 @@ class ECFlexpart: if c.basetime == '00': btlist = [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 0] - if mod(i, int(c.dtime)) == 0 and \ - (c.maxstep > 24 or i in btlist): + if i % int(c.dtime) == 0 and c.maxstep > 24 or i in btlist: if ty not in self.types.keys(): self.types[ty] = {'times': '', 'steps': ''} if ti not in self.types[ty]['times']: - if len(self.types[ty]['times']) > 0: + if self.types[ty]['times']: self.types[ty]['times'] += '/' self.types[ty]['times'] += ti if st not in self.types[ty]['steps']: - if len(self.types[ty]['steps']) > 0: + if self.types[ty]['steps']: self.types[ty]['steps'] += '/' self.types[ty]['steps'] += st i += 1 - # Different grids need different retrievals - # SH = Spherical Harmonics, GG = Gaussian Grid, - # OG = Output Grid, ML = MultiLevel, SL = SingleLevel - self.params = {'SH__ML': '', 'SH__SL': '', - 'GG__ML': '', 'GG__SL': '', - 'OG__ML': '', 'OG__SL': '', - 'OG_OROLSM_SL': '', 'OG_acc_SL': ''} + self.marsclass = c.marsclass self.stream = c.stream self.number = c.number self.resol = c.resol self.accuracy = c.accuracy self.level = c.level - try: + + if c.levelist: self.levelist = c.levelist - except: + else: self.levelist = '1/to/' + c.level # for gaussian grid retrieval self.glevelist = '1/to/' + c.level - try: + if c.gaussian: self.gaussian = c.gaussian - except: + else: self.gaussian = '' - try: + if c.expver: self.expver = c.expver - except: + else: self.expver = '1' - try: + if c.number: self.number = c.number - except: + else: self.number = '0' if 'N' in c.grid: # Gaussian output grid @@ -249,12 +260,21 @@ class ECFlexpart: # 3) Calculation/Retrieval of omega # 4) Download also data for WRF + + # Different grids need different retrievals + # SH = Spherical Harmonics, GG = Gaussian Grid, + # OG = Output Grid, ML = MultiLevel, SL = SingleLevel + self.params = {'SH__ML': '', 'SH__SL': '', + 'GG__ML': '', 'GG__SL': '', + 'OG__ML': '', 'OG__SL': '', + 'OG_OROLSM_SL': '', 'OG_acc_SL': ''} + if fluxes is False: self.params['SH__SL'] = ['LNSP', 'ML', '1', 'OFF'] # "SD/MSL/TCC/10U/10V/2T/2D/129/172" self.params['OG__SL'] = ["141/151/164/165/166/167/168/129/172", \ 'SFC', '1', self.grid] - if len(c.addpar) > 0: + if c.addpar: if c.addpar[0] == '/': c.addpar = c.addpar[1:] self.params['OG__SL'][0] += '/' + '/'.join(c.addpar) @@ -276,8 +296,8 @@ class ECFlexpart: '{}'.format((int(self.resol) + 1) / 2)] self.params['SH__ML'] = ['U/V/D', 'ML', self.glevelist, 'OFF'] else: - print('Warning: This is a very costly parameter combination, \ - use only for debugging!') + print 'Warning: This is a very costly parameter combination, \ + use only for debugging!' self.params['GG__SL'] = ['Q', 'ML', '1', \ '{}'.format((int(self.resol) + 1) / 2)] self.params['GG__ML'] = ['U/V/D/77', 'ML', self.glevelist, \ @@ -286,29 +306,24 @@ class ECFlexpart: if c.omega == '1': self.params['OG__ML'][0] += '/W' - try: - # add cloud water content if necessary - if c.cwc == '1': - self.params['OG__ML'][0] += '/CLWC/CIWC' - except: - pass + # add cloud water content if necessary + if c.cwc == '1': + self.params['OG__ML'][0] += '/CLWC/CIWC' + + # add vorticity and geopotential height for WRF if necessary + if c.wrf == '1': + self.params['OG__ML'][0] += '/Z/VO' + if '/D' not in self.params['OG__ML'][0]: + self.params['OG__ML'][0] += '/D' + #wrf_sfc = 'sp/msl/skt/2t/10u/10v/2d/z/lsm/sst/ci/sd/stl1/ / + # stl2/stl3/stl4/swvl1/swvl2/swvl3/swvl4'.upper() + wrf_sfc = '134/235/167/165/166/168/129/172/34/31/141/ \ + 139/170/183/236/39/40/41/42'.upper() + lwrt_sfc = wrf_sfc.split('/') + for par in lwrt_sfc: + if par not in self.params['OG__SL'][0]: + self.params['OG__SL'][0] += '/' + par - try: - # add vorticity and geopotential height for WRF if necessary - if c.wrf == '1': - self.params['OG__ML'][0] += '/Z/VO' - if '/D' not in self.params['OG__ML'][0]: - self.params['OG__ML'][0] += '/D' - #wrf_sfc = 'sp/msl/skt/2t/10u/10v/2d/z/lsm/sst/ci/sd/stl1/ / - # stl2/stl3/stl4/swvl1/swvl2/swvl3/swvl4'.upper() - wrf_sfc = '134/235/167/165/166/168/129/172/34/31/141/ \ - 139/170/183/236/39/40/41/42'.upper() - lwrt_sfc = wrf_sfc.split('/') - for par in lwrt_sfc: - if par not in self.params['OG__SL'][0]: - self.params['OG__SL'][0] += '/' + par - except: - pass else: self.params['OG_acc_SL'] = ["LSP/CP/SSHF/EWSS/NSSS/SSR", \ 'SFC', '1', self.grid] @@ -327,7 +342,7 @@ class ECFlexpart: momega, momegadiff, mgauss, msmooth, meta, metadiff, mdpdeta @Input: - self: instance of ECFlexpart + self: instance of EcFlexpart The current object of the class. c: instance of class ControlFile @@ -351,27 +366,32 @@ class ECFlexpart: ''' self.inputdir = c.inputdir - area = asarray(self.area.split('/')).astype(float) - grid = asarray(self.grid.split('/')).astype(float) + area = np.asarray(self.area.split('/')).astype(float) + grid = np.asarray(self.grid.split('/')).astype(float) if area[1] > area[3]: area[1] -= 360 - zyk = abs((area[3] - area[1] - 360.) + grid[1]) < 1.e-6 maxl = int((area[3] - area[1]) / grid[1]) + 1 maxb = int((area[0] - area[2]) / grid[0]) + 1 with open(self.inputdir + '/' + filename, 'w') as f: f.write('&NAMGEN\n') f.write(',\n '.join(['maxl = ' + str(maxl), 'maxb = ' + str(maxb), - 'mlevel = ' + self.level, - 'mlevelist = ' + '"' + self.levelist + '"', - 'mnauf = ' + self.resol, 'metapar = ' + '77', - 'rlo0 = ' + str(area[1]), 'rlo1 = ' + str(area[3]), - 'rla0 = ' + str(area[2]), 'rla1 = ' + str(area[0]), - 'momega = ' + c.omega, 'momegadiff = ' + c.omegadiff, - 'mgauss = ' + c.gauss, 'msmooth = ' + c.smooth, - 'meta = ' + c.eta, 'metadiff = ' + c.etadiff, - 'mdpdeta = ' + c.dpdeta])) + 'mlevel = ' + self.level, + 'mlevelist = ' + '"' + self.levelist + '"', + 'mnauf = ' + self.resol, + 'metapar = ' + '77', + 'rlo0 = ' + str(area[1]), + 'rlo1 = ' + str(area[3]), + 'rla0 = ' + str(area[2]), + 'rla1 = ' + str(area[0]), + 'momega = ' + c.omega, + 'momegadiff = ' + c.omegadiff, + 'mgauss = ' + c.gauss, + 'msmooth = ' + c.smooth, + 'meta = ' + c.eta, + 'metadiff = ' + c.etadiff, + 'mdpdeta = ' + c.dpdeta])) f.write('\n/\n') @@ -385,7 +405,7 @@ class ECFlexpart: Prepares MARS retrievals per grid type and submits them. @Input: - self: instance of ECFlexpart + self: instance of EcFlexpart The current object of the class. server: instance of ECMWFService or ECMWFDataServer @@ -448,17 +468,27 @@ class ECFlexpart: # ------ on demand path -------------------------------------------------- if self.basetime is None: - MR = MARSretrieval(self.server, - marsclass=self.marsclass, stream=mfstream, - type=mftype, levtype=pv[1], levelist=pv[2], - resol=self.resol, gaussian=gaussian, - accuracy=self.accuracy, grid=pv[3], - target=mftarget, area=area, date=mfdate, - time=mftime, number=self.number, step=mfstep, - expver=self.expver, param=pv[0]) - - MR.displayInfo() - MR.dataRetrieve() + MR = MarsRetrieval(self.server, + marsclass=self.marsclass, + stream=mfstream, + type=mftype, + levtype=pv[1], + levelist=pv[2], + resol=self.resol, + gaussian=gaussian, + accuracy=self.accuracy, + grid=pv[3], + target=mftarget, + area=area, + date=mfdate, + time=mftime, + number=self.number, + step=mfstep, + expver=self.expver, + param=pv[0]) + + MR.display_info() + MR.data_retrieve() # ------ operational path ------------------------------------------------ else: # check if mars job requests fields beyond basetime. @@ -474,13 +504,14 @@ class ECFlexpart: else: tm1 = -1 - maxtime = datetime.datetime.strptime( - mfdate.split('/')[-1] + mftime.split('/')[tm1], - '%Y%m%d%H') + datetime.timedelta( - hours=int(mfstep.split('/')[sm1])) - elimit = datetime.datetime.strptime( - mfdate.split('/')[-1] + - self.basetime, '%Y%m%d%H') + maxdate = datetime.strptime(mfdate.split('/')[-1] + + mftime.split('/')[tm1], + '%Y%m%d%H') + istep = int(mfstep.split('/')[sm1]) + maxtime = maxdate + timedelta(hours=istep) + + elimit = datetime.strptime(mfdate.split('/')[-1] + + self.basetime, '%Y%m%d%H') if self.basetime == '12': # -------------- flux data ---------------------------- @@ -491,117 +522,157 @@ class ECFlexpart: # if 12h <= maxtime-elimit<12h reduce time for last date # if maxtime-elimit<12h reduce step for last time # A split of the MARS job into 2 is likely necessary. - maxtime = elimit - datetime.timedelta(hours=24) - mfdate = '/'.join(('/'.join(mfdate.split('/')[:-1]), - datetime.datetime.strftime( - maxtime, '%Y%m%d'))) - - MR = MARSretrieval(self.server, - marsclass=self.marsclass, - stream=self.stream, type=mftype, - levtype=pv[1], levelist=pv[2], - resol=self.resol, gaussian=gaussian, - accuracy=self.accuracy, grid=pv[3], - target=mftarget, area=area, - date=mfdate, time=mftime, - number=self.number, step=mfstep, - expver=self.expver, param=pv[0]) - - MR.displayInfo() - MR.dataRetrieve() - - maxtime = elimit - datetime.timedelta(hours=12) - mfdate = datetime.datetime.strftime(maxtime, - '%Y%m%d') + maxtime = elimit - timedelta(hours=24) + mfdate = '/'.join(['/'.join(mfdate.split('/')[:-1]), + datetime.strftime(maxtime, + '%Y%m%d')]) + + MR = MarsRetrieval(self.server, + marsclass=self.marsclass, + stream=self.stream, + type=mftype, + levtype=pv[1], + levelist=pv[2], + resol=self.resol, + gaussian=gaussian, + accuracy=self.accuracy, + grid=pv[3], + target=mftarget, + area=area, + date=mfdate, + time=mftime, + number=self.number, + step=mfstep, + expver=self.expver, + param=pv[0]) + + MR.display_info() + MR.data_retrieve() + + maxtime = elimit - timedelta(hours=12) + mfdate = datetime.strftime(maxtime, '%Y%m%d') mftime = '00' mftarget = self.inputdir + "/" + ftype + pk + \ '.' + mfdate + '.' + str(os.getppid()) +\ '.' + str(os.getpid()) + ".grb" - MR = MARSretrieval(self.server, - marsclass=self.marsclass, - stream=self.stream, type=mftype, - levtype=pv[1], levelist=pv[2], - resol=self.resol, gaussian=gaussian, - accuracy=self.accuracy, grid=pv[3], - target=mftarget, area=area, - date=mfdate, time=mftime, - number=self.number, step=mfstep, - expver=self.expver, param=pv[0]) - - MR.displayInfo() - MR.dataRetrieve() + MR = MarsRetrieval(self.server, + marsclass=self.marsclass, + stream=self.stream, + type=mftype, + levtype=pv[1], + levelist=pv[2], + resol=self.resol, + gaussian=gaussian, + accuracy=self.accuracy, + grid=pv[3], + target=mftarget, + area=area, + date=mfdate, + time=mftime, + number=self.number, + step=mfstep, + expver=self.expver, + param=pv[0]) + + MR.display_info() + MR.data_retrieve() # -------------- non flux data ------------------------ else: - MR = MARSretrieval(self.server, - marsclass=self.marsclass, - stream=self.stream, type=mftype, - levtype=pv[1], levelist=pv[2], - resol=self.resol, gaussian=gaussian, - accuracy=self.accuracy, grid=pv[3], - target=mftarget, area=area, - date=mfdate, time=mftime, - number=self.number, step=mfstep, - expver=self.expver, param=pv[0]) - - MR.displayInfo() - MR.dataRetrieve() + MR = MarsRetrieval(self.server, + marsclass=self.marsclass, + stream=self.stream, + type=mftype, + levtype=pv[1], + levelist=pv[2], + resol=self.resol, + gaussian=gaussian, + accuracy=self.accuracy, + grid=pv[3], + target=mftarget, + area=area, + date=mfdate, + time=mftime, + number=self.number, + step=mfstep, + expver=self.expver, + param=pv[0]) + + MR.display_info() + MR.data_retrieve() else: # basetime == 0 ??? #AP - maxtime = elimit - datetime.timedelta(hours=24) - mfdate = datetime.datetime.strftime(maxtime,'%Y%m%d') + maxtime = elimit - timedelta(hours=24) + mfdate = datetime.strftime(maxtime, '%Y%m%d') mftimesave = ''.join(mftime) if '/' in mftime: times = mftime.split('/') while ((int(times[0]) + - int(mfstep.split('/')[0]) <= 12) and - (pk != 'OG_OROLSM__SL') and 'acc' not in pk): + int(mfstep.split('/')[0]) <= 12) and + (pk != 'OG_OROLSM__SL') and 'acc' not in pk): times = times[1:] if len(times) > 1: mftime = '/'.join(times) else: mftime = times[0] - MR = MARSretrieval(self.server, - marsclass=self.marsclass, - stream=self.stream, type=mftype, - levtype=pv[1], levelist=pv[2], - resol=self.resol, gaussian=gaussian, - accuracy=self.accuracy, grid=pv[3], - target=mftarget, area=area, - date=mfdate, time=mftime, - number=self.number, step=mfstep, - expver=self.expver, param=pv[0]) - - MR.displayInfo() - MR.dataRetrieve() + MR = MarsRetrieval(self.server, + marsclass=self.marsclass, + stream=self.stream, + type=mftype, + levtype=pv[1], + levelist=pv[2], + resol=self.resol, + gaussian=gaussian, + accuracy=self.accuracy, + grid=pv[3], + target=mftarget, + area=area, + date=mfdate, + time=mftime, + number=self.number, + step=mfstep, + expver=self.expver, + param=pv[0]) + + MR.display_info() + MR.data_retrieve() if (int(mftimesave.split('/')[0]) == 0 and - int(mfstep.split('/')[0]) == 0 and - pk != 'OG_OROLSM__SL'): - mfdate = datetime.datetime.strftime(elimit,'%Y%m%d') + int(mfstep.split('/')[0]) == 0 and + pk != 'OG_OROLSM__SL'): + + mfdate = datetime.strftime(elimit, '%Y%m%d') mftime = '00' mfstep = '000' mftarget = self.inputdir + "/" + ftype + pk + \ '.' + mfdate + '.' + str(os.getppid()) +\ '.' + str(os.getpid()) + ".grb" - MR = MARSretrieval(self.server, - marsclass=self.marsclass, - stream=self.stream, type=mftype, - levtype=pv[1], levelist=pv[2], - resol=self.resol, gaussian=gaussian, - accuracy=self.accuracy, grid=pv[3], - target=mftarget, area=area, - date=mfdate, time=mftime, - number=self.number, step=mfstep, - expver=self.expver, param=pv[0]) - - MR.displayInfo() - MR.dataRetrieve() - - print("MARS retrieve done... ") + MR = MarsRetrieval(self.server, + marsclass=self.marsclass, + stream=self.stream, + type=mftype, + levtype=pv[1], + levelist=pv[2], + resol=self.resol, + gaussian=gaussian, + accuracy=self.accuracy, + grid=pv[3], + target=mftarget, + area=area, + date=mfdate, + time=mftime, + number=self.number, + step=mfstep, + expver=self.expver, + param=pv[0]) + + MR.display_info() + MR.data_retrieve() + + print "MARS retrieve done... " return @@ -620,7 +691,7 @@ class ECFlexpart: GRIB2FLEXPART - Conversion of GRIB files to FLEXPART binary format @Input: - self: instance of ECFlexpart + self: instance of EcFlexpart The current object of the class. c: instance of class ControlFile @@ -641,7 +712,7 @@ class ECFlexpart: ''' - print('\n\nPostprocessing:\n Format: {}\n'.format(c.format)) + print '\n\nPostprocessing:\n Format: {}\n'.format(c.format) if c.ecapi is False: print('ecstorage: {}\n ecfsdir: {}\n'. @@ -651,29 +722,29 @@ class ECFlexpart: if not hasattr(c, 'destination'): c.destination = os.getenv('DESTINATION') print('ectrans: {}\n gateway: {}\n destination: {}\n ' - .format(c.ectrans, c.gateway, c.destination)) + .format(c.ectrans, c.gateway, c.destination)) - print('Output filelist: \n') - print(self.outputfilelist) + print 'Output filelist: \n' + print self.outputfilelist if c.format.lower() == 'grib2': for ofile in self.outputfilelist: p = subprocess.check_call(['grib_set', '-s', 'edition=2, \ - productDefinitionTemplateNumber=8', - ofile, ofile + '_2']) + productDefinitionTemplateNumber=8', + ofile, ofile + '_2']) p = subprocess.check_call(['mv', ofile + '_2', ofile]) if int(c.ectrans) == 1 and c.ecapi is False: for ofile in self.outputfilelist: p = subprocess.check_call(['ectrans', '-overwrite', '-gateway', - c.gateway, '-remote', c.destination, - '-source', ofile]) + c.gateway, '-remote', c.destination, + '-source', ofile]) print('ectrans:', p) if int(c.ecstorage) == 1 and c.ecapi is False: for ofile in self.outputfilelist: p = subprocess.check_call(['ecp', '-o', ofile, - os.path.expandvars(c.ecfsdir)]) + os.path.expandvars(c.ecfsdir)]) if c.outputdir != c.inputdir: for ofile in self.outputfilelist: @@ -691,11 +762,11 @@ class ECFlexpart: fname = ofile.split('/') if '.' in fname[-1]: l = fname[-1].split('.') - timestamp = datetime.datetime.strptime(l[0][-6:] + l[1], - '%y%m%d%H') - timestamp += datetime.timedelta(hours=int(l[2])) - cdate = datetime.datetime.strftime(timestamp, '%Y%m%d') - chms = datetime.datetime.strftime(timestamp, '%H%M%S') + timestamp = datetime.strptime(l[0][-6:] + l[1], + '%y%m%d%H') + timestamp += timedelta(hours=int(l[2])) + cdate = datetime.strftime(timestamp, '%Y%m%d') + chms = datetime.strftime(timestamp, '%H%M%S') else: cdate = '20' + fname[-1][-8:-2] chms = fname[-1][-2:] + '0000' @@ -707,7 +778,7 @@ class ECFlexpart: # generate pathnames file pwd = os.path.abspath(c.outputdir) - with open(pwd + '/pathnames','w') as f: + with open(pwd + '/pathnames', 'w') as f: f.write(pwd + '/Options/\n') f.write(pwd + '/\n') f.write(pwd + '/\n') @@ -719,9 +790,8 @@ class ECFlexpart: os.makedirs(pwd+'/Options') # read template COMMAND file - with open(os.path.expandvars( - os.path.expanduser(c.flexpart_root_scripts)) + - '/../Options/COMMAND', 'r') as f: + with open(os.path.expandvars(os.path.expanduser( + c.flexpart_root_scripts)) + '/../Options/COMMAND', 'r') as f: lflist = f.read().split('\n') # find index of list where to put in the @@ -746,10 +816,9 @@ class ECFlexpart: # change to outputdir and start the grib2flexpart run # afterwards switch back to the working dir os.chdir(c.outputdir) - p = subprocess.check_call([os.path.expandvars( - os.path.expanduser(c.flexpart_root_scripts)) + - '/../FLEXPART_PROGRAM/grib2flexpart', - 'useAvailable', '.']) + p = subprocess.check_call([ + os.path.expandvars(os.path.expanduser(c.flexpart_root_scripts)) + + '/../FLEXPART_PROGRAM/grib2flexpart', 'useAvailable', '.']) os.chdir(pwd) return @@ -770,10 +839,10 @@ class ECFlexpart: "stepRange"). @Input: - self: instance of ECFlexpart + self: instance of EcFlexpart The current object of the class. - inputfiles: instance of UIOFiles + inputfiles: instance of UioFiles Contains a list of files. c: instance of class ControlFile @@ -795,14 +864,14 @@ class ECFlexpart: table128 = init128(c.ecmwfdatadir + '/grib_templates/ecmwf_grib1_table_128') - wrfpars = toparamId('sp/mslp/skt/2t/10u/10v/2d/z/lsm/sst/ci/sd/\ + wrfpars = to_param_id('sp/mslp/skt/2t/10u/10v/2d/z/lsm/sst/ci/sd/\ stl1/stl2/stl3/stl4/swvl1/swvl2/swvl3/swvl4', table128) index_keys = ["date", "time", "step"] indexfile = c.inputdir + "/date_time_stepRange.idx" - silentremove(indexfile) - grib = GribTools(inputfiles.files) + silent_remove(indexfile) + grib = Gribtools(inputfiles.files) # creates new index file iid = grib.index(index_keys=index_keys, index_file=indexfile) @@ -810,7 +879,7 @@ class ECFlexpart: index_vals = [] for key in index_keys: index_vals.append(grib_index_get(iid, key)) - print(index_vals[-1]) + print index_vals[-1] # index_vals looks for example like: # index_vals[0]: ('20171106', '20171107', '20171108') ; date # index_vals[1]: ('0', '1200', '1800', '600') ; time @@ -841,39 +910,33 @@ class ECFlexpart: convertFlag = True # remove old fort.* files and open new ones for k, f in fdict.iteritems(): - silentremove(c.inputdir + "/fort." + k) + silent_remove(c.inputdir + "/fort." + k) fdict[k] = open(c.inputdir + '/fort.' + k, 'w') cdate = str(grib_get(gid, 'date')) time = grib_get(gid, 'time') - type = grib_get(gid, 'type') step = grib_get(gid, 'step') # create correct timestamp from the three time informations # date, time, step - timestamp = datetime.datetime.strptime( - cdate + '{:0>2}'.format(time/100), '%Y%m%d%H') - timestamp += datetime.timedelta(hours=int(step)) + timestamp = datetime.strptime(cdate + '{:0>2}'.format(time/100), + '%Y%m%d%H') + timestamp += timedelta(hours=int(step)) - cdateH = datetime.datetime.strftime(timestamp, '%Y%m%d%H') - chms = datetime.datetime.strftime(timestamp, '%H%M%S') + cdateH = datetime.strftime(timestamp, '%Y%m%d%H') if c.basetime is not None: - slimit = datetime.datetime.strptime( - c.start_date + '00', '%Y%m%d%H') + slimit = datetime.strptime(c.start_date + '00', '%Y%m%d%H') bt = '23' if c.basetime == '00': bt = '00' - slimit = datetime.datetime.strptime( - c.end_date + bt, '%Y%m%d%H') - \ - datetime.timedelta(hours=12-int(c.dtime)) + slimit = datetime.strptime(c.end_date + bt, '%Y%m%d%H')\ + - timedelta(hours=12-int(c.dtime)) if c.basetime == '12': bt = '12' - slimit = datetime.datetime.strptime( - c.end_date + bt, '%Y%m%d%H') - \ - datetime.timedelta(hours=12-int(c.dtime)) + slimit = datetime.strptime(c.end_date + bt, '%Y%m%d%H')\ + - timedelta(hours=12-int(c.dtime)) - elimit = datetime.datetime.strptime( - c.end_date + bt, '%Y%m%d%H') + elimit = datetime.strptime(c.end_date + bt, '%Y%m%d%H') if timestamp < slimit or timestamp > elimit: continue @@ -900,7 +963,6 @@ class ECFlexpart: break paramId = grib_get(gid, 'paramId') gridtype = grib_get(gid, 'gridType') - datatype = grib_get(gid, 'dataType') levtype = grib_get(gid, 'typeOfLevel') if paramId == 133 and gridtype == 'reduced_gg': # Relative humidity (Q.grb) is used as a template only @@ -938,7 +1000,7 @@ class ECFlexpart: grib_write(gid, fdict['16']) savedfields.append(paramId) else: - print('duplicate ' + str(paramId) + ' not written') + print 'duplicate ' + str(paramId) + ' not written' try: if c.wrf == '1': @@ -962,16 +1024,17 @@ class ECFlexpart: pwd = os.getcwd() os.chdir(c.inputdir) if os.stat('fort.21').st_size == 0 and int(c.eta) == 1: - print('Parameter 77 (etadot) is missing, most likely it is \ - not available for this type or date/time\n') - print('Check parameters CLASS, TYPE, STREAM, START_DATE\n') - myerror(c, 'fort.21 is empty while parameter eta is set \ + print 'Parameter 77 (etadot) is missing, most likely it is \ + not available for this type or date/time\n' + print 'Check parameters CLASS, TYPE, STREAM, START_DATE\n' + my_error(c, 'fort.21 is empty while parameter eta is set \ to 1 in CONTROL file') # create the corresponding output file fort.15 # (generated by CONVERT2) + fort.16 (paramId 167 and 168) - p = subprocess.check_call([os.path.expandvars( - os.path.expanduser(c.exedir)) + '/CONVERT2'], shell=True) + p = subprocess.check_call( + [os.path.expandvars(os.path.expanduser(c.exedir)) + + '/CONVERT2'], shell=True) os.chdir(pwd) # create final output filename, e.g. EN13040500 (ENYYMMDDHH) @@ -982,7 +1045,7 @@ class ECFlexpart: else: suffix = cdateH[2:10] fnout += suffix - print("outputfile = " + fnout) + print "outputfile = " + fnout self.outputfilelist.append(fnout) # needed for final processing # create outputfile and copy all data from intermediate files @@ -1004,11 +1067,8 @@ class ECFlexpart: shutil.copyfileobj( open(c.inputdir + '/fort.25', 'rb'), fout) - try: - if c.wrf == '1': - fwrf.close() - except: - pass + if c.wrf == '1': + fwrf.close() grib_index_release(iid) @@ -1024,10 +1084,10 @@ class ECFlexpart: stress data (dapoly, cubic polynomial). @Input: - self: instance of ECFlexpart + self: instance of EcFlexpart The current object of the class. - inputfiles: instance of UIOFiles + inputfiles: instance of UioFiles Contains a list of files. c: instance of class ControlFile @@ -1049,19 +1109,19 @@ class ECFlexpart: table128 = init128(c.ecmwfdatadir + '/grib_templates/ecmwf_grib1_table_128') - pars = toparamId(self.params['OG_acc_SL'][0], table128) + pars = to_param_id(self.params['OG_acc_SL'][0], table128) index_keys = ["date", "time", "step"] indexfile = c.inputdir + "/date_time_stepRange.idx" - silentremove(indexfile) - grib = GribTools(inputfiles.files) + silent_remove(indexfile) + grib = Gribtools(inputfiles.files) # creates new index file iid = grib.index(index_keys=index_keys, index_file=indexfile) # read values of index keys index_vals = [] for key in index_keys: - key_vals = grib_index_get(iid,key) - print(key_vals) + key_vals = grib_index_get(iid, key) + print key_vals # have to sort the steps for disaggregation, # therefore convert to int first if key == 'step': @@ -1082,6 +1142,8 @@ class ECFlexpart: svalsdict[str(p)] = [] stepsdict[str(p)] = [] + print 'maxstep: ', c.maxstep + for prod in product(*index_vals): # e.g. prod = ('20170505', '0', '12') # ( date ,time, step) @@ -1091,23 +1153,19 @@ class ECFlexpart: grib_index_select(iid, index_keys[i], prod[i]) gid = grib_new_from_index(iid) - # do convert2 program if gid at this time is not None, - # therefore save in hid - hid = gid if gid is not None: cdate = grib_get(gid, 'date') time = grib_get(gid, 'time') - type = grib_get(gid, 'type') step = grib_get(gid, 'step') # date+time+step-2*dtime # (since interpolated value valid for step-2*dtime) - sdate = datetime.datetime(year=cdate/10000, - month=mod(cdate, 10000)/100, - day=mod(cdate, 100), - hour=time/100) - fdate = sdate + datetime.timedelta( - hours=step-2*int(c.dtime)) - sdates = sdate + datetime.timedelta(hours=step) + sdate = datetime(year=cdate/10000, + month=(cdate % 10000)/100, + day=(cdate % 100), + hour=time/100) + fdate = sdate + timedelta(hours=step-2*int(c.dtime)) + sdates = sdate + timedelta(hours=step) + elimit = None else: break @@ -1125,13 +1183,14 @@ class ECFlexpart: h = open(hnout, 'w') else: fnout = c.inputdir + '/flux' + fdate.strftime('%Y%m%d%H') - gnout = c.inputdir + '/flux' + (fdate+datetime.timedelta( - hours = int(c.dtime))).strftime('%Y%m%d%H') + gnout = c.inputdir + '/flux' + (fdate + + timedelta(hours=int(c.dtime)) + ).strftime('%Y%m%d%H') hnout = c.inputdir + '/flux' + sdates.strftime('%Y%m%d%H') g = open(gnout, 'w') h = open(hnout, 'w') - print("outputfile = " + fnout) + print "outputfile = " + fnout f = open(fnout, 'w') # read message for message and store relevant data fields @@ -1155,7 +1214,7 @@ class ECFlexpart: else: fak = 3600. - values = (reshape(values, (nj, ni))).flatten() / fak + values = (np.reshape(values, (nj, ni))).flatten() / fak vdp.append(values[:]) # save the accumulated values if step <= int(c.dtime): svdp.append(values[:] / int(c.dtime)) @@ -1163,7 +1222,7 @@ class ECFlexpart: svdp.append((vdp[-1] - vdp[-2]) / int(c.dtime)) print(cparamId, atime, step, len(values), - values[0], std(values)) + values[0], np.std(values)) # save the 1/3-hourly or specific values # svdp.append(values[:]) sd.append(step) @@ -1171,9 +1230,9 @@ class ECFlexpart: if len(svdp) >= 3: if len(svdp) > 3: if cparamId == '142' or cparamId == '143': - values = Disagg.darain(svdp) + values = disaggregation.darain(svdp) else: - values = Disagg.dapoly(svdp) + values = disaggregation.dapoly(svdp) if not (step == c.maxstep and c.maxstep > 12 \ or sdates == elimit): @@ -1196,27 +1255,26 @@ class ECFlexpart: grib_write(gid, f) if c.basetime is not None: - elimit = datetime.datetime.strptime(c.end_date + - c.basetime, - '%Y%m%d%H') + elimit = datetime.strptime(c.end_date + + c.basetime, '%Y%m%d%H') else: - elimit = sdate + datetime.timedelta(2*int(c.dtime)) + elimit = sdate + timedelta(2*int(c.dtime)) # squeeze out information of last two steps contained # in svdp # if step+int(c.dtime) == c.maxstep and c.maxstep>12 - # or sdates+datetime.timedelta(hours = int(c.dtime)) + # or sdates+timedelta(hours = int(c.dtime)) # >= elimit: # Note that svdp[0] has not been popped in this case - if (step == c.maxstep and c.maxstep > 12 - or sdates == elimit): + if step == c.maxstep and c.maxstep > 12 or \ + sdates == elimit: values = svdp[3] grib_set_values(gid, values) grib_set(gid, 'step', 0) - truedatetime = fdate + datetime.timedelta( - hours=2*int(c.dtime)) + truedatetime = fdate + timedelta(hours= + 2*int(c.dtime)) grib_set(gid, 'time', truedatetime.hour * 100) grib_set(gid, 'date', truedatetime.year * 10000 + truedatetime.month * 100 + @@ -1225,13 +1283,12 @@ class ECFlexpart: #values = (svdp[1]+svdp[2])/2. if cparamId == '142' or cparamId == '143': - values = Disagg.darain(list(reversed(svdp))) + values = disaggregation.darain(list(reversed(svdp))) else: - values = Disagg.dapoly(list(reversed(svdp))) + values = disaggregation.dapoly(list(reversed(svdp))) - grib_set(gid, 'step',0) - truedatetime = fdate + datetime.timedelta( - hours=int(c.dtime)) + grib_set(gid, 'step', 0) + truedatetime = fdate + timedelta(hours=int(c.dtime)) grib_set(gid, 'time', truedatetime.hour * 100) grib_set(gid, 'date', truedatetime.year * 10000 + truedatetime.month * 100 + @@ -1249,4 +1306,5 @@ class ECFlexpart: grib_index_release(iid) + exit() return diff --git a/python/GribTools.py b/python/GribTools.py index 2870334..e79e768 100644 --- a/python/GribTools.py +++ b/python/GribTools.py @@ -1,11 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#************************************************************************ -# TODO AP -# - GribTools name möglicherweise etwas verwirrend. -# - change self.filename in self.filenames!!! -# - bis auf --init-- und index wird keine Funktion verwendet!? -#************************************************************************ #******************************************************************************* # @Author: Anne Fouilloux (University of Oslo) # @@ -35,23 +29,29 @@ # # @Class Content: # - __init__ -# - getkeys -# - setkeys +# - get_keys +# - set_keys # - copy # - index # +# @Class Attributes: +# - filenames +# #******************************************************************************* # ------------------------------------------------------------------------------ # MODULES # ------------------------------------------------------------------------------ import os -from gribapi import * +from gribapi import grib_new_from_file, grib_is_defined, grib_get, \ + grib_release, grib_set, grib_write, grib_index_read, \ + grib_index_new_from_file, grib_index_add_file, \ + grib_index_write # ------------------------------------------------------------------------------ # CLASS # ------------------------------------------------------------------------------ -class GribTools: +class Gribtools(object): ''' Class for GRIB utilities (new methods) based on GRIB API ''' @@ -61,7 +61,7 @@ class GribTools: def __init__(self, filenames): ''' @Description: - Initialise an object of GribTools and assign a list + Initialise an object of Gribtools and assign a list of filenames. @Input: @@ -72,12 +72,12 @@ class GribTools: <nothing> ''' - self.filename = filenames + self.filenames = filenames return - def getkeys(self, keynames, wherekeynames=[], wherekeyvalues=[]): + def get_keys(self, keynames, wherekeynames=[], wherekeyvalues=[]): ''' @Description: get keyvalues for a given list of keynames @@ -87,10 +87,10 @@ class GribTools: keynames: list of strings List of keynames. - wherekeynames: list of ???, optional + wherekeynames: list of strings, optional Default value is an empty list. - wherekeyvalues: list of ???, optional + wherekeyvalues: list of strings, optional Default value is an empty list. @Return: @@ -98,7 +98,7 @@ class GribTools: List of keyvalues for given keynames. ''' - fileid = open(self.filename, 'r') + fileid = open(self.filenames, 'r') return_list = [] @@ -135,8 +135,8 @@ class GribTools: return return_list - def setkeys(self, fromfile, keynames, keyvalues, wherekeynames=[], - wherekeyvalues=[], strict=False, filemode='w'): + def set_keys(self, fromfile, keynames, keyvalues, wherekeynames=[], + wherekeyvalues=[], strict=False, filemode='w'): ''' @Description: Opens the file to read the grib messages and then write @@ -149,16 +149,16 @@ class GribTools: fromfile: string Filename of the input file to read the grib messages from. - keynames: list of ??? + keynames: list of strings List of keynames. Default is an empty list. - keyvalues: list of ??? + keyvalues: list of strings List of keynames. Default is an empty list. - wherekeynames: list of ???, optional + wherekeynames: list of strings, optional Default value is an empty list. - wherekeyvalues: list of ???, optional + wherekeyvalues: list of strings, optional Default value is an empty list. strict: boolean, optional @@ -173,7 +173,7 @@ class GribTools: <nothing> ''' - fout = open(self.filename, filemode) + fout = open(self.filenames, filemode) fin = open(fromfile) while 1: @@ -195,24 +195,14 @@ class GribTools: str(grib_get(gid_in, wherekey)))) i += 1 -#AP is it secured that the order of keynames is equal to keyvalues? if select: i = 0 for key in keynames: grib_set(gid_in, key, keyvalues[i]) i += 1 -#AP this is a redundant code section -# delete the if/else : -# -# grib_write(gid_in, fout) -# - if strict: - if select: - grib_write(gid_in, fout) - else: - grib_write(gid_in, fout) -#AP end + grib_write(gid_in, fout) + grib_release(gid_in) fin.close() @@ -237,10 +227,10 @@ class GribTools: different to (False) the keynames/keyvalues list passed to the function. Default is True. - keynames: list of ???, optional + keynames: list of strings, optional List of keynames. Default is an empty list. - keyvalues: list of ???, optional + keyvalues: list of strings, optional List of keynames. Default is an empty list. filemode: string, optional @@ -251,7 +241,7 @@ class GribTools: ''' fin = open(filename_in) - fout = open(self.filename, filemode) + fout = open(self.filenames, filemode) while 1: gid_in = grib_new_from_file(fin) @@ -306,29 +296,24 @@ class GribTools: iid: integer Grib index id. ''' - print("... index will be done") - self.iid = None + print "... index will be done" + iid = None - if (os.path.exists(index_file)): - self.iid = grib_index_read(index_file) - print("Use existing index file: %s " % (index_file)) + if os.path.exists(index_file): + iid = grib_index_read(index_file) + print "Use existing index file: %s " % (index_file) else: - for file in self.filename: - print("Inputfile: %s " % (file)) - if self.iid is None: - self.iid = grib_index_new_from_file(file, index_keys) + for filename in self.filenames: + print "Inputfile: %s " % (filename) + if iid is None: + iid = grib_index_new_from_file(filename, index_keys) else: print 'in else zweig' - grib_index_add_file(self.iid, file) - - if self.iid is not None: - grib_index_write(self.iid, index_file) - - print('... index done') - - return self.iid - - + grib_index_add_file(iid, filename) + if iid is not None: + grib_index_write(iid, index_file) + print '... index done' + return iid diff --git a/python/MARSretrieval.py b/python/MarsRetrieval.py similarity index 90% rename from python/MARSretrieval.py rename to python/MarsRetrieval.py index 3c1c5b3..eccac65 100644 --- a/python/MARSretrieval.py +++ b/python/MarsRetrieval.py @@ -1,9 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#************************************************************************ -# TODO AP -# - -#************************************************************************ #******************************************************************************* # @Author: Anne Fouilloux (University of Oslo) # @@ -12,8 +8,8 @@ # @Change History: # # November 2015 - Leopold Haimberger (University of Vienna): -# - optimized displayInfo -# - optimized dataRetrieve and seperate between python and shell +# - optimized display_info +# - optimized data_retrieve and seperate between python and shell # script call # # February 2018 - Anne Philipp (University of Vienna): @@ -36,8 +32,29 @@ # # @Class Content: # - __init__ -# - displayInfo -# - dataRetrieve +# - display_info +# - data_retrieve +# +# @Class Attributes: +# - server +# - marsclass +# - dtype +# - levtype +# - levelist +# - repres +# - date +# - resol +# - stream +# - area +# - time +# - step +# - expver +# - number +# - accuracy +# - grid +# - gaussian +# - target +# - param # #******************************************************************************* @@ -47,16 +64,10 @@ import subprocess import os -ecapi = True -try: - import ecmwfapi -except ImportError: - ecapi = False - # ------------------------------------------------------------------------------ # CLASS # ------------------------------------------------------------------------------ -class MARSretrieval: +class MarsRetrieval(object): ''' Class for submitting MARS retrievals. @@ -67,14 +78,14 @@ class MARSretrieval: ''' - def __init__(self, server, marsclass = "ei", type = "", levtype = "", - levelist = "", repres = "", date = "", resol = "", stream = "", - area = "", time = "", step = "", expver = "1", number = "", - accuracy = "", grid = "", gaussian = "", target = "", - param = ""): + def __init__(self, server, marsclass="ei", dtype="", levtype="", + levelist="", repres="", date="", resol="", stream="", + area="", time="", step="", expver="1", number="", + accuracy="", grid="", gaussian="", target="", + param=""): ''' @Description: - Initialises the instance of the MARSretrieval class and + Initialises the instance of the MarsRetrieval class and defines and assigns a set of the necessary retrieval parameters for the FLEXPART input data. A description of MARS keywords/arguments, their dependencies @@ -83,7 +94,7 @@ class MARSretrieval: https://software.ecmwf.int/wiki/display/UDOC/MARS+keywords @Input: - self: instance of MARSretrieval + self: instance of MarsRetrieval For description see class documentation. server: instance of ECMWFService (from ECMWF Web-API) @@ -95,7 +106,7 @@ class MARSretrieval: E4 (ERA40), OD (Operational archive), ea (ERA5). Default is the ERA-Interim dataset "ei". - type: string, optional + dtype: string, optional Determines the type of fields to be retrieved. Selects between observations, images or fields. Examples for fields: Analysis (an), Forecast (fc), @@ -274,7 +285,7 @@ class MARSretrieval: self.server = server self.marsclass = marsclass - self.type = type + self.dtype = dtype self.levtype = levtype self.levelist = levelist self.repres = repres @@ -295,13 +306,13 @@ class MARSretrieval: return - def displayInfo(self): + def display_info(self): ''' @Description: Prints all class attributes and their values. @Input: - self: instance of MARSretrieval + self: instance of MarsRetrieval For description see class documentation. @Return: @@ -313,14 +324,14 @@ class MARSretrieval: # iterate through all attributes and print them # with their corresponding values for item in attrs.items(): - if item[0] in ('server'): + if item[0] in 'server': pass else: - print(item[0] + ': ' + str(item[1])) + print item[0] + ': ' + str(item[1]) return - def dataRetrieve(self): + def data_retrieve(self): ''' @Description: Submits a MARS retrieval. Depending on the existence of @@ -329,7 +340,7 @@ class MARSretrieval: are taken from the defined class attributes. @Input: - self: instance of MARSretrieval + self: instance of MarsRetrieval For description see class documentation. @Return: @@ -343,7 +354,7 @@ class MARSretrieval: # needed for the retrieval call s = 'ret' for k, v in attrs.iteritems(): - if k in ('server'): + if k in 'server': continue if k == 'marsclass': k = 'class' @@ -359,11 +370,11 @@ class MARSretrieval: try: self.server.execute(s, target) except: - print('MARS Request failed, \ - have you already registered at apps.ecmwf.int?') + print 'MARS Request failed, \ + have you already registered at apps.ecmwf.int?' raise IOError if os.stat(target).st_size == 0: - print('MARS Request returned no data - please check request') + print 'MARS Request returned no data - please check request' raise IOError # MARS request via extra process in shell else: @@ -372,14 +383,14 @@ class MARSretrieval: stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) pout = p.communicate(input=s)[0] - print(pout.decode()) + print pout.decode() if 'Some errors reported' in pout.decode(): - print('MARS Request failed - please check request') + print 'MARS Request failed - please check request' raise IOError if os.stat(target).st_size == 0: - print('MARS Request returned no data - please check request') + print 'MARS Request returned no data - please check request' raise IOError return diff --git a/python/UIOFiles.py b/python/UioFiles.py similarity index 79% rename from python/UIOFiles.py rename to python/UioFiles.py index 7b01333..dbb5409 100644 --- a/python/UIOFiles.py +++ b/python/UioFiles.py @@ -1,11 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#************************************************************************ -# TODO AP -# - checken welche regelmässigen methoden auf diese Files noch angewendet werden -# und dann hier implementieren -# cleanup hier rein -#************************************************************************ #******************************************************************************* # @Author: Anne Fouilloux (University of Oslo) # @@ -14,15 +8,15 @@ # @Change History: # # November 2015 - Leopold Haimberger (University of Vienna): -# - modified method listFiles to work with glob instead of listdir -# - added pattern search in method listFiles +# - modified method list_files to work with glob instead of listdir +# - added pattern search in method list_files # # February 2018 - Anne Philipp (University of Vienna): # - applied PEP8 style guide # - added documentation -# - optimisation of method listFiles since it didn't work correctly +# - optimisation of method list_files since it didn't work correctly # for sub directories -# - additional speed up of method listFiles +# - additional speed up of method list_files # - modified the class so that it is initiated with a pattern instead # of suffixes. Gives more precision in selection of files. # @@ -39,8 +33,12 @@ # # @Class Content: # - __init__ -# - listFiles -# - deleteFiles +# - list_files +# - delete_files +# +# @Class Attributes: +# - pattern +# - files # #******************************************************************************* @@ -48,19 +46,17 @@ # MODULES # ------------------------------------------------------------------------------ import os -import glob import fnmatch -import time # software specific module from flex_extract -import profiling -from Tools import silentremove +#import profiling +from tools import silent_remove # ------------------------------------------------------------------------------ # CLASS # ------------------------------------------------------------------------------ -class UIOFiles: +class UioFiles(object): ''' Class to manipulate files. At initialisation it has the attribute pattern which stores a regular expression pattern for the files associated @@ -75,7 +71,7 @@ class UIOFiles: Assignes a specific pattern for these files. @Input: - self: instance of UIOFiles + self: instance of UioFiles Description see class documentation. pattern: string @@ -86,18 +82,19 @@ class UIOFiles: ''' self.pattern = pattern + self.files = None return #@profiling.timefn - def listFiles(self, path, callid=0): + def list_files(self, path, callid=0): ''' @Description: Lists all files in the directory with the matching regular expression pattern. @Input: - self: instance of UIOFiles + self: instance of UioFiles Description see class documentation. path: string @@ -131,24 +128,24 @@ class UIOFiles: # do recursive calls for sub-direcorties if subdirs: for subdir in subdirs: - self.listFiles(os.path.join(path, subdir), callid=1) + self.list_files(os.path.join(path, subdir), callid=1) return - def deleteFiles(self): + def delete_files(self): ''' @Description: Deletes the files. @Input: - self: instance of UIOFiles + self: instance of UioFiles Description see class documentation. @Return: <nothing> ''' - for f in self.files: - silentremove(f) + for old_file in self.files: + silent_remove(old_file) return diff --git a/python/Disagg.py b/python/disaggregation.py similarity index 93% rename from python/Disagg.py rename to python/disaggregation.py index 20aee50..9f285c1 100644 --- a/python/Disagg.py +++ b/python/disaggregation.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP +# ToDo AP # - check alist of size 4 ? # - write a test, IMPORTANT #************************************************************************ @@ -19,7 +19,7 @@ # - applied PEP8 style guide # - added structured documentation # - outsourced the disaggregation functions dapoly and darain -# to a new module named Disagg +# to a new module named disaggregation # # @License: # (C) Copyright 2015-2018. @@ -28,7 +28,7 @@ # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. # # @Module Description: -# Disaggregation of deaccumulated flux data from an ECMWF model FG field. +# disaggregationregation of deaccumulated flux data from an ECMWF model FG field. # Initially the flux data to be concerned are: # - large-scale precipitation # - convective precipitation @@ -69,7 +69,7 @@ def dapoly(alist): Interpolation of deaccumulated fluxes of an ECMWF model FG field using a cubic polynomial solution which conserves the integrals of the fluxes within each timespan. - Disaggregation is done for 4 accumluated timespans which generates + disaggregationregation is done for 4 accumluated timespans which generates a new, disaggregated value which is output at the central point of the 4 accumulation timespans. This new point is used for linear interpolation of the complete timeseries afterwards. @@ -110,7 +110,7 @@ def darain(alist): Interpolation of deaccumulated fluxes of an ECMWF model FG rainfall field using a modified linear solution which conserves the integrals of the fluxes within each timespan. - Disaggregation is done for 4 accumluated timespans which generates + disaggregationregation is done for 4 accumluated timespans which generates a new, disaggregated value which is output at the central point of the 4 accumulation timespans. This new point is used for linear interpolation of the complete timeseries afterwards. @@ -143,13 +143,3 @@ def darain(alist): nfield = xac + xbd return nfield - - - - - - - - - - diff --git a/python/getMARSdata.py b/python/get_mars_data.py similarity index 74% rename from python/getMARSdata.py rename to python/get_mars_data.py index 43872cb..14f6c03 100755 --- a/python/getMARSdata.py +++ b/python/get_mars_data.py @@ -1,9 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#************************************************************************ -# TODO AP -# - add function docstrings!!!! -#************************************************************************ #******************************************************************************* # @Author: Anne Fouilloux (University of Oslo) # @@ -12,10 +8,10 @@ # @Change History: # # November 2015 - Leopold Haimberger (University of Vienna): -# - moved the getEIdata program into a function "getMARSdata" +# - moved the getEIdata program into a function "get_mars_data" # - moved the AgurmentParser into a seperate function # - adatpted the function for the use in flex_extract -# - renamed file to getMARSdata +# - renamed file to get_mars_data # # February 2018 - Anne Philipp (University of Vienna): # - applied PEP8 style guide @@ -41,7 +37,7 @@ # # @Program Content: # - main -# - getMARSdata +# - get_mars_data # #******************************************************************************* @@ -53,23 +49,21 @@ import sys import datetime import inspect try: - ecapi=True + ecapi = True import ecmwfapi except ImportError: - ecapi=False + ecapi = False + +# software specific classes and modules from flex_extract +from tools import my_error, normal_exit, interpret_args_and_control +from EcFlexpart import EcFlexpart +from UioFiles import UioFiles # add path to pythonpath so that python finds its buddies -localpythonpath = os.path.dirname(os.path.abspath( +LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath( inspect.getfile(inspect.currentframe()))) -if localpythonpath not in sys.path: - sys.path.append(localpythonpath) - -# software specific classes and modules from flex_extract -from ControlFile import ControlFile -from Tools import myerror, normalexit, \ - interpret_args_and_control -from ECFlexpart import ECFlexpart -from UIOFiles import UIOFiles +if LOCAL_PYTHON_PATH not in sys.path: + sys.path.append(LOCAL_PYTHON_PATH) # ------------------------------------------------------------------------------ # FUNCTION @@ -77,9 +71,9 @@ from UIOFiles import UIOFiles def main(): ''' @Description: - If getMARSdata is called from command line, this function controls + If get_mars_data is called from command line, this function controls the program flow and calls the argumentparser function and - the getMARSdata function for retrieving EC data. + the get_mars_data function for retrieving EC data. @Input: <nothing> @@ -88,12 +82,12 @@ def main(): <nothing> ''' args, c = interpret_args_and_control() - getMARSdata(args, c) - normalexit(c) + get_mars_data(c) + normal_exit(c) return -def getMARSdata(args, c): +def get_mars_data(c): ''' @Description: Retrieves the EC data needed for a FLEXPART simulation. @@ -102,9 +96,6 @@ def getMARSdata(args, c): is set. @Input: - args: instance of ArgumentParser - Contains the commandline arguments from script/program call. - c: instance of class ControlFile Contains all the parameters of CONTROL file, which are e.g.: DAY1(start_date), DAY2(end_date), DTIME, MAXSTEP, TYPE, TIME, @@ -125,9 +116,9 @@ def getMARSdata(args, c): if not os.path.exists(c.inputdir): os.makedirs(c.inputdir) - print("Retrieving EC data!") - print("start date %s " % (c.start_date)) - print("end date %s " % (c.end_date)) + print "Retrieving EC data!" + print "start date %s " % (c.start_date) + print "end date %s " % (c.end_date) if ecapi: server = ecmwfapi.ECMWFService("mars") @@ -159,9 +150,9 @@ def getMARSdata(args, c): # -------------- flux data ------------------------------------------------ print 'removing old flux content of ' + c.inputdir - tobecleaned = UIOFiles('*_acc_*.' + str(os.getppid()) + '.*.grb') - tobecleaned.listFiles(c.inputdir) - tobecleaned.deleteFiles() + tobecleaned = UioFiles('*_acc_*.' + str(os.getppid()) + '.*.grb') + tobecleaned.list_files(c.inputdir) + tobecleaned.delete_files() # if forecast for maximum one day (upto 23h) are to be retrieved, # collect accumulation data (flux data) @@ -171,7 +162,7 @@ def getMARSdata(args, c): day = startm1 while day < endp1: # retrieve MARS data for the whole period - flexpart = ECFlexpart(c, fluxes=True) + flexpart = EcFlexpart(c, fluxes=True) tmpday = day + datechunk - datetime.timedelta(days=1) if tmpday < endp1: dates = day.strftime("%Y%m%d") + "/to/" + \ @@ -185,7 +176,7 @@ def getMARSdata(args, c): try: flexpart.retrieve(server, dates, c.inputdir) except IOError: - myerror(c, 'MARS request failed') + my_error(c, 'MARS request failed') day += datechunk @@ -198,7 +189,7 @@ def getMARSdata(args, c): day = start while day <= end: # retrieve MARS data for the whole period - flexpart = ECFlexpart(c, fluxes=True) + flexpart = EcFlexpart(c, fluxes=True) tmpday = day + datechunk - datetime.timedelta(days=1) if tmpday < end: dates = day.strftime("%Y%m%d") + "/to/" + \ @@ -212,39 +203,38 @@ def getMARSdata(args, c): try: flexpart.retrieve(server, dates, c.inputdir) except IOError: - myerror(c, 'MARS request failed') + my_error(c, 'MARS request failed') day += datechunk # -------------- non flux data -------------------------------------------- print 'removing old non flux content of ' + c.inputdir - tobecleaned = UIOFiles('*__*.' + str(os.getppid()) + '.*.grb') - tobecleaned.listFiles(c.inputdir) - tobecleaned.deleteFiles() + tobecleaned = UioFiles('*__*.' + str(os.getppid()) + '.*.grb') + tobecleaned.list_files(c.inputdir) + tobecleaned.delete_files() day = start while day <= end: - # retrieve all non flux MARS data for the whole period - flexpart = ECFlexpart(c, fluxes=False) - tmpday = day + datechunk - datetime.timedelta(days=1) - if tmpday < end: - dates = day.strftime("%Y%m%d") + "/to/" + \ - tmpday.strftime("%Y%m%d") - else: - dates = day.strftime("%Y%m%d") + "/to/" + \ - end.strftime("%Y%m%d") + # retrieve all non flux MARS data for the whole period + flexpart = EcFlexpart(c, fluxes=False) + tmpday = day + datechunk - datetime.timedelta(days=1) + if tmpday < end: + dates = day.strftime("%Y%m%d") + "/to/" + \ + tmpday.strftime("%Y%m%d") + else: + dates = day.strftime("%Y%m%d") + "/to/" + \ + end.strftime("%Y%m%d") - print "retrieve " + dates + " in dir " + c.inputdir + print "retrieve " + dates + " in dir " + c.inputdir - try: - flexpart.retrieve(server, dates, c.inputdir) - except IOError: - myerror(c, 'MARS request failed') + try: + flexpart.retrieve(server, dates, c.inputdir) + except IOError: + my_error(c, 'MARS request failed') - day += datechunk + day += datechunk return if __name__ == "__main__": main() - diff --git a/python/install.py b/python/install.py index 09cc622..835b506 100755 --- a/python/install.py +++ b/python/install.py @@ -1,8 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP -# - localpythonpath should not be set in module load section! +# ToDo AP # - create a class Installation and divide installation in 3 subdefs for # ecgate, local and cca seperatly # - Change History ist nicht angepasst ans File! Original geben lassen @@ -17,6 +16,7 @@ # February 2018 - Anne Philipp (University of Vienna): # - applied PEP8 style guide # - added documentation +# - moved install_args_and_control in here # # @License: # (C) Copyright 2015-2018. @@ -44,23 +44,22 @@ # ------------------------------------------------------------------------------ # MODULES # ------------------------------------------------------------------------------ -import datetime import os import sys import glob import subprocess import inspect -from argparse import ArgumentParser,ArgumentDefaultsHelpFormatter - -# add path to pythonpath so that python finds its buddies -localpythonpath = os.path.dirname(os.path.abspath( - inspect.getfile(inspect.currentframe()))) -if localpythonpath not in sys.path: - sys.path.append(localpythonpath) +from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter # software specific classes and modules from flex_extract from ControlFile import ControlFile +# add path to pythonpath so that python finds its buddies +LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath( + inspect.getfile(inspect.currentframe()))) +if LOCAL_PYTHON_PATH not in sys.path: + sys.path.append(LOCAL_PYTHON_PATH) + # ------------------------------------------------------------------------------ # FUNCTIONS # ------------------------------------------------------------------------------ @@ -77,14 +76,14 @@ def main(): <nothing> ''' - os.chdir(localpythonpath) + os.chdir(LOCAL_PYTHON_PATH) args, c = install_args_and_control() if args.install_target is not None: install_via_gateway(c, args.install_target) else: - print('Please specify installation target (local|ecgate|cca)') - print('use -h or --help for help') + print 'Please specify installation target (local|ecgate|cca)' + print 'use -h or --help for help' sys.exit() @@ -151,25 +150,25 @@ def install_args_and_control(): try: c = ControlFile(args.controlfile) - except: - print('Could not read CONTROL file "' + args.controlfile + '"') - print('Either it does not exist or its syntax is wrong.') - print('Try "' + sys.argv[0].split('/')[-1] + - ' -h" to print usage information') + except IOError: + print 'Could not read CONTROL file "' + args.controlfile + '"' + print 'Either it does not exist or its syntax is wrong.' + print 'Try "' + sys.argv[0].split('/')[-1] + \ + ' -h" to print usage information' exit(1) if args.install_target != 'local': - if (args.ecgid is None or args.ecuid is None or args.gateway is None - or args.destination is None): - print('Please enter your ECMWF user id and group id as well as \ + if args.ecgid is None or args.ecuid is None or args.gateway is None \ + or args.destination is None: + print 'Please enter your ECMWF user id and group id as well as \ the \nname of the local gateway and the ectrans \ - destination ') - print('with command line options --ecuid --ecgid \ - --gateway --destination') - print('Try "' + sys.argv[0].split('/')[-1] + - ' -h" to print usage information') - print('Please consult ecaccess documentation or ECMWF user support \ - for further details') + destination ' + print 'with command line options --ecuid --ecgid \ + --gateway --destination' + print 'Try "' + sys.argv[0].split('/')[-1] + \ + ' -h" to print usage information' + print 'Please consult ecaccess documentation or ECMWF user support \ + for further details' sys.exit(1) else: c.ecuid = args.ecuid @@ -177,10 +176,8 @@ def install_args_and_control(): c.gateway = args.gateway c.destination = args.destination - try: + if args.makefile: c.makefile = args.makefile - except: - pass if args.install_target == 'local': if args.flexpart_root_scripts is None: @@ -239,7 +236,7 @@ def install_via_gateway(c, target): data = 'export FLEXPART_ROOT_SCRIPTS=' + \ c.flexpart_root_scripts else: - data='export FLEXPART_ROOT_SCRIPTS=$HOME' + data = 'export FLEXPART_ROOT_SCRIPTS=$HOME' if target.lower() != 'local': if '--workdir' in data: data = '#SBATCH --workdir=/scratch/ms/' + c.ecgid + \ @@ -291,16 +288,14 @@ def install_via_gateway(c, target): fo.write('DESTINATION ' + c.destination + '\n') fo.close() - - if target.lower() == 'local': # compile CONVERT2 if c.flexpart_root_scripts is None or c.flexpart_root_scripts == '../': - print('Warning: FLEXPART_ROOT_SCRIPTS has not been specified') - print('Only CONVERT2 will be compiled in ' + ecd + '/../src') + print 'Warning: FLEXPART_ROOT_SCRIPTS has not been specified' + print 'Only CONVERT2 will be compiled in ' + ecd + '/../src' else: c.flexpart_root_scripts = os.path.expandvars(os.path.expanduser( - c.flexpart_root_scripts)) + c.flexpart_root_scripts)) if os.path.abspath(ecd) != os.path.abspath(c.flexpart_root_scripts): os.chdir('/') p = subprocess.check_call(['tar', '-cvf', @@ -310,7 +305,7 @@ def install_via_gateway(c, target): ecd + 'src']) try: os.makedirs(c.flexpart_root_scripts + '/ECMWFDATA7.1') - except: + finally: pass os.chdir(c.flexpart_root_scripts + '/ECMWFDATA7.1') p = subprocess.check_call(['tar', '-xvf', @@ -328,17 +323,18 @@ def install_via_gateway(c, target): if flist: p = subprocess.check_call(['rm'] + flist) try: - print(('Using makefile: ' + makefile)) + print 'Using makefile: ' + makefile p = subprocess.check_call(['make', '-f', makefile]) - p = subprocess.check_call(['ls', '-l','CONVERT2']) - except: - print('compile failed - please edit ' + makefile + - ' or try another Makefile in the src directory.') - print('most likely GRIB_API_INCLUDE_DIR, GRIB_API_LIB ' - 'and EMOSLIB must be adapted.') - print('Available Makefiles:') - print(glob.glob('Makefile*')) - + p = subprocess.check_call(['ls', '-l', 'CONVERT2']) + except subprocess.CalledProcessError as e: + print 'compile failed with the following error:' + print e.output + print 'please edit ' + makefile + \ + ' or try another Makefile in the src directory.' + print 'most likely GRIB_API_INCLUDE_DIR, GRIB_API_LIB \ + and EMOSLIB must be adapted.' + print 'Available Makefiles:' + print glob.glob('Makefile*') elif target.lower() == 'ecgate': os.chdir('/') p = subprocess.check_call(['tar', '-cvf', @@ -351,18 +347,24 @@ def install_via_gateway(c, target): ecd + '../ECMWFDATA7.1.tar', 'ecgate:/home/ms/' + c.ecgid + '/' + c.ecuid + '/ECMWFDATA7.1.tar']) - except: - print('ecaccess-file-put failed! Probably the eccert key has expired.') + except subprocess.CalledProcessError as e: + print 'ecaccess-file-put failed! \ + Probably the eccert key has expired.' + exit(1) + + try: + p = subprocess.check_call(['ecaccess-job-submit', + '-queueName', + target, + ecd + 'python/compilejob.ksh']) + print 'compilejob.ksh has been submitted to ecgate for \ + installation in ' + c.ec_flexpart_root_scripts + \ + '/ECMWFDATA7.1' + print 'You should get an email with subject flexcompile within \ + the next few minutes' + except subprocess.CalledProcessError as e: + print 'ecaccess-job-submit failed!' exit(1) - p = subprocess.check_call(['ecaccess-job-submit', - '-queueName', - target, - ecd + 'python/compilejob.ksh']) - print('compilejob.ksh has been submitted to ecgate for ' - 'installation in ' + c.ec_flexpart_root_scripts + - '/ECMWFDATA7.1') - print('You should get an email with subject flexcompile within ' - 'the next few minutes') elif target.lower() == 'cca': os.chdir('/') @@ -376,23 +378,27 @@ def install_via_gateway(c, target): ecd + '../ECMWFDATA7.1.tar', 'cca:/home/ms/' + c.ecgid + '/' + c.ecuid + '/ECMWFDATA7.1.tar']) - except: - print('ecaccess-file-put failed! ' - 'Probably the eccert key has expired.') + except subprocess.CalledProcessError as e: + print 'ecaccess-file-put failed! \ + Probably the eccert key has expired.' exit(1) - p=subprocess.check_call(['ecaccess-job-submit', - '-queueName', - target, - ecd + 'python/compilejob.ksh']) - print('compilejob.ksh has been submitted to cca for installation in ' + - c.ec_flexpart_root_scripts + '/ECMWFDATA7.1') - print('You should get an email with subject flexcompile ' - 'within the next few minutes') + try: + p = subprocess.check_call(['ecaccess-job-submit', + '-queueName', + target, + ecd + 'python/compilejob.ksh']) + print 'compilejob.ksh has been submitted to cca for installation in ' +\ + c.ec_flexpart_root_scripts + '/ECMWFDATA7.1' + print 'You should get an email with subject flexcompile \ + within the next few minutes' + except subprocess.CalledProcessError as e: + print 'ecaccess-job-submit failed!' + exit(1) else: - print('ERROR: unknown installation target ', target) - print('Valid targets: ecgate, cca, local') + print 'ERROR: unknown installation target ', target + print 'Valid targets: ecgate, cca, local' return diff --git a/python/plot_retrieved.py b/python/plot_retrieved.py index b5a086f..d924c35 100755 --- a/python/plot_retrieved.py +++ b/python/plot_retrieved.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP +# ToDo AP # - documentation der Funktionen # - docu der progam functionality # - apply pep8 @@ -18,7 +18,7 @@ # - added documentation # - created function main and moved the two function calls for # arguments and plotting into it -# - added function getBasics to extract the boundary conditions +# - added function get_basics to extract the boundary conditions # of the data fields from the first grib file it gets. # # @License: @@ -32,11 +32,11 @@ # # @Program Content: # - main -# - getBasics -# - plotRetrieved -# - plotTS -# - plotMap -# - getPlotArgs +# - get_basics +# - plot_retrieved +# - plot_timeseries +# - plot_map +# - get_plot_args # #******************************************************************************* @@ -48,48 +48,36 @@ import datetime import os import inspect import sys -import glob from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter - -from matplotlib.pylab import * -import matplotlib.patches as mpatches -from mpl_toolkits.basemap import Basemap, addcyclic -import matplotlib.colors as mcolors -from matplotlib.font_manager import FontProperties -from matplotlib.patches import Polygon -import matplotlib.cm as cmx -import matplotlib.colors as colors -#from rasotools.utils import stats - -font = {'family': 'monospace', 'size': 12} -matplotlib.rcParams['xtick.major.pad'] = '20' - -matplotlib.rc('font', **font) - -from eccodes import * +import matplotlib +import matplotlib.pyplot as plt +from mpl_toolkits.basemap import Basemap +from eccodes import codes_grib_new_from_file, codes_get, codes_release, \ + codes_get_values import numpy as np -# add path to pythonpath so that python finds its buddies -localpythonpath = os.path.dirname(os.path.abspath( - inspect.getfile(inspect.currentframe()))) -if localpythonpath not in sys.path: - sys.path.append(localpythonpath) - # software specific classes and modules from flex_extract -from Tools import silentremove from ControlFile import ControlFile -#from GribTools import GribTools -from UIOFiles import UIOFiles +from UioFiles import UioFiles + +# add path to pythonpath so that python finds its buddies +LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath( + inspect.getfile(inspect.currentframe()))) +if LOCAL_PYTHON_PATH not in sys.path: + sys.path.append(LOCAL_PYTHON_PATH) +font = {'family': 'monospace', 'size': 12} +matplotlib.rcParams['xtick.major.pad'] = '20' +matplotlib.rc('font', **font) # ------------------------------------------------------------------------------ # FUNCTION # ------------------------------------------------------------------------------ def main(): ''' @Description: - If plotRetrieved is called from command line, this function controls + If plot_retrieved is called from command line, this function controls the program flow and calls the argumentparser function and - the plotRetrieved function for plotting the retrieved GRIB data. + the plot_retrieved function for plotting the retrieved GRIB data. @Input: <nothing> @@ -97,12 +85,12 @@ def main(): @Return: <nothing> ''' - args, c = getPlotArgs() - plotRetrieved(args, c) + args, c = get_plot_args() + plot_retrieved(c) return -def getBasics(ifile, verb=False): +def get_basics(ifile, verb=False): """ @Description: An example grib file will be opened and basic information will @@ -112,6 +100,7 @@ def getBasics(ifile, verb=False): @Input: ifile: string Contains the full absolute path to the ECMWF grib file. + verb (opt): bool Is True if there should be extra output in verbose mode. Default value is False. @@ -126,7 +115,6 @@ def getBasics(ifile, verb=False): 'jDirectionIncrementInDegrees', 'iDirectionIncrementInDegrees' """ - from eccodes import * data = {} @@ -140,37 +128,38 @@ def getBasics(ifile, verb=False): gid = codes_grib_new_from_file(f) # information needed from grib message - keys = [ - 'Ni', + keys = ['Ni', 'Nj', 'latitudeOfFirstGridPointInDegrees', 'longitudeOfFirstGridPointInDegrees', 'latitudeOfLastGridPointInDegrees', 'longitudeOfLastGridPointInDegrees', 'jDirectionIncrementInDegrees', - 'iDirectionIncrementInDegrees' - ] + 'iDirectionIncrementInDegrees'] - if verb: print('\nInformations are: ') + if verb: + print '\nInformations are: ' for key in keys: # Get the value of the key in a grib message. - data[key] = codes_get(gid,key) - if verb: print "%s = %s" % (key,data[key]) - if verb: print '\n' + data[key] = codes_get(gid, key) + if verb: + print "%s = %s" % (key, data[key]) + if verb: + print '\n' # Free the memory for the message referred as gribid. codes_release(gid) return data -def getFilesPerDate(files, datelist): +def get_files_per_date(files, datelist): ''' @Description: The filenames contain dates which are used to select a list of files for a specific time period specified in datelist. @Input: - files: instance of UIOFiles + files: instance of UioFiles For description see class documentation. It contains the attribute "files" which is a list of pathes to filenames. @@ -184,24 +173,21 @@ def getFilesPerDate(files, datelist): ''' filelist = [] - for file in files: - fdate = file[-8:] - ddate = datetime.datetime.strptime(fdate, '%y%m%d%H') + for filename in files: + filedate = filename[-8:] + ddate = datetime.datetime.strptime(filedate, '%y%m%d%H') if ddate in datelist: - filelist.append(file) + filelist.append(filename) return filelist -def plotRetrieved(args, c): +def plot_retrieved(c): ''' @Description: Reads GRIB data from a specified time period, a list of levels and a specified list of parameter. @Input: - args: instance of ArgumentParser - Contains the commandline arguments from script/program call. - c: instance of class ControlFile Contains all necessary information of a CONTROL file. The parameters are: DAY1, DAY2, DTIME, MAXSTEP, TYPE, TIME, STEP, CLASS, STREAM, @@ -228,40 +214,16 @@ def plotRetrieved(args, c): print 'datelist: ', datelist - c.paramIds = asarray(c.paramIds, dtype='int') - c.levels = asarray(c.levels, dtype='int') - c.area = asarray(c.area) - - # index_keys = ["date", "time", "step"] - # indexfile = c.inputdir + "/date_time_stepRange.idx" - # silentremove(indexfile) - # grib = GribTools(inputfiles.files) - # # creates new index file - # iid = grib.index(index_keys=index_keys, index_file=indexfile) - - # # read values of index keys - # index_vals = [] - # for key in index_keys: - # index_vals.append(grib_index_get(iid, key)) - # print(index_vals[-1]) - # # index_vals looks for example like: - # # index_vals[0]: ('20171106', '20171107', '20171108') ; date - # # index_vals[1]: ('0', '1200', '1800', '600') ; time - # # index_vals[2]: ('0', '12', '3', '6', '9') ; stepRange - - + c.paramIds = np.asarray(c.paramIds, dtype='int') + c.levels = np.asarray(c.levels, dtype='int') + c.area = np.asarray(c.area) - - #index_keys = ["date", "time", "step", "paramId"] - #indexfile = c.inputdir + "/date_time_stepRange.idx" - #silentremove(indexfile) - - files = UIOFiles(c.prefix+'*') - files.listFiles(c.inputdir) - ifiles = getFilesPerDate(files.files, datelist) + files = UioFiles(c.prefix+'*') + files.list_files(c.inputdir) + ifiles = get_files_per_date(files.files, datelist) ifiles.sort() - gdict = getBasics(ifiles[0], verb=False) + gdict = get_basics(ifiles[0], verb=False) fdict = dict() fmeta = dict() @@ -273,14 +235,14 @@ def plotRetrieved(args, c): fmeta[key] = [] fstamp[key] = [] - for file in ifiles: - f = open(file) - print( "Opening file for reading data --- %s" % file) - fdate = datetime.datetime.strptime(file[-8:], "%y%m%d%H") + for filename in ifiles: + f = open(filename) + print "Opening file for reading data --- %s" % filename + fdate = datetime.datetime.strptime(filename[-8:], "%y%m%d%H") # Load in memory a grib message from a file. gid = codes_grib_new_from_file(f) - while(gid is not None): + while gid is not None: gtype = codes_get(gid, 'type') paramId = codes_get(gid, 'paramId') parameterName = codes_get(gid, 'parameterName') @@ -289,32 +251,32 @@ def plotRetrieved(args, c): if paramId in c.paramIds and level in c.levels: key = '{:0>3}_{:0>3}'.format(paramId, level) print 'key: ', key - if len(fstamp[key]) != 0 : + if fstamp[key]: for i in range(len(fstamp[key])): if fdate < fstamp[key][i]: fstamp[key].insert(i, fdate) fmeta[key].insert(i, [paramId, parameterName, gtype, - fdate, level]) - fdict[key].insert(i, flipud(reshape( - codes_get_values(gid), - [gdict['Nj'], gdict['Ni']]))) + fdate, level]) + fdict[key].insert(i, np.flipud(np.reshape( + codes_get_values(gid), + [gdict['Nj'], gdict['Ni']]))) break elif fdate > fstamp[key][i] and i == len(fstamp[key])-1: fstamp[key].append(fdate) fmeta[key].append([paramId, parameterName, gtype, - fdate, level]) - fdict[key].append(flipud(reshape( - codes_get_values(gid), - [gdict['Nj'], gdict['Ni']]))) + fdate, level]) + fdict[key].append(np.flipud(np.reshape( + codes_get_values(gid), + [gdict['Nj'], gdict['Ni']]))) break elif fdate > fstamp[key][i] and i != len(fstamp[key])-1 \ and fdate < fstamp[key][i+1]: fstamp[key].insert(i, fdate) fmeta[key].insert(i, [paramId, parameterName, gtype, - fdate, level]) - fdict[key].insert(i, flipud(reshape( - codes_get_values(gid), - [gdict['Nj'], gdict['Ni']]))) + fdate, level]) + fdict[key].insert(i, np.flipud(np.reshape( + codes_get_values(gid), + [gdict['Nj'], gdict['Ni']]))) break else: pass @@ -322,7 +284,7 @@ def plotRetrieved(args, c): fstamp[key].append(fdate) fmeta[key].append((paramId, parameterName, gtype, fdate, level)) - fdict[key].append(flipud(reshape( + fdict[key].append(np.flipud(np.reshape( codes_get_values(gid), [gdict['Nj'], gdict['Ni']]))) codes_release(gid) @@ -331,25 +293,21 @@ def plotRetrieved(args, c): gid = codes_grib_new_from_file(f) f.close() - #print 'fstamp: ', fstamp - #exit() - for k in fdict.keys(): - print 'fmeta: ', len(fmeta),fmeta + for k in fdict.iterkeys(): + print 'fmeta: ', len(fmeta), fmeta fml = fmeta[k] fdl = fdict[k] print 'fm1: ', len(fml), fml - #print 'fd1: ', fdl - #print zip(fdl, fml) for fd, fm in zip(fdl, fml): print fm ftitle = fm[1] + ' {} '.format(fm[-1]) + \ - datetime.datetime.strftime(fm[3], '%Y%m%d%H') #+ ' ' + stats(fd) + datetime.datetime.strftime(fm[3], '%Y%m%d%H') pname = '_'.join(fm[1].split()) + '_{}_'.format(fm[-1]) + \ datetime.datetime.strftime(fm[3], '%Y%m%d%H') - plotMap(c, fd, fm, gdict, ftitle, pname, 'png') + plot_map(c, fd, fm, gdict, ftitle, pname, 'png') - for k in fdict.keys(): + for k in fdict.iterkeys(): fml = fmeta[k] fdl = fdict[k] fsl = fstamp[k] @@ -357,25 +315,35 @@ def plotRetrieved(args, c): fm = fml[0] fd = fdl[0] ftitle = fm[1] + ' {} '.format(fm[-1]) + \ - datetime.datetime.strftime(fm[3], '%Y%m%d%H') #+ ' ' + stats(fd) + datetime.datetime.strftime(fm[3], '%Y%m%d%H') pname = '_'.join(fm[1].split()) + '_{}_'.format(fm[-1]) + \ datetime.datetime.strftime(fm[3], '%Y%m%d%H') - lat = -20 - lon = 20 - plotTS(c, fdl, fml, fsl, lat, lon, - gdict, ftitle, pname, 'png') + lat = -20. + lon = 20. + plot_timeseries(c, fdl, fml, fsl, lat, lon, gdict, + ftitle, pname, 'png') return -def plotTS(c, flist, fmetalist, ftimestamps, lat, lon, - gdict, ftitle, filename, fending, show=False): +def plot_timeseries(c, flist, fmetalist, ftimestamps, lat, lon, + gdict, ftitle, filename, fending, show=False): ''' @Description: + Creates a timeseries plot for a given lat/lon position. @Input: - c: + c: instance of class ControlFile + Contains all necessary information of a CONTROL file. The parameters + are: DAY1, DAY2, DTIME, MAXSTEP, TYPE, TIME, STEP, CLASS, STREAM, + NUMBER, EXPVER, GRID, LEFT, LOWER, UPPER, RIGHT, LEVEL, LEVELIST, + RESOL, GAUSS, ACCURACY, OMEGA, OMEGADIFF, ETA, ETADIFF, DPDETA, + SMOOTH, FORMAT, ADDPAR, WRF, CWC, PREFIX, ECSTORAGE, ECTRANS, + ECFSDIR, MAILOPS, MAILFAIL, GRIB2FLEXPART, DEBUG, INPUTDIR, + OUTPUTDIR, FLEXPART_ROOT_SCRIPTS + For more information about format and content of the parameter see + documentation. - flist: + flist: numpy array, 2d The actual data values to be plotted from the grib messages. fmetalist: list of strings @@ -385,11 +353,20 @@ def plotTS(c, flist, fmetalist, ftimestamps, lat, lon, ftimestamps: list of datetime Contains the time stamps. - lat: + lat: float + The latitude for which the timeseries should be plotted. - lon: + lon: float + The longitude for which the timeseries should be plotted. - gdict: + gdict: dict + Contains basic informations of the ECMWF grib files, e.g. + 'Ni', 'Nj', 'latitudeOfFirstGridPointInDegrees', + 'longitudeOfFirstGridPointInDegrees', + 'latitudeOfLastGridPointInDegrees', + 'longitudeOfLastGridPointInDegrees', + 'jDirectionIncrementInDegrees', + 'iDirectionIncrementInDegrees' ftitle: string The title of the timeseries. @@ -410,39 +387,36 @@ def plotTS(c, flist, fmetalist, ftimestamps, lat, lon, t1 = time.time() - llx = gdict['longitudeOfFirstGridPointInDegrees'] - if llx > 180. : - llx -= 360. - lly = gdict['latitudeOfLastGridPointInDegrees'] - dxout = gdict['iDirectionIncrementInDegrees'] - dyout = gdict['jDirectionIncrementInDegrees'] - urx = gdict['longitudeOfLastGridPointInDegrees'] - ury = gdict['latitudeOfFirstGridPointInDegrees'] - numxgrid = gdict['Ni'] - numygrid = gdict['Nj'] - - farr = asarray(flist) + #llx = gdict['longitudeOfFirstGridPointInDegrees'] + #if llx > 180. : + # llx -= 360. + #lly = gdict['latitudeOfLastGridPointInDegrees'] + #dxout = gdict['iDirectionIncrementInDegrees'] + #dyout = gdict['jDirectionIncrementInDegrees'] + #urx = gdict['longitudeOfLastGridPointInDegrees'] + #ury = gdict['latitudeOfFirstGridPointInDegrees'] + #numxgrid = gdict['Ni'] + #numygrid = gdict['Nj'] + + farr = np.asarray(flist) #(time, lat, lon) - lonindex = linspace(llx, urx, numxgrid) - latindex = linspace(lly, ury, numygrid) - #print len(lonindex), len(latindex), farr.shape - - #latindex = (lat + 90) * 180 / (gdict['Nj'] - 1) - #lonindex = (lon + 179) * 360 / gdict['Ni'] - #print latindex, lonindex + #lonindex = linspace(llx, urx, numxgrid) + #latindex = linspace(lly, ury, numygrid) - - ts = farr[:, 0, 0]#latindex[0], lonindex[0]] + ts = farr[:, 0, 0] fig = plt.figure(figsize=(12, 6.7)) plt.plot(ftimestamps, ts) plt.title(ftitle) - plt.savefig(c.outputdir+'/'+filename+'_TS.'+fending, facecolor=fig.get_facecolor(), edgecolor='none',format=fending) + plt.savefig(c.outputdir + '/' + filename + '_TS.' + fending, + facecolor=fig.get_facecolor(), + edgecolor='none', + format=fending) print 'created ', c.outputdir + '/' + filename - if show == True: + if show: plt.show() fig.clf() plt.close(fig) @@ -451,9 +425,10 @@ def plotTS(c, flist, fmetalist, ftimestamps, lat, lon, return -def plotMap(c, flist, fmetalist, gdict, ftitle, filename, fending, show=False): +def plot_map(c, flist, fmetalist, gdict, ftitle, filename, fending, show=False): ''' @Description: + Creates a basemap plot with imshow for a given data field. @Input: c: instance of class ControlFile @@ -467,9 +442,12 @@ def plotMap(c, flist, fmetalist, gdict, ftitle, filename, fending, show=False): For more information about format and content of the parameter see documentation. - flist + flist: numpy array, 2d + The actual data values to be plotted from the grib messages. - fmetalist + fmetalist: list of strings + Contains some meta date for the data field to be plotted: + parameter id, parameter Name, grid type, datetime, level gdict: dict Contains basic informations of the ECMWF grib files, e.g. @@ -503,60 +481,81 @@ def plotMap(c, flist, fmetalist, gdict, ftitle, filename, fending, show=False): #mbaxes = fig.add_axes([0.05, 0.15, 0.8, 0.7]) llx = gdict['longitudeOfFirstGridPointInDegrees'] #- 360 - if llx > 180. : + if llx > 180.: llx -= 360. lly = gdict['latitudeOfLastGridPointInDegrees'] - dxout = gdict['iDirectionIncrementInDegrees'] - dyout = gdict['jDirectionIncrementInDegrees'] + #dxout = gdict['iDirectionIncrementInDegrees'] + #dyout = gdict['jDirectionIncrementInDegrees'] urx = gdict['longitudeOfLastGridPointInDegrees'] ury = gdict['latitudeOfFirstGridPointInDegrees'] - numxgrid = gdict['Ni'] - numygrid = gdict['Nj'] + #numxgrid = gdict['Ni'] + #numygrid = gdict['Nj'] m = Basemap(projection='cyl', llcrnrlon=llx, llcrnrlat=lly, - urcrnrlon=urx, urcrnrlat=ury,resolution='i') + urcrnrlon=urx, urcrnrlat=ury, resolution='i') - lw = 0.5 + #lw = 0.5 m.drawmapboundary() - x = linspace(llx, urx, numxgrid) - y = linspace(lly, ury, numygrid) + #x = linspace(llx, urx, numxgrid) + #y = linspace(lly, ury, numygrid) - xx, yy = m(*meshgrid(x, y)) + #xx, yy = m(*meshgrid(x, y)) #s = m.contourf(xx, yy, flist) s = plt.imshow(flist.T, - extent=(llx, urx, lly, ury), - alpha=1.0, - interpolation='nearest' - #vmin=vn, - #vmax=vx, - #cmap=my_cmap, - #levels=levels, - #cmap=my_cmap, - #norm=LogNorm(vn,vx) - ) - - title(ftitle, y=1.08) + extent=(llx, urx, lly, ury), + alpha=1.0, + interpolation='nearest' + #vmin=vn, + #vmax=vx, + #cmap=my_cmap, + #levels=levels, + #cmap=my_cmap, + #norm=LogNorm(vn,vx) + ) + + plt.title(ftitle, y=1.08) cb = m.colorbar(s, location="right", pad="10%") - #cb.set_label('Contribution per cell (ng m$^{-3}$)',size=14) - - thickline = np.arange(lly,ury+1,10.) - thinline = np.arange(lly,ury+1,5.) - m.drawparallels(thickline,color='gray',dashes=[1,1],linewidth=0.5,labels=[1,1,1,1], xoffset=1.) # draw parallels - m.drawparallels(np.setdiff1d(thinline,thickline),color='lightgray',dashes=[1,1],linewidth=0.5,labels=[0,0,0,0]) # draw parallels - - thickline = np.arange(llx,urx+1,10.) - thinline = np.arange(llx,urx+1,5.) - m.drawmeridians(thickline,color='gray',dashes=[1,1],linewidth=0.5,labels=[1,1,1,1],yoffset=1.) # draw meridians - m.drawmeridians(np.setdiff1d(thinline,thickline),color='lightgray',dashes=[1,1],linewidth=0.5,labels=[0,0,0,0]) # draw meridians + cb.set_label('label', size=14) + + thickline = np.arange(lly, ury+1, 10.) + thinline = np.arange(lly, ury+1, 5.) + m.drawparallels(thickline, + color='gray', + dashes=[1, 1], + linewidth=0.5, + labels=[1, 1, 1, 1], + xoffset=1.) + m.drawparallels(np.setdiff1d(thinline, thickline), + color='lightgray', + dashes=[1, 1], + linewidth=0.5, + labels=[0, 0, 0, 0]) + + thickline = np.arange(llx, urx+1, 10.) + thinline = np.arange(llx, urx+1, 5.) + m.drawmeridians(thickline, + color='gray', + dashes=[1, 1], + linewidth=0.5, + labels=[1, 1, 1, 1], + yoffset=1.) + m.drawmeridians(np.setdiff1d(thinline, thickline), + color='lightgray', + dashes=[1, 1], + linewidth=0.5, + labels=[0, 0, 0, 0]) m.drawcoastlines() m.drawcountries() - plt.savefig(c.outputdir+'/'+filename+'_MAP.'+fending, facecolor=fig.get_facecolor(), edgecolor='none',format=fending) + plt.savefig(c.outputdir + '/' + filename + '_MAP.' + fending, + facecolor=fig.get_facecolor(), + edgecolor='none', + format=fending) print 'created ', c.outputdir + '/' + filename - if show == True: + if show: plt.show() fig.clf() plt.close(fig) @@ -565,7 +564,7 @@ def plotMap(c, flist, fmetalist, gdict, ftitle, filename, fending, show=False): return -def getPlotArgs(): +def get_plot_args(): ''' @Description: Assigns the command line arguments and reads CONTROL file @@ -596,13 +595,13 @@ def getPlotArgs(): # the most important arguments parser.add_argument("--start_date", dest="start_date", help="start date YYYYMMDD") - parser.add_argument( "--end_date", dest="end_date", - help="end_date YYYYMMDD") + parser.add_argument("--end_date", dest="end_date", + help="end_date YYYYMMDD") parser.add_argument("--start_step", dest="start_step", help="start step in hours") - parser.add_argument( "--end_step", dest="end_step", - help="end step in hours") + parser.add_argument("--end_step", dest="end_step", + help="end step in hours") # some arguments that override the default in the CONTROL file parser.add_argument("--levelist", dest="levelist", @@ -634,9 +633,8 @@ def getPlotArgs(): c = ControlFile(args.controlfile) except IOError: try: - c = ControlFile(localpythonpath + args.controlfile) - - except: + c = ControlFile(LOCAL_PYTHON_PATH + args.controlfile) + except IOError: print 'Could not read CONTROL file "' + args.controlfile + '"' print 'Either it does not exist or its syntax is wrong.' print 'Try "' + sys.argv[0].split('/')[-1] + \ @@ -681,4 +679,3 @@ def getPlotArgs(): if __name__ == "__main__": main() - diff --git a/python/prepareFLEXPART.py b/python/prepare_flexpart.py similarity index 79% rename from python/prepareFLEXPART.py rename to python/prepare_flexpart.py index b1e0b86..2e5a160 100755 --- a/python/prepareFLEXPART.py +++ b/python/prepare_flexpart.py @@ -1,9 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP -# - wieso cleanup in main wenn es in prepareflexpart bereits abgefragt wurde? -# doppelt gemoppelt? +# ToDo AP # - wieso start=startm1 wenn basetime = 0 ? wenn die fluxes nicht mehr # relevant sind? verstehe ich nicht #************************************************************************ @@ -28,11 +26,11 @@ # - applied PEP8 style guide # - added documentation # - minor changes in programming style for consistence -# - BUG: removed call of cleanup-Function after call of +# - BUG: removed call of clean_up-Function after call of # prepareFlexpart in main since it is already called in # prepareFlexpart at the end! # - created function main and moved the two function calls for -# arguments and prepareFLEXPART into it +# arguments and prepare_flexpart into it # # @License: # (C) Copyright 2014-2018. @@ -43,7 +41,7 @@ # @Program Functionality: # This program prepares the final version of the grib files which are # then used by FLEXPART. It converts the bunch of grib files extracted -# via getMARSdata by doing for example the necessary conversion to get +# via get_mars_data by doing for example the necessary conversion to get # consistent grids or the disaggregation of flux data. Finally, the # program combines the data fields in files per available hour with the # naming convention xxYYMMDDHH, where xx should be 2 arbitrary letters @@ -51,24 +49,25 @@ # # @Program Content: # - main -# - prepareFLEXPART +# - prepare_flexpart # #******************************************************************************* # ------------------------------------------------------------------------------ # MODULES # ------------------------------------------------------------------------------ -import shutil import datetime -#import time import os import inspect import sys import socket -from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter -hostname = socket.gethostname() -ecapi = 'ecmwf' not in hostname +# software specific classes and modules from flex_extract +from UioFiles import UioFiles +from tools import interpret_args_and_control, clean_up +from EcFlexpart import EcFlexpart + +ecapi = 'ecmwf' not in socket.gethostname() try: if ecapi: import ecmwfapi @@ -76,24 +75,20 @@ except ImportError: ecapi = False # add path to pythonpath so that python finds its buddies -localpythonpath = os.path.dirname(os.path.abspath( +LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath( inspect.getfile(inspect.currentframe()))) -if localpythonpath not in sys.path: - sys.path.append(localpythonpath) +if LOCAL_PYTHON_PATH not in sys.path: + sys.path.append(LOCAL_PYTHON_PATH) -# software specific classes and modules from flex_extract -from UIOFiles import UIOFiles -from Tools import interpret_args_and_control, cleanup -from ECFlexpart import ECFlexpart # ------------------------------------------------------------------------------ # FUNCTION # ------------------------------------------------------------------------------ def main(): ''' @Description: - If prepareFLEXPART is called from command line, this function controls + If prepare_flexpart is called from command line, this function controls the program flow and calls the argumentparser function and - the prepareFLEXPART function for preparation of GRIB data for FLEXPART. + the prepare_flexpart function for preparation of GRIB data for FLEXPART. @Input: <nothing> @@ -102,14 +97,14 @@ def main(): <nothing> ''' args, c = interpret_args_and_control() - prepareFLEXPART(args, c) + prepare_flexpart(args, c) return -def prepareFLEXPART(args, c): +def prepare_flexpart(args, c): ''' @Description: - Lists all grib files retrieved from MARS with getMARSdata and + Lists all grib files retrieved from MARS with get_mars_data and uses prepares data for the use in FLEXPART. Specific data fields are converted to a different grid and the flux data are going to be disaggregated. The data fields are collected by hour and stored in @@ -156,27 +151,27 @@ def prepareFLEXPART(args, c): # one day ahead of the start date and # one day after the end date is needed startm1 = start - datetime.timedelta(days=1) - endp1 = end + datetime.timedelta(days=1) +# endp1 = end + datetime.timedelta(days=1) # get all files with flux data to be deaccumulated - inputfiles = UIOFiles('*OG_acc_SL*.' + c.ppid + '.*') - inputfiles.listFiles(c.inputdir) + inputfiles = UioFiles('*OG_acc_SL*.' + c.ppid + '.*') + inputfiles.list_files(c.inputdir) # create output dir if necessary if not os.path.exists(c.outputdir): os.makedirs(c.outputdir) # deaccumulate the flux data - flexpart = ECFlexpart(c, fluxes=True) + flexpart = EcFlexpart(c, fluxes=True) flexpart.write_namelist(c, 'fort.4') flexpart.deacc_fluxes(inputfiles, c) - print('Prepare ' + start.strftime("%Y%m%d") + - "/to/" + end.strftime("%Y%m%d")) + print 'Prepare ' + start.strftime("%Y%m%d") + \ + "/to/" + end.strftime("%Y%m%d") # get a list of all files from the root inputdir - inputfiles = UIOFiles('????__??.*' + c.ppid + '.*') - inputfiles.listFiles(c.inputdir) + inputfiles = UioFiles('????__??.*' + c.ppid + '.*') + inputfiles.list_files(c.inputdir) # produce FLEXPART-ready GRIB files and # process GRIB files - @@ -184,16 +179,16 @@ def prepareFLEXPART(args, c): if c.basetime == '00': start = startm1 - flexpart = ECFlexpart(c, fluxes=False) + flexpart = EcFlexpart(c, fluxes=False) flexpart.create(inputfiles, c) flexpart.process_output(c) # check if in debugging mode, then store all files # otherwise delete temporary files if int(c.debug) != 0: - print('Temporary files left intact') + print 'Temporary files left intact' else: - cleanup(c) + clean_up(c) return diff --git a/python/profiling.py b/python/profiling.py index b20e6e6..526c17f 100644 --- a/python/profiling.py +++ b/python/profiling.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP +# ToDo AP # - check of license of book content #************************************************************************ #******************************************************************************* @@ -65,7 +65,7 @@ def timefn(fn): t1 = time.time() result = fn(*args, **kwargs) t2 = time.time() - print("@timefn:" + fn.func_name + " took " + str(t2 - t1) + " seconds") + print "@timefn:" + fn.func_name + " took " + str(t2 - t1) + " seconds" return result diff --git a/python/submit.py b/python/submit.py index 0685103..fcf5735 100755 --- a/python/submit.py +++ b/python/submit.py @@ -1,15 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#************************************************************************ -# TODO AP -# -# - Change History ist nicht angepasst ans File! Original geben lassen -# - dead code ? what to do? -# - seperate operational and reanlysis for clarification -# - divide in two submits , ondemand und operational -# - -#************************************************************************ - #******************************************************************************* # @Author: Anne Fouilloux (University of Oslo) # @@ -36,7 +26,7 @@ # This program is the main program of flex_extract and controls the # program flow. # If it is supposed to work locally then it works through the necessary -# functions getMARSdata and prepareFlexpart. Otherwise it prepares +# functions get_mars_data and prepareFlexpart. Otherwise it prepares # a shell job script which will do the necessary work on the # ECMWF server and is submitted via ecaccess-job-submit. # @@ -51,19 +41,19 @@ # ------------------------------------------------------------------------------ import os import sys -import glob import subprocess import inspect -# add path to pythonpath so that python finds its buddies -localpythonpath = os.path.dirname(os.path.abspath( - inspect.getfile(inspect.currentframe()))) -if localpythonpath not in sys.path: - sys.path.append(localpythonpath) # software specific classes and modules from flex_extract -from Tools import interpret_args_and_control, normalexit -from getMARSdata import getMARSdata -from prepareFLEXPART import prepareFLEXPART +from tools import interpret_args_and_control, normal_exit +from get_mars_data import get_mars_data +from prepare_flexpart import prepare_flexpart + +# add path to pythonpath so that python finds its buddies +LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath( + inspect.getfile(inspect.currentframe()))) +if LOCAL_PYTHON_PATH not in sys.path: + sys.path.append(LOCAL_PYTHON_PATH) # ------------------------------------------------------------------------------ # FUNCTIONS @@ -83,17 +73,19 @@ def main(): @Return: <nothing> ''' - calledfromdir = os.getcwd() + + called_from_dir = os.getcwd() args, c = interpret_args_and_control() + # on local side if args.queue is None: if c.inputdir[0] != '/': - c.inputdir = os.path.join(calledfromdir, c.inputdir) + c.inputdir = os.path.join(called_from_dir, c.inputdir) if c.outputdir[0] != '/': - c.outputdir = os.path.join(calledfromdir, c.outputdir) - getMARSdata(args, c) - prepareFLEXPART(args, c) - normalexit(c) + c.outputdir = os.path.join(called_from_dir, c.outputdir) + get_mars_data(args, c) + prepare_flexpart(args, c) + normal_exit(c) # on ECMWF server else: submit(args.job_template, c, args.queue) @@ -138,11 +130,10 @@ def submit(jtemplate, c, queue): insert_point = lftext.index('EOF') # put all parameters of ControlFile instance into a list - clist = c.tolist() + clist = c.to_list() # ondemand colist = [] # operational mt = 0 -#AP wieso 2 for loops? for elem in clist: if 'maxstep' in elem: mt = int(elem.split(' ')[1]) @@ -151,8 +142,7 @@ def submit(jtemplate, c, queue): if 'start_date' in elem: elem = 'start_date ' + '${MSJ_YEAR}${MSJ_MONTH}${MSJ_DAY}' if 'end_date' in elem: -#AP Fehler?! Muss end_date heissen - elem = 'start_date ' + '${MSJ_YEAR}${MSJ_MONTH}${MSJ_DAY}' + elem = 'end_date ' + '${MSJ_YEAR}${MSJ_MONTH}${MSJ_DAY}' if 'base_time' in elem: elem = 'base_time ' + '${MSJ_BASETIME}' if 'time' in elem and mt > 24: @@ -172,11 +162,13 @@ def submit(jtemplate, c, queue): try: p = subprocess.check_call(['ecaccess-job-submit', '-queueName', queue, 'job.ksh']) - except: - print('ecaccess-job-submit failed, probably eccert has expired') + except subprocess.CalledProcessError as e: + print 'ecaccess-job-submit failed!' + print 'Error Message: ' + print e.output exit(1) - print('You should get an email with subject flex.hostname.pid') + print 'You should get an email with subject flex.hostname.pid' return diff --git a/python/testsuite.py b/python/test_suite.py similarity index 86% rename from python/testsuite.py rename to python/test_suite.py index 199fa89..c20b5f8 100755 --- a/python/testsuite.py +++ b/python/test_suite.py @@ -1,11 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP -# +# ToDo AP # - provide more tests # - provide more documentation -# - #************************************************************************ #******************************************************************************* @@ -27,7 +25,7 @@ # # @Program Functionality: # This script triggers the ECMWFDATA test suite. Call with -# testsuite.py [test group] +# test_suite.py [test group] # # @Program Content: # @@ -45,9 +43,9 @@ import subprocess # PROGRAM # ------------------------------------------------------------------------------ try: - taskfile = open('testsuite.json') -except: - print 'could not open suite definition file testsuite.json' + taskfile = open('test_suite.json') +except IOError: + print 'could not open suite definition file test_suite.json' exit() if not os.path.isfile('../src/CONVERT2'): @@ -72,14 +70,14 @@ jobfailed = 0 for g in groups: try: tk, tv = g, tasks[g] - except: - continue + finally: + pass garglist = [] for ttk, ttv in tv.iteritems(): if isinstance(ttv, basestring): if ttk != 'script': garglist.append('--' + ttk) - if '$' == ttv[0]: + if ttv[0] == '$': garglist.append(os.path.expandvars(ttv)) else: garglist.append(ttv) @@ -88,11 +86,11 @@ for g in groups: arglist = [] for tttk, tttv in ttv.iteritems(): if isinstance(tttv, basestring): - arglist.append('--' + tttk) - if '$' in tttv[0]: - arglist.append(os.path.expandvars(tttv)) - else: - arglist.append(tttv) + arglist.append('--' + tttk) + if '$' in tttv[0]: + arglist.append(os.path.expandvars(tttv)) + else: + arglist.append(tttv) print 'Command: ', ' '.join([tv['script']] + garglist + arglist) o = '../test/' + tk + '_' + ttk + '_' + '_'.join(ttv.keys()) print 'Output will be sent to ', o @@ -100,7 +98,7 @@ for g in groups: try: p = subprocess.check_call([tv['script']] + garglist + arglist, stdout=f, stderr=f) - except: + except subprocess.CalledProcessError as e: f.write('\nFAILED\n') print 'FAILED' jobfailed += 1 @@ -110,5 +108,3 @@ for g in groups: print 'Test suite tasks completed' print str(jobcounter-jobfailed) + ' successful, ' + str(jobfailed) + ' failed' print 'If tasks have been submitted via ECACCESS please check emails' - -#print tasks diff --git a/python/Tools.py b/python/tools.py similarity index 78% rename from python/Tools.py rename to python/tools.py index 24e9e02..b74fd68 100644 --- a/python/Tools.py +++ b/python/tools.py @@ -1,10 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- #************************************************************************ -# TODO AP -# - check myerror -# - check normalexit -# - check getListAsString +# ToDo AP +# - check my_error +# - check normal_exit +# - check get_list_as_string # - seperate args and control interpretation #************************************************************************ #******************************************************************************* @@ -14,17 +14,17 @@ # # @Change History: # October 2014 - Anne Fouilloux (University of Oslo) -# - created functions silentremove and product (taken from ECMWF) +# - created functions silent_remove and product (taken from ECMWF) # # November 2015 - Leopold Haimberger (University of Vienna) -# - created functions: interpret_args_and_control, cleanup -# myerror, normalexit, init128, toparamId +# - created functions: interpret_args_and_control, clean_up +# my_error, normal_exit, init128, to_param_id # # April 2018 - Anne Philipp (University of Vienna): # - applied PEP8 style guide # - added documentation -# - moved all functions from file FlexpartTools to this file Tools -# - added function getListAsString +# - moved all functions from file Flexparttools to this file tools +# - added function get_list_as_string # # @License: # (C) Copyright 2014-2018. @@ -38,14 +38,14 @@ # # @Module Content: # - interpret_args_and_control -# - cleanup -# - myerror -# - normalexit +# - clean_up +# - my_error +# - normal_exit # - product -# - silentremove +# - silent_remove # - init128 -# - toparamId -# - getListAsString +# - to_param_id +# - get_list_as_string # #******************************************************************************* @@ -56,10 +56,11 @@ import os import errno import sys import glob +import inspect +import subprocess import traceback -from numpy import * -from gribapi import * from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter +import numpy as np # software specific class from flex_extract from ControlFile import ControlFile @@ -122,13 +123,13 @@ def interpret_args_and_control(): help="root directory for storing output files") parser.add_argument("--flexpart_root_scripts", dest="flexpart_root_scripts", help="FLEXPART root directory (to find grib2flexpart \ - and COMMAND file)\n\ Normally ECMWFDATA resides in \ + and COMMAND file)\n Normally ECMWFDATA resides in \ the scripts directory of the FLEXPART distribution") - # this is only used by prepareFLEXPART.py to rerun a postprocessing step + # this is only used by prepare_flexpart.py to rerun a postprocessing step parser.add_argument("--ppid", dest="ppid", help="Specify parent process id for \ - rerun of prepareFLEXPART") + rerun of prepare_flexpart") # arguments for job submission to ECMWF, only needed by submit.py parser.add_argument("--job_template", dest='job_template', @@ -151,20 +152,22 @@ def interpret_args_and_control(): c = ControlFile(args.controlfile) except IOError: try: - c = ControlFile(localpythonpath + args.controlfile) - except: - print('Could not read CONTROL file "' + args.controlfile + '"') - print('Either it does not exist or its syntax is wrong.') - print('Try "' + sys.argv[0].split('/')[-1] + - ' -h" to print usage information') + LOCAL_PYTHON_PATH = os.path.dirname(os.path.abspath( + inspect.getfile(inspect.currentframe()))) + c = ControlFile(LOCAL_PYTHON_PATH + args.controlfile) + except IOError: + print 'Could not read CONTROL file "' + args.controlfile + '"' + print 'Either it does not exist or its syntax is wrong.' + print 'Try "' + sys.argv[0].split('/')[-1] + \ + ' -h" to print usage information' exit(1) # check for having at least a starting date if args.start_date is None and getattr(c, 'start_date') is None: - print('start_date specified neither in command line nor \ - in CONTROL file ' + args.controlfile) - print('Try "' + sys.argv[0].split('/')[-1] + - ' -h" to print usage information') + print 'start_date specified neither in command line nor \ + in CONTROL file ' + args.controlfile + print 'Try "' + sys.argv[0].split('/')[-1] + \ + ' -h" to print usage information' exit(1) # save all existing command line parameter to the ControlFile instance @@ -205,8 +208,8 @@ def interpret_args_and_control(): afloat = '.' in args.area l = args.area.split('/') if afloat: - for i in range(len(l)): - l[i] = str(int(float(l[i]) * 1000)) + for i, item in enumerate(l): + item = str(int(float(item) * 1000)) c.upper, c.left, c.lower, c.right = l # NOTE: basetime activates the ''operational mode'' @@ -217,11 +220,11 @@ def interpret_args_and_control(): l = args.step.split('/') if 'to' in args.step.lower(): if 'by' in args.step.lower(): - ilist = arange(int(l[0]), int(l[2]) + 1, int(l[4])) + ilist = np.arange(int(l[0]), int(l[2]) + 1, int(l[4])) c.step = ['{:0>3}'.format(i) for i in ilist] else: - myerror(None, args.step + ':\n' + - 'please use "by" as well if "to" is used') + my_error(None, args.step + ':\n' + + 'please use "by" as well if "to" is used') else: c.step = l @@ -238,7 +241,7 @@ def interpret_args_and_control(): return args, c -def cleanup(c): +def clean_up(c): ''' @Description: Remove all files from intermediate directory @@ -262,21 +265,21 @@ def cleanup(c): <nothing> ''' - print("cleanup") + print "clean_up" cleanlist = glob.glob(c.inputdir + "/*") for cl in cleanlist: if c.prefix not in cl: - silentremove(cl) + silent_remove(cl) if c.ecapi is False and (c.ectrans == '1' or c.ecstorage == '1'): - silentremove(cl) + silent_remove(cl) - print("Done") + print "Done" return -def myerror(c, message='ERROR'): +def my_error(c, message='ERROR'): ''' @Description: Prints a specified error message which can be passed to the function @@ -302,32 +305,34 @@ def myerror(c, message='ERROR'): @Return: <nothing> ''' - # uncomment if user wants email notification directly from python - #try: - #target = c.mailfail - #except AttributeError: - #target = os.getenv('USER') - - #if(type(target) is not list): - #target = [target] - - print(message) - - # uncomment if user wants email notification directly from python - #for t in target: - #p = subprocess.Popen(['mail','-s ECMWFDATA v7.0 ERROR', os.path.expandvars(t)], - # stdin = subprocess.PIPE, stdout = subprocess.PIPE, - # stderr = subprocess.PIPE, bufsize = 1) - #tr = '\n'.join(traceback.format_stack()) - #pout = p.communicate(input = message+'\n\n'+tr)[0] - #print 'Email sent to '+os.path.expandvars(t) # +' '+pout.decode() + + print message + + # comment if user does not want email notification directly from python + try: + target = [] + target.extend(c.mailfail) + except AttributeError: + target = [] + target.extend(os.getenv('USER')) + + for t in target: + p = subprocess.Popen(['mail', '-s ECMWFDATA v7.0 ERROR', + os.path.expandvars(t)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1) + tr = '\n'.join(traceback.format_stack()) + pout = p.communicate(input=message + '\n\n' + tr)[0] + print 'Email sent to ' + os.path.expandvars(t) + ' ' + pout.decode() exit(1) return -def normalexit(c, message='Done!'): +def normal_exit(c, message='Done!'): ''' @Description: Prints a specific exit message which can be passed to the function. @@ -353,23 +358,23 @@ def normalexit(c, message='Done!'): <nothing> ''' - # Uncomment if user wants notification directly from python - #try: - #target = c.mailops - #if(type(target) is not list): - #target = [target] - #for t in target: - #p = subprocess.Popen(['mail','-s ECMWFDATA v7.0 normal exit', - # os.path.expandvars(t)], - # stdin = subprocess.PIPE, - # stdout = subprocess.PIPE, - # stderr = subprocess.PIPE, bufsize = 1) - #pout = p.communicate(input = message+'\n\n')[0] - #print pout.decode() - #except: - #pass - - print(message) + print message + + # comment if user does not want notification directly from python + try: + target = [] + target.extend(c.mailops) + for t in target: + p = subprocess.Popen(['mail', '-s ECMWFDATA v7.0 normal exit', + os.path.expandvars(t)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1) + pout = p.communicate(input=message+'\n\n')[0] + print 'Email sent to ' + os.path.expandvars(t) + ' ' + pout.decode() + finally: + pass return @@ -410,7 +415,7 @@ def product(*args, **kwds): return -def silentremove(filename): +def silent_remove(filename): ''' @Description: If "filename" exists , it is removed. @@ -434,13 +439,13 @@ def silentremove(filename): return -def init128(fn): +def init128(filepath): ''' @Description: Opens and reads the grib file with table 128 information. @Input: - fn: string + filepath: string Path to file of ECMWF grib table number 128. @Return: @@ -450,7 +455,7 @@ def init128(fn): short name of the parameter. ''' table128 = dict() - with open(fn) as f: + with open(filepath) as f: fdata = f.read().split('\n') for data in fdata: if data[0] != '!': @@ -459,7 +464,7 @@ def init128(fn): return table128 -def toparamId(pars, table): +def to_param_id(pars, table): ''' @Description: Transform parameter names to parameter ids @@ -485,31 +490,33 @@ def toparamId(pars, table): cpar = pars.upper().split('/') ipar = [] for par in cpar: - found = False for k, v in table.iteritems(): if par == k or par == v: ipar.append(int(k)) - found = True break - if found is False: - print('Warning: par ' + par + ' not found in table 128') + else: + print 'Warning: par ' + par + ' not found in table 128' return ipar -def getListAsString(listObj): +def get_list_as_string(list_obj, concatenate_sign=', '): ''' @Description: Converts a list of arbitrary content into a single string. @Input: - listObj: list + list_obj: list A list with arbitrary content. + concatenate_sign: string, optional + A string which is used to concatenate the single + list elements. Default value is ", ". + @Return: - strOfList: string + str_of_list: string The content of the list as a single string. ''' - strOfList = ", ".join( str(l) for l in listObj) + str_of_list = concatenate_sign.join(str(l) for l in list_obj) - return strOfList + return str_of_list -- GitLab