meteoraster 2.5__py3-none-any.whl
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.
- meteoraster/__init__.py +5 -0
- meteoraster/meteoraster.py +1267 -0
- meteoraster/resample.py +269 -0
- meteoraster/utils.py +359 -0
- meteoraster-2.5.dist-info/METADATA +419 -0
- meteoraster-2.5.dist-info/RECORD +10 -0
- meteoraster-2.5.dist-info/WHEEL +5 -0
- meteoraster-2.5.dist-info/licenses/LICENSE +202 -0
- meteoraster-2.5.dist-info/licenses/NOTICE +18 -0
- meteoraster-2.5.dist-info/top_level.txt +1 -0
meteoraster/resample.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
|
|
2
|
+
import math
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
class Resample(object):
|
|
7
|
+
'''
|
|
8
|
+
Resamples meteorological data for use in hydrological and forecasting models
|
|
9
|
+
handles pandas dataframes [production timestamps x leadtime timedeltas]
|
|
10
|
+
Does not need initialization as the main method (resample) is a class method.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
v1.01:
|
|
14
|
+
imported to meteoraster from tethys
|
|
15
|
+
'''
|
|
16
|
+
|
|
17
|
+
VERSION = '1.01'
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def resample(cls, data, timestep_frequency, resamplingType, date_from=None, date_to=None, print_func=None):
|
|
21
|
+
'''
|
|
22
|
+
Main method of the class.
|
|
23
|
+
Takes the following inputs:
|
|
24
|
+
data: pandas dataframe [production timestamps x leadtime timedeltas]
|
|
25
|
+
timestep_frequency: pandas pd.Timedelta or equivalent string (e.g., '1D', '3H', '30min', '30T', etc...).
|
|
26
|
+
resamplingType: 'sum', 'mean', or 'linear. 'sum' and 'mean' are very similar. There is a difference only when the timestep_frequency is larger than the production frequency (1D case) or the leadtime frequency (2D case).
|
|
27
|
+
date_from: first datetime that is displayed (rows)
|
|
28
|
+
date_to: last datetime that is displayed (rows) - when upsampling additional values may be added.
|
|
29
|
+
print_func: a function handle to print the evolution of the calculations
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### THE VALIDITY CHECK SHOULD BE REVIEWED
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
headers = list(data.columns.names)
|
|
36
|
+
headers.remove('leadtime')
|
|
37
|
+
|
|
38
|
+
# Default values for the date
|
|
39
|
+
if isinstance(date_from, type(None)):
|
|
40
|
+
date_from = data.index[0]
|
|
41
|
+
if isinstance(date_to, type(None)):
|
|
42
|
+
date_to = data.index[-1]
|
|
43
|
+
|
|
44
|
+
if isinstance(timestep_frequency, dict):
|
|
45
|
+
timestep_frequency = pd.Timedelta(**timestep_frequency)
|
|
46
|
+
elif isinstance(timestep_frequency, str):
|
|
47
|
+
timestep_frequency = pd.Timedelta(timestep_frequency)
|
|
48
|
+
|
|
49
|
+
idx_map = None
|
|
50
|
+
resampled = []
|
|
51
|
+
for g0_, mis0_ in data.groupby(headers, axis=1):
|
|
52
|
+
# Loop - separate handling of each header combination (indexes are calculated on the first pass)
|
|
53
|
+
g0_ = [mis0_.columns.get_level_values(h)[0] for h in headers]
|
|
54
|
+
mis0_ = mis0_.droplevel(headers, axis=1)
|
|
55
|
+
|
|
56
|
+
# Progress message
|
|
57
|
+
if not isinstance(print_func, type(None)):
|
|
58
|
+
print_func(g0_[-1])
|
|
59
|
+
|
|
60
|
+
# Infer frequencies
|
|
61
|
+
# Production (rows)
|
|
62
|
+
prod_frequency = pd.infer_freq(mis0_.index.values[0:3])
|
|
63
|
+
if not prod_frequency:
|
|
64
|
+
prod_frequency = pd.infer_freq(mis0_.index.values[3:6])
|
|
65
|
+
if not prod_frequency[0].isdigit():
|
|
66
|
+
prod_frequency = '1' + prod_frequency
|
|
67
|
+
prod_frequency = pd.Timedelta(prod_frequency)
|
|
68
|
+
|
|
69
|
+
date_from_ = date_from - timestep_frequency * math.ceil(prod_frequency / timestep_frequency)
|
|
70
|
+
|
|
71
|
+
# Leadtime (columns)
|
|
72
|
+
if mis0_.shape[1]>2:
|
|
73
|
+
lead_frequency = pd.infer_freq(mis0_.columns.get_level_values('leadtime'))
|
|
74
|
+
if not lead_frequency[0].isdigit():
|
|
75
|
+
lead_frequency = '1' + lead_frequency
|
|
76
|
+
lead_frequency = pd.Timedelta(lead_frequency)
|
|
77
|
+
elif mis0_.shape[1]==2:
|
|
78
|
+
tmp = mis0_.columns.get_level_values('leadtime')
|
|
79
|
+
lead_frequency = tmp[1]-tmp[0]
|
|
80
|
+
else:
|
|
81
|
+
lead_frequency = prod_frequency
|
|
82
|
+
|
|
83
|
+
# Check frequency ratio
|
|
84
|
+
fr_ratio = prod_frequency/lead_frequency
|
|
85
|
+
if mis0_.shape[1]!=1 and mis0_.shape[1]<fr_ratio:
|
|
86
|
+
raise Exception('Problem with the leadtime [%u (multiple of %u) "%s"] . Does it agree with the production step ["%s"]?' % (mis0_.shape[1], fr_ratio, lead_frequency, prod_frequency))
|
|
87
|
+
|
|
88
|
+
# Check timestep ratio
|
|
89
|
+
ts_ratio = timestep_frequency/lead_frequency
|
|
90
|
+
if mis0_.shape[1]!=1 and mis0_.shape[1]/ts_ratio!=int(mis0_.shape[1]/ts_ratio):
|
|
91
|
+
additional_mis0_steps = int((int(mis0_.shape[1]/ts_ratio)+1) * ts_ratio)-mis0_.shape[1]
|
|
92
|
+
additional_cols = pd.timedelta_range(start=mis0_.columns[-1], periods=additional_mis0_steps+1, freq=lead_frequency, closed='right')
|
|
93
|
+
additional_mis0 = pd.DataFrame(np.nan, index=mis0_.index, columns=additional_cols)
|
|
94
|
+
mis0_ = pd.concat((mis0_, additional_mis0), axis=1)
|
|
95
|
+
#===============================================================
|
|
96
|
+
# valid_mis0_steps = int(int(mis0_.shape[1]/ts_ratio) * ts_ratio)
|
|
97
|
+
# mis0_ = mis0_.iloc[:, :valid_mis0_steps]
|
|
98
|
+
#===============================================================
|
|
99
|
+
|
|
100
|
+
print(' Issue with the leadtime [%u (multiple of %u) "%s"]. Does it agree with the time step ["%s"]? Augmented %u steps.' % (mis0_.shape[1], ts_ratio, lead_frequency, prod_frequency, additional_mis0_steps))
|
|
101
|
+
#===============================================================
|
|
102
|
+
# raise Exception('Problem with the leadtime [%u (multiple of %u) "%s"]. Does it agree with the time step ["%s"]?' % (mis0_.shape[1], ts_ratio, lead_frequency, prod_frequency))
|
|
103
|
+
#===============================================================
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
calculation_frequency = min((prod_frequency, pd.Timedelta('1H'), timestep_frequency, lead_frequency))
|
|
107
|
+
if mis0_.shape[1]==1:
|
|
108
|
+
resampled_ = cls._resample_aux_1D(mis0_, prod_frequency, calculation_frequency, timestep_frequency, resamplingType, date_from_, date_to)
|
|
109
|
+
else:
|
|
110
|
+
resampled_, idx_map = cls._resample_aux_2D(mis0_, prod_frequency, lead_frequency, calculation_frequency, timestep_frequency, resamplingType, date_from_, date_to, idx_map)
|
|
111
|
+
|
|
112
|
+
#===================================================================
|
|
113
|
+
# mis0_.to_clipboard(excel=True)
|
|
114
|
+
# resampled_.reindex(production_range, axis=0).to_clipboard(excel=True)
|
|
115
|
+
#===================================================================
|
|
116
|
+
resampled_.columns = pd.MultiIndex.from_product([[i] for i in g0_] + [resampled_.columns.tolist()], names=headers + ['delay'])
|
|
117
|
+
resampled.append(resampled_)
|
|
118
|
+
|
|
119
|
+
# Aggregate to the desired time step
|
|
120
|
+
resampled = pd.concat(resampled, axis=1)
|
|
121
|
+
production_range = pd.date_range(start=date_from, end=date_to, freq=timestep_frequency)
|
|
122
|
+
|
|
123
|
+
return resampled.reindex(production_range, axis=0)
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _resample_aux_1D(cls, data, prod_frequency, calculation_frequency, timestep_frequency, resamplingType, date_from, date_to):
|
|
127
|
+
'''
|
|
128
|
+
|
|
129
|
+
prod_frequency assumed to be compatible with timestep_frequency
|
|
130
|
+
calculation frequency is at least hourly to account for timezone shifts
|
|
131
|
+
'''
|
|
132
|
+
|
|
133
|
+
# Calculate reference step windows
|
|
134
|
+
production_calculation_window = int(prod_frequency/calculation_frequency)
|
|
135
|
+
timestep_calculation_window = int(timestep_frequency/calculation_frequency)
|
|
136
|
+
|
|
137
|
+
calculation_range = pd.date_range(start=date_from, end=date_to + prod_frequency, freq=calculation_frequency, inclusive='left')
|
|
138
|
+
resampled_ = data.reindex(calculation_range, axis=0)
|
|
139
|
+
|
|
140
|
+
if resamplingType=='sum' or resamplingType=='mean':
|
|
141
|
+
if production_calculation_window>1:
|
|
142
|
+
resampled_ = resampled_.ffill(limit=production_calculation_window-1) / production_calculation_window # value per calculation step
|
|
143
|
+
resampled_ = resampled_.resample(rule=timestep_frequency, axis=0).sum(min_count=timestep_calculation_window) # sum per time step
|
|
144
|
+
|
|
145
|
+
production_ratio = int(production_calculation_window / timestep_calculation_window)
|
|
146
|
+
if production_ratio > 1:
|
|
147
|
+
if production_ratio != production_calculation_window / timestep_calculation_window:
|
|
148
|
+
raise Exception('Production step not multiple of calculation time step.')
|
|
149
|
+
|
|
150
|
+
tmp = []
|
|
151
|
+
for i0 in range(production_ratio):
|
|
152
|
+
tmp.append(resampled_.shift(-i0))
|
|
153
|
+
tmp = pd.concat(tmp, axis=1)
|
|
154
|
+
tmp.columns = pd.timedelta_range('0D', end=prod_frequency, freq=timestep_frequency, name='leadtimes', closed='left')
|
|
155
|
+
resampled_ = tmp.sum(axis=1, min_count=production_ratio).to_frame()
|
|
156
|
+
if resamplingType=='mean':
|
|
157
|
+
resampled_ /= production_ratio
|
|
158
|
+
resampled_.columns = [pd.Timedelta('0D')]
|
|
159
|
+
|
|
160
|
+
else:
|
|
161
|
+
if resamplingType=='mean':
|
|
162
|
+
resampled_ *= production_calculation_window / timestep_calculation_window
|
|
163
|
+
elif resamplingType=='linear':
|
|
164
|
+
if production_calculation_window>1:
|
|
165
|
+
resampled_ = resampled_.interpolate(axis=0, limit=production_calculation_window-1, method='linear', limit_area='inside')
|
|
166
|
+
elif resamplingType=='max':
|
|
167
|
+
if production_calculation_window>1:
|
|
168
|
+
resampled_ = resampled_.ffill(limit=production_calculation_window-1) # value per calculation step
|
|
169
|
+
resampled_ = resampled_.resample(rule=timestep_frequency, axis=0).max(min_count=timestep_calculation_window) # sum per time step
|
|
170
|
+
else:
|
|
171
|
+
raise Exception('Resampling type not implemented: %s.' % resamplingType)
|
|
172
|
+
|
|
173
|
+
return resampled_
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def _resample_aux_2D(cls, data, prod_frequency, leadtime_frequency, calculation_frequency, timestep_frequency, resamplingType, date_from, date_to, idx_map):
|
|
177
|
+
'''
|
|
178
|
+
|
|
179
|
+
'''
|
|
180
|
+
|
|
181
|
+
# Reindex to calculation delta when required
|
|
182
|
+
if isinstance(idx_map, type(None)):
|
|
183
|
+
idx_map = cls._resample_aux_2D_idx(data, prod_frequency, leadtime_frequency, calculation_frequency, timestep_frequency, resamplingType, date_from, date_to, idx_map)
|
|
184
|
+
|
|
185
|
+
# Calculate reference step windows
|
|
186
|
+
leadtime_calculation_window = int(leadtime_frequency/calculation_frequency)
|
|
187
|
+
timestep_calculation_window = int(timestep_frequency/calculation_frequency)
|
|
188
|
+
|
|
189
|
+
resampled_ = idx_map.copy()*np.nan
|
|
190
|
+
tmp0 = idx_map.dropna().astype(int).values.ravel()
|
|
191
|
+
tmp1 = np.isfinite(idx_map).astype(bool).values.ravel()
|
|
192
|
+
resampled_.loc[tmp1,:] = data.values.ravel()[tmp0]
|
|
193
|
+
resampled_ = resampled_.unstack('leadtime')
|
|
194
|
+
resampled_ = resampled_.droplevel(0, axis=1)
|
|
195
|
+
|
|
196
|
+
# Aggregate to desired time step (columns)
|
|
197
|
+
if timestep_calculation_window>1 or leadtime_calculation_window>1:
|
|
198
|
+
if resamplingType=='sum' or resamplingType=='mean':
|
|
199
|
+
resampled_ = resampled_.resample(rule=timestep_frequency, axis=1).sum(min_count=timestep_calculation_window) / timestep_calculation_window
|
|
200
|
+
if resamplingType=='sum':
|
|
201
|
+
resampled_ *= timestep_calculation_window / leadtime_calculation_window
|
|
202
|
+
elif resamplingType=='linear':
|
|
203
|
+
if leadtime_calculation_window>1:
|
|
204
|
+
resampled_ = resampled_.interpolate(axis=1, limit=leadtime_calculation_window-1, method='linear')
|
|
205
|
+
leadtimes_ = pd.timedelta_range(start=max((pd.Timedelta(hours=0), resampled_.columns[0])), end=resampled_.columns[-1], freq=timestep_frequency)
|
|
206
|
+
resampled_ = resampled_.reindex(leadtimes_, axis=1)
|
|
207
|
+
elif resamplingType=='max':
|
|
208
|
+
resampled_ = resampled_.resample(rule=timestep_frequency, axis=1).max(min_count=timestep_calculation_window)
|
|
209
|
+
else:
|
|
210
|
+
raise Exception('Resampling type not implemented: %s.' % resamplingType)
|
|
211
|
+
|
|
212
|
+
return (resampled_, idx_map)
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def _resample_aux_2D_idx(cls, data, prod_frequency, leadtime_frequency, calculation_frequency, timestep_frequency, resamplingType, date_from, date_to, idx_map):
|
|
216
|
+
'''
|
|
217
|
+
|
|
218
|
+
'''
|
|
219
|
+
# Calculate reference step windows
|
|
220
|
+
production_calculation_window = int(prod_frequency/calculation_frequency)
|
|
221
|
+
leadtime_calculation_window = int(leadtime_frequency/calculation_frequency)
|
|
222
|
+
|
|
223
|
+
# Reindex to calculation delta when required
|
|
224
|
+
if isinstance(idx_map, type(None)):
|
|
225
|
+
idx_map = data.copy()
|
|
226
|
+
idx_map.loc[:,:] = np.arange(idx_map.size).reshape(idx_map.shape, order='C')
|
|
227
|
+
idx_map = idx_map
|
|
228
|
+
leadtimes = data.columns
|
|
229
|
+
|
|
230
|
+
# Handle columns (leadtimes)
|
|
231
|
+
if leadtime_calculation_window>1:
|
|
232
|
+
if resamplingType=='linear':
|
|
233
|
+
leadtime_index = pd.timedelta_range(start=leadtimes[0] - leadtime_frequency + calculation_frequency, end=leadtimes[-1] + leadtime_frequency, freq=calculation_frequency, closed='left')
|
|
234
|
+
else:
|
|
235
|
+
leadtime_index = pd.timedelta_range(start=leadtimes[0], end=leadtimes[-1]+leadtime_frequency, freq=calculation_frequency, closed='left')
|
|
236
|
+
if resamplingType=='sum' or resamplingType=='mean' or resamplingType=='max':
|
|
237
|
+
idx_map = idx_map.reindex(leadtime_index, axis=1).ffill(limit=leadtime_calculation_window-1, axis=1)
|
|
238
|
+
elif resamplingType=='linear':
|
|
239
|
+
idx_map = idx_map.reindex(leadtime_index, axis=1)
|
|
240
|
+
else:
|
|
241
|
+
raise Exception('Resampling type not implemented: %s.' % resamplingType)
|
|
242
|
+
idx_map.columns.name = 'leadtime'
|
|
243
|
+
|
|
244
|
+
# Handle rows (production times)
|
|
245
|
+
if production_calculation_window>1:
|
|
246
|
+
calculation_index = pd.date_range(start=date_from, end=date_to+leadtime_frequency, freq=calculation_frequency, inclusive='left')
|
|
247
|
+
|
|
248
|
+
idx_map = idx_map.reindex(calculation_index, axis=0)
|
|
249
|
+
valid = idx_map.iloc[0, :]*np.nan
|
|
250
|
+
shift = 1
|
|
251
|
+
for r0 in range(0, idx_map.shape[0]):
|
|
252
|
+
if np.isfinite(idx_map.iloc[r0, :]).any():
|
|
253
|
+
valid = idx_map.iloc[r0, :]
|
|
254
|
+
shift = 1
|
|
255
|
+
else:
|
|
256
|
+
idx_map.iloc[r0, :] = valid.shift(-shift)
|
|
257
|
+
shift += 1
|
|
258
|
+
|
|
259
|
+
idx_map = idx_map.stack('leadtime', dropna=False).to_frame()
|
|
260
|
+
|
|
261
|
+
return idx_map
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def production_to_event(cls, data, prod_frequency):
|
|
265
|
+
'''
|
|
266
|
+
'''
|
|
267
|
+
|
|
268
|
+
raise(Exception('Not implemented'))
|
|
269
|
+
|
meteoraster/utils.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Created on 10/02/2023
|
|
3
|
+
|
|
4
|
+
@author: Jose Pedro Matos
|
|
5
|
+
'''
|
|
6
|
+
|
|
7
|
+
import warnings
|
|
8
|
+
import numpy as np
|
|
9
|
+
import xarray as xr
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from meteoraster import MeteoRaster
|
|
14
|
+
|
|
15
|
+
def readERA5_monthly(file, variable, **kwargs):
|
|
16
|
+
'''
|
|
17
|
+
Return a dict:
|
|
18
|
+
data: 5-D numpy array [productionDate, ensembleMember, leadtime, latitude, longitude]
|
|
19
|
+
production dates: 1-D numpy array
|
|
20
|
+
leadtimes: 1-D numpy array
|
|
21
|
+
latitudes: 1-D numpy array
|
|
22
|
+
longitudes: 1-D numpy array
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
data = {}
|
|
26
|
+
with xr.open_dataset(file, engine='cfgrib', indexpath='', **kwargs) as ds:
|
|
27
|
+
data['latitudes'] = ds.latitude.data
|
|
28
|
+
data['longitudes'] = ds.longitude.data
|
|
29
|
+
data['productionDates'] = pd.to_datetime(ds.time.data)
|
|
30
|
+
tmp = ds[variable][:, :, :].data
|
|
31
|
+
data['data'] = np.expand_dims(tmp, [1, 2])
|
|
32
|
+
data['leadtimes'] = np.array([pd.DateOffset(days=0)])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if variable == 'tp':
|
|
36
|
+
data['data'] *= 1000 #to mm/day
|
|
37
|
+
days_in_month = data['productionDates'].days_in_month
|
|
38
|
+
days_in_month = np.expand_dims(days_in_month, (1, 2, 3, 4))
|
|
39
|
+
days_in_month = np.tile(days_in_month, [1] + list(data['data'].shape[1:]))
|
|
40
|
+
data['data'] *= days_in_month
|
|
41
|
+
units = 'mm/month'
|
|
42
|
+
elif variable == 't2m':
|
|
43
|
+
data['data'] -= 273.15
|
|
44
|
+
units = 'C'
|
|
45
|
+
elif variable == 'ro':
|
|
46
|
+
data['data'] *= 1000
|
|
47
|
+
units = 'mm/month'
|
|
48
|
+
elif variable == 'e':
|
|
49
|
+
data['data'] *= 1000
|
|
50
|
+
units = 'mm/month'
|
|
51
|
+
|
|
52
|
+
tmp = MeteoRaster(data, variable=variable, units=units)
|
|
53
|
+
return tmp
|
|
54
|
+
|
|
55
|
+
def readERA5Land_monthly(file, variable, **kwargs):
|
|
56
|
+
'''
|
|
57
|
+
Return a dict:
|
|
58
|
+
data: 5-D numpy array [productionDate, ensembleMember, leadtime, latitude, longitude]
|
|
59
|
+
production dates: 1-D numpy array
|
|
60
|
+
leadtimes: 1-D numpy array
|
|
61
|
+
latitudes: 1-D numpy array
|
|
62
|
+
longitudes: 1-D numpy array
|
|
63
|
+
|
|
64
|
+
https://cds.climate.copernicus.eu/datasets/reanalysis-era5-land-monthly-means?tab=overview
|
|
65
|
+
|
|
66
|
+
t2m: temperature
|
|
67
|
+
tp: total precipitation
|
|
68
|
+
...
|
|
69
|
+
'''
|
|
70
|
+
|
|
71
|
+
if variable=='tp':
|
|
72
|
+
if 'backend_kwargs' in kwargs.keys():
|
|
73
|
+
kwargs['backend_kwargs'].update({'filter_by_keys': {'shortName': 'tp', 'stepType': 'avgad'}})
|
|
74
|
+
else:
|
|
75
|
+
kwargs['backend_kwargs'] = {'filter_by_keys': {'shortName': 'tp', 'stepType': 'avgad'}}
|
|
76
|
+
|
|
77
|
+
data = {}
|
|
78
|
+
with xr.open_dataset(file, engine='cfgrib', indexpath='', **kwargs) as ds:
|
|
79
|
+
data['latitudes'] = ds.latitude.data
|
|
80
|
+
data['longitudes'] = ds.longitude.data
|
|
81
|
+
data['productionDates'] = pd.to_datetime(ds.time.data)
|
|
82
|
+
tmp = ds[variable][:, :, :].data
|
|
83
|
+
data['data'] = np.expand_dims(tmp, [1, 2])
|
|
84
|
+
data['leadtimes'] = np.array([pd.DateOffset(days=0)])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if variable == 'tp':
|
|
88
|
+
data['data'] *= 1000 #to mm/day
|
|
89
|
+
days_in_month = data['productionDates'].days_in_month
|
|
90
|
+
days_in_month = np.expand_dims(days_in_month, (1, 2, 3, 4))
|
|
91
|
+
days_in_month = np.tile(days_in_month, [1] + list(data['data'].shape[1:]))
|
|
92
|
+
data['data'] *= days_in_month
|
|
93
|
+
units = 'mm/month'
|
|
94
|
+
elif variable == 't2m':
|
|
95
|
+
data['data'] -= 273.15
|
|
96
|
+
units = 'C'
|
|
97
|
+
elif variable == 'ro':
|
|
98
|
+
data['data'] *= 1000
|
|
99
|
+
units = 'mm/month'
|
|
100
|
+
elif variable == 'e':
|
|
101
|
+
data['data'] *= 1000
|
|
102
|
+
units = 'mm/month'
|
|
103
|
+
else:
|
|
104
|
+
raise(Exception(f'How to handle variable "{variable}" must be defined.'))
|
|
105
|
+
|
|
106
|
+
tmp = MeteoRaster(data, variable=variable, units=units)
|
|
107
|
+
return tmp
|
|
108
|
+
|
|
109
|
+
def read_ERA5Land_hourly(file, variable, file_next_year=None):
|
|
110
|
+
'''
|
|
111
|
+
Returns a MeteoRaster object with the ERA5 Land data
|
|
112
|
+
'''
|
|
113
|
+
|
|
114
|
+
cumulative = {'tp': True, 'ssr': True, 'u10': False, 'v10': False, 'sd': False, 't2m': False}
|
|
115
|
+
|
|
116
|
+
def _read_ERA5Land_helper(file, variable):
|
|
117
|
+
'''
|
|
118
|
+
Just reads the grib file
|
|
119
|
+
'''
|
|
120
|
+
|
|
121
|
+
data = {}
|
|
122
|
+
with xr.open_dataset(file, engine='cfgrib', indexpath='') as ds:
|
|
123
|
+
data['latitudes'] = ds.latitude.data
|
|
124
|
+
data['longitudes'] = ds.longitude.data
|
|
125
|
+
data['productionDates'] = ds.time.data
|
|
126
|
+
if isinstance(data['productionDates'], np.datetime64):
|
|
127
|
+
data['productionDates'] = np.array([data['productionDates']])
|
|
128
|
+
data['data'] = ds[variable][:, :, :].data
|
|
129
|
+
data['steps'] = ds.step.data
|
|
130
|
+
|
|
131
|
+
return data
|
|
132
|
+
|
|
133
|
+
data = _read_ERA5Land_helper(file, variable)
|
|
134
|
+
|
|
135
|
+
if cumulative[variable]:
|
|
136
|
+
first_timestamp = pd.Timestamp(data['productionDates'][0])
|
|
137
|
+
last_timestamp = pd.Timestamp(data['productionDates'][-1])
|
|
138
|
+
if first_timestamp==pd.Timestamp(first_timestamp.year, 12, 31) and np.isnan(data['data'][0, -2, :, :]).min():
|
|
139
|
+
# Last day of the previous year with data on the last hour only... To remove.
|
|
140
|
+
data['data'] = data['data'][1:, :, :, :]
|
|
141
|
+
data['productionDates'] = data['productionDates'][1:]
|
|
142
|
+
warnings.warn('Last hour of the previous year dropped...')
|
|
143
|
+
|
|
144
|
+
if last_timestamp==pd.Timestamp(last_timestamp.year, 12, 31) and np.isnan(data['data'][-1, -1, :, :]).max():
|
|
145
|
+
if file_next_year is not None:
|
|
146
|
+
# Last day of the previous year with data missing on the last hour... To lead next year and take that value
|
|
147
|
+
data_last = _read_ERA5Land_helper(file_next_year, variable)
|
|
148
|
+
data['data'][-1, -1, :, :] = data_last['data'][0, -1, :, :]
|
|
149
|
+
warnings.warn('First hour of the following year added...')
|
|
150
|
+
|
|
151
|
+
if len(data['data'].shape)==4:
|
|
152
|
+
# (date, hour, ...) > (timestamp, ---)
|
|
153
|
+
data['data'] = np.diff(data['data'], n=1, axis=1, prepend=0)
|
|
154
|
+
else:
|
|
155
|
+
data['data'] = np.diff(data['data'], n=1, axis=0, prepend=0)
|
|
156
|
+
|
|
157
|
+
if variable == 'tp':
|
|
158
|
+
data['data'] *= 1000
|
|
159
|
+
units = 'mm/hr'
|
|
160
|
+
elif variable == 't2m':
|
|
161
|
+
data['data'] -= 273.15
|
|
162
|
+
units = 'C'
|
|
163
|
+
elif variable == 'sd':
|
|
164
|
+
#=======================================================================
|
|
165
|
+
# raise Exception('Cumulative?')
|
|
166
|
+
#=======================================================================
|
|
167
|
+
data['data'] *= 1000
|
|
168
|
+
units = 'mm'
|
|
169
|
+
elif variable == 'ssr':
|
|
170
|
+
data['data'] /= 3600
|
|
171
|
+
units = 'W/m2'
|
|
172
|
+
elif variable == 'u10' or variable == 'v10':
|
|
173
|
+
units = 'm/s'
|
|
174
|
+
|
|
175
|
+
#=======================================================================
|
|
176
|
+
# if variable == 'tp' or variable == 't2m':
|
|
177
|
+
# tmp = np.reshape(tmp, (tmp.shape[0]*tmp.shape[1], tmp.shape[2], tmp.shape[3]))
|
|
178
|
+
# times = np.tile(data['productionDates'], (24, 1)).transpose() + np.tile(ds.step.data, (data['productionDates'].shape[0], 1))
|
|
179
|
+
# data['productionDates'] = times.ravel()
|
|
180
|
+
#=======================================================================
|
|
181
|
+
|
|
182
|
+
if not isinstance(data['steps'], Iterable):
|
|
183
|
+
data['steps'] = [data['steps']]
|
|
184
|
+
|
|
185
|
+
if variable !='sd':
|
|
186
|
+
data['data'] = np.reshape(data['data'], (data['data'].shape[0]*data['data'].shape[1], data['data'].shape[-2], data['data'].shape[-1]))
|
|
187
|
+
|
|
188
|
+
times = np.tile(data['productionDates'], (24, 1)).transpose() + np.tile(data['steps'] - data['steps'][0], (data['productionDates'].shape[0], 1))
|
|
189
|
+
data['productionDates'] = times.ravel()
|
|
190
|
+
|
|
191
|
+
data['data'] = np.expand_dims(data['data'], [1, 2])
|
|
192
|
+
data['leadtimes'] = np.array([pd.DateOffset(days=0)])
|
|
193
|
+
|
|
194
|
+
tmp = MeteoRaster(data, units=units, variable=variable)
|
|
195
|
+
tmp.trim()
|
|
196
|
+
|
|
197
|
+
return tmp
|
|
198
|
+
|
|
199
|
+
def read_GFS(path, timestamp, leadtime, variable, crop=None):
|
|
200
|
+
'''
|
|
201
|
+
TMP
|
|
202
|
+
PRATE
|
|
203
|
+
WEASD
|
|
204
|
+
DPT
|
|
205
|
+
RH
|
|
206
|
+
SNOD
|
|
207
|
+
SUNSD
|
|
208
|
+
UGRD
|
|
209
|
+
VGRD
|
|
210
|
+
'''
|
|
211
|
+
|
|
212
|
+
GFS_info = dict(TMP=dict(units='C', variable='t2m', transform=lambda x: x),
|
|
213
|
+
PRATE=dict(units='mm/3h', variable='tp', transform=lambda x: x*3600*3), # kg/(m^2 s) > mm/3h
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
lead_hours = leadtime // np.timedelta64(1, 'h')
|
|
217
|
+
|
|
218
|
+
path_ = Path(path) / timestamp.strftime('%Y/%m') / '00' / ('%03u' % lead_hours)
|
|
219
|
+
file = path_ / 'gfs_4_{date:s}_00_{lead:03d}.nc'.format(date=timestamp.strftime('%Y.%m.%d'), lead=lead_hours)
|
|
220
|
+
|
|
221
|
+
data = {}
|
|
222
|
+
with xr.open_dataset(file) as ds:
|
|
223
|
+
data['latitudes'] = ds.lat.data
|
|
224
|
+
data['longitudes'] = ds.lon.data
|
|
225
|
+
data['productionDates'] = ds.productionDatetimes.data
|
|
226
|
+
data['leadtimes'] = np.array([pd.Timedelta(hours=int(ds.leadtimes.data/1000000000/3600))])
|
|
227
|
+
tmp = ds[variable][:, :, :, :].data
|
|
228
|
+
data['data'] = np.expand_dims(tmp, [1])
|
|
229
|
+
|
|
230
|
+
if GFS_info[variable]['transform']:
|
|
231
|
+
data['data'] = GFS_info[variable]['transform'](data['data'])
|
|
232
|
+
|
|
233
|
+
meteoraster = MeteoRaster(data, units=GFS_info[variable]['units'], variable=GFS_info[variable]['variable'])
|
|
234
|
+
if crop:
|
|
235
|
+
meteoraster = meteoraster.getCropped(**crop)
|
|
236
|
+
|
|
237
|
+
return meteoraster
|
|
238
|
+
|
|
239
|
+
def read_C3S(file, variable, engine='cfgrib', **kwargs):
|
|
240
|
+
'''
|
|
241
|
+
Reads c3s forecasts (seasonal forecast monthly statistics on single levels)
|
|
242
|
+
https://cds.climate.copernicus.eu/datasets/seasonal-monthly-single-levels?tab=overview
|
|
243
|
+
|
|
244
|
+
v0.1
|
|
245
|
+
2024.10.04
|
|
246
|
+
|
|
247
|
+
Return a dict:
|
|
248
|
+
data: 5-D numpy array [productionDate, ensembleMember, leadtime, latitude, longitude]
|
|
249
|
+
production dates: 1-D numpy array
|
|
250
|
+
leadtimes: 1-D numpy array
|
|
251
|
+
latitudes: 1-D numpy array
|
|
252
|
+
longitudes: 1-D numpy array
|
|
253
|
+
'''
|
|
254
|
+
|
|
255
|
+
data = {}
|
|
256
|
+
with xr.open_dataset(file, engine=engine, indexpath='', **kwargs) as ds:
|
|
257
|
+
data['latitudes'] = ds.latitude.data
|
|
258
|
+
data['longitudes'] = ds.longitude.data
|
|
259
|
+
data['productionDates'] = ds.time.data
|
|
260
|
+
data_ = ds[variable][:, :, :, :].data
|
|
261
|
+
steps_ = pd.to_timedelta([pd.Timedelta(f'{d}d') for d in (ds.step.data/86400/1000000000).astype(int)])
|
|
262
|
+
|
|
263
|
+
data_ = np.rollaxis(data_, 1, 0)
|
|
264
|
+
|
|
265
|
+
months_ = np.round(steps_.days/30).astype(int)
|
|
266
|
+
months_unique = np.unique(months_)
|
|
267
|
+
data['leadtimes'] = [pd.DateOffset(months=m-1) for m in months_unique]
|
|
268
|
+
|
|
269
|
+
data_shape = list(data_.shape)
|
|
270
|
+
data_shape[2] = len(months_unique)
|
|
271
|
+
data['data'] = np.empty(data_shape)*np.nan
|
|
272
|
+
for i0, m0 in enumerate(months_unique):
|
|
273
|
+
idxs = months_==m0
|
|
274
|
+
data['data'][:, :, i0, :, :] = np.nanmean(data_[:, :, idxs, :, :], axis=2)
|
|
275
|
+
|
|
276
|
+
if variable == 'tprate':
|
|
277
|
+
data['data'] *= 1000 # to mm/s
|
|
278
|
+
|
|
279
|
+
idx = pd.MultiIndex.from_product([data['productionDates'], data['leadtimes']],
|
|
280
|
+
names=['productionDates', 'leadtimes']).to_frame()
|
|
281
|
+
days_in_month = idx.sum(axis=1).dt.days_in_month.unstack('leadtimes')
|
|
282
|
+
seconds_in_month = days_in_month*86400
|
|
283
|
+
|
|
284
|
+
multiplier = np.expand_dims(seconds_in_month.values, (1, -2, -1))
|
|
285
|
+
tmp = data_shape.copy()
|
|
286
|
+
tmp[0] = 1
|
|
287
|
+
tmp[2] = 1
|
|
288
|
+
multiplier = np.tile(multiplier, tmp)
|
|
289
|
+
data['data'] *= multiplier
|
|
290
|
+
|
|
291
|
+
units = 'mm/month'
|
|
292
|
+
variable='tp'
|
|
293
|
+
elif variable == 't2m':
|
|
294
|
+
data['data'] -= 273.15
|
|
295
|
+
units = 'C'
|
|
296
|
+
|
|
297
|
+
tmp = MeteoRaster(data, units=units, variable=variable)
|
|
298
|
+
return(tmp)
|
|
299
|
+
|
|
300
|
+
def readCORDEX_monthly(file, variable, convention=pd.offsets.MonthBegin(), remove_time=True):
|
|
301
|
+
'''
|
|
302
|
+
Reads CORDEX CMIP 5 monthly data.
|
|
303
|
+
|
|
304
|
+
variable:
|
|
305
|
+
pr - precipitation
|
|
306
|
+
tas - air temperature near surface
|
|
307
|
+
|
|
308
|
+
convention (pandas offset):
|
|
309
|
+
pd.offsets.MonthBegin()
|
|
310
|
+
None - file default
|
|
311
|
+
'''
|
|
312
|
+
|
|
313
|
+
data = {}
|
|
314
|
+
with xr.open_dataset(file) as ds:
|
|
315
|
+
data['latitudes'] = ds.lat.data
|
|
316
|
+
data['longitudes'] = ds.lon.data
|
|
317
|
+
data['productionDates'] = ds.time.data
|
|
318
|
+
data['data'] = ds[variable][:, :, :].data
|
|
319
|
+
data['leadtimes'] = np.array([pd.DateOffset(days=0)])
|
|
320
|
+
|
|
321
|
+
if not convention is None:
|
|
322
|
+
data['productionDates'] = [d + convention for d in data['productionDates']]
|
|
323
|
+
if remove_time:
|
|
324
|
+
data['productionDates'] = [d.normalize() for d in data['productionDates']]
|
|
325
|
+
|
|
326
|
+
if variable=='tas':
|
|
327
|
+
units = 'C'
|
|
328
|
+
variable = 'Air temperature near surface'
|
|
329
|
+
elif variable=='pr':
|
|
330
|
+
units = 'mm/month'
|
|
331
|
+
variable = 'Precipitation'
|
|
332
|
+
else:
|
|
333
|
+
raise(Exception(f'How to handle variable "{variable}" must be defined.'))
|
|
334
|
+
|
|
335
|
+
data['data'] = np.expand_dims(data['data'], [1, 2])
|
|
336
|
+
|
|
337
|
+
tmp = MeteoRaster(data, units=units, variable=variable)
|
|
338
|
+
tmp.trim()
|
|
339
|
+
|
|
340
|
+
return tmp
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == '__main__':
|
|
344
|
+
|
|
345
|
+
import matplotlib.pyplot as plt
|
|
346
|
+
|
|
347
|
+
#===========================================================================
|
|
348
|
+
# file = Path(r'C:\Users\zepedro\Documents\GitHub\Climate-change\data\ERA5 land') / 'pr_MOROCCO_ERA5Land.grib'
|
|
349
|
+
# era5 = readERA5Land_monthly(file, 'tp')
|
|
350
|
+
# data = era5.plot_mean(coastline=True, borders=False, colorbar=True,
|
|
351
|
+
# colorbar_label=f'[{era5.units}]', cmap='viridis', central_longitude=20, central_latitude=30)
|
|
352
|
+
#===========================================================================
|
|
353
|
+
|
|
354
|
+
#===========================================================================
|
|
355
|
+
# file = Path(r'C:\Users\zepedro\Documents\GitHub\Climate-change\data\ERA5 land') / 'tas_MOROCCO_ERA5Land.grib'
|
|
356
|
+
# era5 = readERA5Land_monthly(file, 't2m')
|
|
357
|
+
# data = era5.plot_mean(coastline=True, borders=False, colorbar=True,
|
|
358
|
+
# colorbar_label=f'[{era5.units}]', cmap='magma', central_longitude=20, central_latitude=30)
|
|
359
|
+
#===========================================================================
|