readmet 0.7.3__tar.gz → 0.8.5.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,23 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: readmet
3
- Version: 0.7.3
4
- Summary: Python library for reading and writing rare formats used in Meteorology.
3
+ Version: 0.8.5.dev4
4
+ Summary: Python library for reading and writing less popular formats used in meteorology.
5
5
  Home-page:
6
6
  Author: Clemens Drüe
7
7
  Author-email: druee@uni-trier.de
8
8
  License: EUPL-1.2
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
+ Requires-Dist: numpy
12
+ Requires-Dist: pandas
13
+ Dynamic: author
14
+ Dynamic: author-email
15
+ Dynamic: description
16
+ Dynamic: description-content-type
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: requires-dist
20
+ Dynamic: summary
11
21
 
12
22
  readmet
13
23
  =======
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ __title__ = 'readmet'
4
+ __description__ = ('Python library for reading and writing '
5
+ 'less popular formats used in meteorology.')
6
+ __url__ = ''
7
+ __version__ = '0.8.5_dev4'
8
+ __author__ = u'Clemens Drüe'
9
+ __author_email__ = 'druee@uni-trier.de'
10
+ __license__ = 'EUPL-1.2'
11
+ __copyright__ = '(C) 2019-2025 Clemens Drüe'
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- '''
3
+ """
4
4
  The classes and functions in this category handle files
5
5
  in format "akterm" by German weather service (DWD)
6
6
 
@@ -45,12 +45,13 @@ The original columns in dat file specification are:
45
45
  | QPP | Qualitätsbyte Niederschlag | 0,9 |
46
46
  +-------+--------------------------------------+-------------+
47
47
 
48
- '''
48
+ """
49
49
 
50
50
  import logging
51
51
  import numpy as np
52
52
  import pandas as pd
53
53
 
54
+ logger = logging.getLogger(__name__)
54
55
  #
55
56
  #
56
57
  _AKT_COLUMNS = ['KENN', 'STA', 'JAHR', 'MON', 'TAG', 'STUN', 'NULL',
@@ -63,10 +64,83 @@ z0_classes = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1., 1.5, 2]
63
64
  #
64
65
  # ------------------------------------------------------------------------
65
66
  #
67
+ def precipitation_to_synop(precip):
68
+ """
69
+ generate SYNOP code numbers for precipitation amount
66
70
 
71
+ Parameters
72
+ ----------
73
+ precip : pandas.Series or array-like
74
+ precipitation amount in mm / observation period
75
+
76
+ Returns
77
+ -------
78
+ s : pandas.Series
79
+ SYNOP key PP (precipitation amount):
80
+ 000 no precipitation
81
+ 001 1 mm
82
+ 002 2 mm
83
+ ... ...
84
+ 988 988 mm
85
+ 989 989 mm or more
86
+ 990 0.05 mm or less
87
+ 991 0.1 mm
88
+ 992 0.2 mm
89
+ ... ...
90
+ 999 0.9 mm
91
+ """
92
+ p = pd.Series(precip).round(1)
93
+ s = pd.Series(np.nan, index=p.index)
94
+ for k,v in p.items():
95
+ if np.isnan(v) or v < 0.:
96
+ i = np.nan
97
+ elif v == 0.:
98
+ i = 0
99
+ elif v < 1.:
100
+ i = 990 + int(10 * v)
101
+ elif v <= 988.:
102
+ i = int(v)
103
+ elif v > 988.:
104
+ i = 989
105
+ else:
106
+ raise ValueError('internal error for rain amount: %f' % format(v))
107
+ s.loc[k] = i
108
+ return s
109
+
110
+ def precipitation_from_synop(synop):
111
+ """
112
+ calculate precipitation amount from SYNOP code numbers
113
+
114
+ Parameters
115
+ ----------
116
+ synop : pandas.Series or array-like
117
+ SYNOP code numbers
118
+
119
+ Returns
120
+ -------
121
+ p : pandas.Series
122
+ precipitation amount in mm
123
+ """
124
+ s = pd.Series(synop).astype(float)
125
+ p = pd.Series(np.nan, index=s.index)
126
+ for k,v in s.items():
127
+ if np.isnan(v) or v < 0.:
128
+ i = np.nan
129
+ elif v == 0.:
130
+ i = 0.
131
+ elif v < 990.:
132
+ i = v
133
+ elif v < 991.:
134
+ i = 0.5
135
+ elif v <= 999.:
136
+ i = (v - 990.) / 10.
137
+ else:
138
+ raise ValueError('internal error synop code: %.0f' % format(v))
139
+ p.loc[k] = i
140
+ return p
67
141
 
68
142
  class DataFile(object):
69
- '''
143
+ """
70
144
  object class that holds data and metadata of a dmna file
71
145
 
72
146
  :param file: (optional, string) filename (optionally including path). \
@@ -74,50 +148,56 @@ class DataFile(object):
74
148
  :param data: (optional, padas.DataFrame) timeseries data. \
75
149
  Expected format: Time (datetime64) as data index,
76
150
  wind speed in m/s in column ``FF``, winddirection din degrees in
77
- column ``DD``, stability class in column ``KM``
151
+ column ``DD``, stability class in column ``KM``.
152
+ Precipitation amount in mm column `PP` if prec is True.
78
153
  :param z0: (optional, float) surface roughness lenght in m.
79
- If data is not given, this parameter is ignored.
154
+ This parameter is only used if `data` is given.
80
155
  If ``None`` or missing, effective anemometer height are set to 0.
81
156
  :param has: (optional, float) height of the anemometer in m.
82
- If data is not given, this parameter is ignored.
157
+ This parameter is only used if `data` is given.
83
158
  If missing, 10 m is used.
84
- '''
159
+ :param prec: (optional, bool or None) If True file / data
160
+ contains additional column `PP` for rain amount in mm.
161
+ Default to None.
162
+ """
85
163
 
86
164
  file = None
87
- ''' name of file loaded into object '''
165
+ """ name of file loaded into object """
88
166
  header = None
89
- ''' array containing the header lines as strings'''
167
+ """ array containing the header lines as strings"""
90
168
  vars = None
91
- ''' Number of variables in file '''
169
+ """ Number of variables in file """
92
170
  heights = None
93
- ''' effective anemometer heights for each rouchness class in m'''
171
+ """ effective anemometer heights for each rouchness class in m"""
94
172
  data = None
95
- ''' DataFrame containing the data from the file loaded.
173
+ """ DataFrame containing the data from the file loaded.
96
174
  The orginal columns KENN, 'JAHR', 'MON', 'TAG', 'STUN', 'NULL'
97
175
  are not contained in the DataFrame, instead the date and time
98
176
  are given in the index (datetime64).
99
- '''
177
+ """
100
178
  prec = False
101
- ''' file is extended AKTerm Format containing additional columns
179
+ """ file is extended AKTerm Format containing additional columns
102
180
  containing precipitation infromation
103
- '''
181
+ """
104
182
  # ----------------------------------------------------------------------
105
183
  #
106
184
  # read header
107
185
  #
108
186
 
109
- def _get_header(self, f):
110
- '''
187
+ def _get_header(self, f=None):
188
+ """
111
189
  parses the file as text, finds the divider line "*"
112
190
  and returns the header as dictionary
113
- '''
191
+ """
192
+ if f is None:
193
+ f = self.file
114
194
  header = []
115
195
  f.seek(0)
116
196
  for line in f:
117
197
  stripped = line.strip()
118
198
  if stripped.startswith("*"):
119
199
  header.append(stripped[1:].strip())
120
- logging.debug('header: %s' % header[-1])
200
+ logger.debug('header: %s' % header[-1])
121
201
  else:
122
202
  break
123
203
  return header
@@ -126,11 +206,13 @@ class DataFile(object):
126
206
  # read header
127
207
  #
128
208
 
129
- def _get_heights(self, f):
130
- '''
209
+ def _get_heights(self, f=None):
210
+ """
131
211
  parses the file as text, line prefixed by "+"
132
212
  and returns the effective anemometer heights
133
- '''
213
+ """
214
+ if f is None:
215
+ f = self.file
134
216
  heights = []
135
217
  f.seek(0)
136
218
  for line in f.readlines():
@@ -138,7 +220,7 @@ class DataFile(object):
138
220
  if stripped.startswith("+"):
139
221
  numstr = stripped.split(':')[1]
140
222
  heights = np.fromstring(numstr, dtype=int, sep=' ') * 0.1
141
- logging.debug('heights: %s' % format(heights))
223
+ logger.debug('heights: %s' % format(heights))
142
224
  break
143
225
  return heights
144
226
 
@@ -146,12 +228,14 @@ class DataFile(object):
146
228
  #
147
229
  # read data
148
230
  #
149
- def _get_data(self, f, prec=False):
150
- '''
231
+ def _get_data(self, f=None, prec=False):
232
+ """
151
233
  parses the file as text, skips header
152
234
  and returns the data as dataframe
153
235
  if prec is True, precipitation columns are read
154
- '''
236
+ """
237
+ if f is None:
238
+ f = self.file
155
239
  header_lines = 0
156
240
  f.seek(0)
157
241
  for line in f.readlines():
@@ -196,26 +280,30 @@ class DataFile(object):
196
280
  # 9 Windrichtung fehlt
197
281
  data['DD'] = data['DD'].mask(data['QDD'] == 9, np.nan, axis=0)
198
282
  #
199
- # 9 Niderschlag fehlt oder verdaechtig
283
+ # 9 Niederschlag fehlt oder verdächtig
200
284
  if prec:
201
- data['PP'] = data['PP'].mask(data['QPP'] == 9, np.nan, axis=0)
285
+ # 9 Niederschlag fehlt; SYNOP code -> mm
286
+ data['PP'] = precipitation_from_synop(
287
+ data['PP'].mask(data['QPP'] == 9, np.nan, axis=0)
288
+ )
289
+
202
290
  #
203
291
  # Make datetime:
204
- data.index = pd.to_datetime({'year': data['JAHR'],
292
+ data.index = pd.to_datetime(pd.DataFrame({'year': data['JAHR'],
205
293
  'month': data['MON'],
206
294
  'day': data['TAG'],
207
- 'hour': data['STUN']})
295
+ 'hour': data['STUN']}))
208
296
  data.drop(columns=['KENN', 'JAHR', 'MON', 'TAG', 'STUN', 'NULL'])
209
297
  return data
210
298
  # ----------------------------------------------------------------------
211
299
  #
212
- # ouput data
300
+ # output data
213
301
  #
214
302
 
215
303
  def _out_data(self, prec=False):
216
- '''
304
+ """
217
305
  prepare DataFrame consistent and in proper unis for output
218
- '''
306
+ """
219
307
  out = self.data.copy()
220
308
  if 'KENN' not in out.columns:
221
309
  out['KENN'] = 'AK'
@@ -226,9 +314,9 @@ class DataFile(object):
226
314
  if q not in out.columns:
227
315
  out[q] = 0
228
316
  # flag 9 marks "no value"
229
- out[q].mask(out[x].isna(), 9, inplace=True)
317
+ out[q] = out[q].mask(out[x].isna(), 9)
230
318
  # value 7 marks "no value"
231
- out['KM'].mask(out['KM'].isna(), 7, inplace=True)
319
+ out['KM'] = out['KM'].mask(out['KM'].isna(), 7)
232
320
  for q in ['QQ1', 'QQ2', 'QQ3']:
233
321
  if q not in out.columns:
234
322
  out[q] = 0
@@ -270,7 +358,10 @@ class DataFile(object):
270
358
  out['DD'] = out['DD'].mask(out['QDD'] == 9, 999, axis=0)
271
359
  #
272
360
  if prec:
273
- out['PP'] = out['PP'].mask(out['QPP'] == 9, np.nan, axis=0)
361
+ # 9 Niederschlag fehlt; mm -> SYNOP code
362
+ out['PP'] = precipitation_to_synop(
363
+ out['PP'].mask(out['QPP'] == 9, np.nan, axis=0)
364
+ )
274
365
  # make columns integer
275
366
  if prec:
276
367
  akt_columns = _AKTN_COLUMNS
@@ -281,7 +372,7 @@ class DataFile(object):
281
372
  try:
282
373
  out[c] = out[c].map(np.round).map(int)
283
374
  except Exception as e:
284
- logging.error('column did not convert: ' + c)
375
+ logger.error('column did not convert: ' + c)
285
376
  raise e
286
377
  #
287
378
  # reorder columns:
@@ -319,7 +410,7 @@ class DataFile(object):
319
410
  #
320
411
 
321
412
  def get_h_anemo(self, z0=None):
322
- '''
413
+ """
323
414
  returns the effective anemometer height(s) from the object
324
415
 
325
416
  :param z0: roughness length for which the effective anemometer height
@@ -327,7 +418,7 @@ class DataFile(object):
327
418
  If missing, all heights are returned as array
328
419
  :return: effective anemometer height in m
329
420
  :rtype: float or array
330
- '''
421
+ """
331
422
  if z0 is None:
332
423
  re = self.heights
333
424
  else:
@@ -345,12 +436,12 @@ class DataFile(object):
345
436
  #
346
437
 
347
438
  def set_h_anemo(self, z0=None, has=None):
348
- '''
439
+ """
349
440
  sets the effective anemometer height(s) from z0 and has
350
441
  :param z0: roughness length at the site of the wind measurement in m
351
442
  :param has: height of the wind measurement in m.
352
443
  If ``None`` or missing, 10 m is used.
353
- '''
444
+ """
354
445
  if has is None:
355
446
  has = 10.
356
447
  self.heights = h_eff(z0, has)
@@ -387,8 +478,8 @@ class DataFile(object):
387
478
  #
388
479
 
389
480
  def load(self, file):
390
- '''
391
- loads the contents of a akterm file into the object
481
+ """
482
+ loads the contents of an akterm file into the object
392
483
 
393
484
  :param file: filename (optionally including path). \
394
485
  If missing, an emtpy
@@ -396,7 +487,8 @@ class DataFile(object):
396
487
  DD in DEG, an KM Korman/Meixner stability class,
397
488
  columns QDD,QFF,QQ1,QQ1,HM are contained as in the
398
489
  file.
399
- '''
490
+ """
491
+ logger.info('loading file: %s' % file)
400
492
  with open(self.file, 'r') as f:
401
493
  self.header = self._get_header(f)
402
494
  if len(self.header) >= 1 and _PREC_KEYWORD in self.header[0]:
@@ -427,7 +519,7 @@ class DataFile(object):
427
519
  else:
428
520
  self.data = pd.DataFrame(data)
429
521
  if z0 is None:
430
- self.heights = [0 for x in z0_classes]
522
+ self.heights = [0] * len(z0_classes)
431
523
  else:
432
524
  self.set_h_anemo(z0, has)
433
525
  self.file = None
@@ -444,15 +536,15 @@ class DataFile(object):
444
536
 
445
537
 
446
538
  def h_eff(z0s, has):
447
- '''
539
+ """
448
540
  calulate effectice anemometer heights for all
449
541
  roughness-length classes used in asutal2000
450
- :param z0: roughness length at the site of the wind measurement in m
542
+ :param z0s: roughness length at the site of the wind measurement in m
451
543
  :param has: height of the wind measurement in m
452
544
  :return: list of length 9 containing the nine
453
545
  effecive anemometer heights
454
546
  :rtype: list(9)
455
- '''
547
+ """
456
548
  href = 250
457
549
  d0s = _displacement_factor * z0s
458
550
  ps = np.log((has - d0s) / z0s) / np.log((href - d0s) / z0s)
@@ -468,41 +560,9 @@ def h_eff(z0s, has):
468
560
  if __name__ == '__main__':
469
561
  # import matplotlib.pyplot as plt
470
562
  # from matplotlib import cm
471
- logging.basicConfig(level=logging.DEBUG)
563
+ logger.setLevel(logging.DEBUG)
472
564
  #
473
565
  # test axes
474
566
  qq = DataFile('../tests/anno95.akterm')
475
567
  print(qq.data[['KENN', 'FF', 'DD']])
476
568
  qq.write('../tests/out.akterm')
477
-
478
- #
479
- # test 2D
480
- # dmna=DataFile('../tests/so2-y00a.dmna')
481
- # blah=dmna.data['con']
482
- # print(np.shape(blah))
483
- # print(np.nanmin(blah),np.nanmax(blah))
484
- # blah[5,10:15,:]=0.
485
- # plt.contourf(
486
- # np.transpose(blah[:,:,0]),
487
- # cmap=cm.get_cmap('YlGnBu')
488
- # )
489
-
490
- # # test 3D
491
- # dmna=DataFile('../tests/w1018a00.dmna')
492
- # blah=np.sqrt( dmna.data['Vx']**2 + dmna.data['Vy']**2 )
493
- # print(np.shape(blah))
494
- # print(np.nanmin(blah),np.nanmax(blah))
495
- # blah[5,5:10,:]=0.
496
- # plt.contourf(
497
- # np.transpose(blah[:,:,4]),
498
- # cmap=cm.get_cmap('magma')
499
- # )
500
- #
501
-
502
- # # test zeitreihe
503
- # dmna=DataFile('../tests/zeitreihe.dmna')
504
- # t=dmna.data['te']
505
- # blah=dmna.data['ua']
506
- # print(np.shape(blah))
507
- # print(np.nanmin(blah),np.nanmax(blah))
508
- # plt.plot(t,blah)