pyVPRM 3.0__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.
- pyVPRM/VPRM.py +1118 -0
- pyVPRM/__init__.py +1 -0
- pyVPRM/lib/__init__.py +0 -0
- pyVPRM/lib/downmodis.py +1012 -0
- pyVPRM/lib/fancy_plot.py +75 -0
- pyVPRM/lib/flux_tower_class.py +444 -0
- pyVPRM/lib/fluxnet_info/fluxnet_sites.pkl +0 -0
- pyVPRM/lib/fluxnet_info/site_infos.txt +218 -0
- pyVPRM/lib/functions.py +471 -0
- pyVPRM/meteorologies/__init__.py +0 -0
- pyVPRM/meteorologies/era5_class_dkrz.py +279 -0
- pyVPRM/meteorologies/era5_class_draft.py +60 -0
- pyVPRM/meteorologies/era5_monthly_xr.py +149 -0
- pyVPRM/meteorologies/met_base_class.py +67 -0
- pyVPRM/meteorologies/met_local_measurement.py +56 -0
- pyVPRM/sat_managers/__init__.py +0 -0
- pyVPRM/sat_managers/base_manager.py +430 -0
- pyVPRM/sat_managers/city.py +21 -0
- pyVPRM/sat_managers/copernicus.py +29 -0
- pyVPRM/sat_managers/esa_world_cover.py +21 -0
- pyVPRM/sat_managers/mapbiomas.py +35 -0
- pyVPRM/sat_managers/modis.py +266 -0
- pyVPRM/sat_managers/proba_v.py +86 -0
- pyVPRM/sat_managers/sentinel2.py +192 -0
- pyVPRM/sat_managers/synmap.py +21 -0
- pyVPRM/sat_managers/viirs.py +232 -0
- pyVPRM/sat_managers/viirs09ga.py +173 -0
- pyVPRM/vprm_configs/__init__.py +0 -0
- pyVPRM/vprm_configs/copernicus_land_cover.yaml +94 -0
- pyVPRM/vprm_configs/esa_world_cover.yaml +71 -0
- pyVPRM/vprm_configs/synmap.yaml +106 -0
- pyVPRM/vprm_models/__init__.py +1 -0
- pyVPRM/vprm_models/model_params/__init__.py +0 -0
- pyVPRM/vprm_models/vprm_base.py +691 -0
- pyVPRM/vprm_models/vprm_base_no_xeric.py +585 -0
- pyVPRM/vprm_models/vprm_modified.py +530 -0
- pyVPRM/vprm_models/vprm_nn.py +130 -0
- pyVPRM-3.0.dist-info/LICENSE +21 -0
- pyVPRM-3.0.dist-info/METADATA +40 -0
- pyVPRM-3.0.dist-info/RECORD +42 -0
- pyVPRM-3.0.dist-info/WHEEL +5 -0
- pyVPRM-3.0.dist-info/top_level.txt +1 -0
pyVPRM/VPRM.py
ADDED
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
3
|
+
warnings.filterwarnings("ignore")
|
|
4
|
+
import sys
|
|
5
|
+
import os
|
|
6
|
+
import pathlib
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pyVPRM
|
|
9
|
+
from pyVPRM.sat_managers.base_manager import satellite_data_manager
|
|
10
|
+
from pyVPRM.lib.functions import (
|
|
11
|
+
add_corners_to_1d_grid,
|
|
12
|
+
do_lowess_smoothing,
|
|
13
|
+
make_xesmf_grid,
|
|
14
|
+
to_esmf_grid,
|
|
15
|
+
)
|
|
16
|
+
from scipy.ndimage import uniform_filter
|
|
17
|
+
from pyproj import Transformer
|
|
18
|
+
import copy
|
|
19
|
+
from joblib import Parallel, delayed
|
|
20
|
+
import xarray as xr
|
|
21
|
+
import scipy
|
|
22
|
+
import uuid
|
|
23
|
+
import time
|
|
24
|
+
from scipy.optimize import curve_fit
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import datetime
|
|
27
|
+
from dateutil import parser
|
|
28
|
+
from multiprocessing import Process
|
|
29
|
+
import rasterio
|
|
30
|
+
from astropy.convolution import convolve
|
|
31
|
+
from datetime import datetime, timedelta
|
|
32
|
+
import yaml
|
|
33
|
+
from loguru import logger
|
|
34
|
+
|
|
35
|
+
regridder_options = dict()
|
|
36
|
+
regridder_options["conservative"] = "conserve"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class vprm:
|
|
40
|
+
"""
|
|
41
|
+
Class for the Vegetation Photosynthesis and Respiration Model
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self, vprm_config_path, land_cover_map=None, verbose=False, n_cpus=1, sites=None
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initialize a class instance
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
land_cover_map (xarray): A pre calculated map with the land cover types
|
|
52
|
+
verbose (bool): Set true for additional output when debugging
|
|
53
|
+
n_cpus: Number of CPUs
|
|
54
|
+
sites: For fitting. Provide a list of sites.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The lowess smoothed array
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
logger.info("Running with pyVPRM version {}".format(pyVPRM.__version__))
|
|
61
|
+
self.sat_imgs = []
|
|
62
|
+
|
|
63
|
+
self.sites = sites
|
|
64
|
+
if self.sites is not None:
|
|
65
|
+
self.lonlats = [i.get_lonlat() for i in sites]
|
|
66
|
+
self.n_cpus = n_cpus
|
|
67
|
+
self.counter = 0
|
|
68
|
+
self.fit_params_dict = None
|
|
69
|
+
self.res = None
|
|
70
|
+
|
|
71
|
+
self.new = True
|
|
72
|
+
self.timestamps = []
|
|
73
|
+
self.t2m = None
|
|
74
|
+
|
|
75
|
+
# self.target_shape = None
|
|
76
|
+
|
|
77
|
+
self.sat_img_buffer = dict()
|
|
78
|
+
self.buffer = dict()
|
|
79
|
+
self.buffer["cur_lat"] = None
|
|
80
|
+
self.buffer["cur_lon"] = None
|
|
81
|
+
self.prototype_lat_lon = None
|
|
82
|
+
|
|
83
|
+
self.land_cover_type = land_cover_map
|
|
84
|
+
# land_cover_type: tmin, topt, tmax
|
|
85
|
+
|
|
86
|
+
with open(vprm_config_path, "r") as stream:
|
|
87
|
+
try:
|
|
88
|
+
cfg = yaml.safe_load(stream)
|
|
89
|
+
except yaml.YAMLError as exc:
|
|
90
|
+
logger.info(exc)
|
|
91
|
+
|
|
92
|
+
self.temp_coefficients = dict()
|
|
93
|
+
self.map_to_vprm_class = dict()
|
|
94
|
+
for key in cfg:
|
|
95
|
+
if "tmin" in cfg[key].keys():
|
|
96
|
+
self.temp_coefficients[cfg[key]["vprm_class"]] = [
|
|
97
|
+
cfg[key]["tmin"],
|
|
98
|
+
cfg[key]["topt"],
|
|
99
|
+
cfg[key]["tmax"],
|
|
100
|
+
cfg[key]["tlow"],
|
|
101
|
+
]
|
|
102
|
+
for c in cfg[key]["class_numbers"]:
|
|
103
|
+
self.map_to_vprm_class[c] = cfg[key]["vprm_class"]
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
def to_wrf_output(
|
|
107
|
+
self,
|
|
108
|
+
out_grid,
|
|
109
|
+
weights_for_regridder=None,
|
|
110
|
+
regridder_save_path=None,
|
|
111
|
+
driver="xEMSF",
|
|
112
|
+
interp_method="conservative",
|
|
113
|
+
n_cpus=None,
|
|
114
|
+
mpi=True,
|
|
115
|
+
logs=False,
|
|
116
|
+
):
|
|
117
|
+
"""
|
|
118
|
+
Generate output in the format that can be used as an input for WRF
|
|
119
|
+
|
|
120
|
+
Parameters:
|
|
121
|
+
out_grid (dict or xarray): Can be either a dictionary with 1D lats and lons
|
|
122
|
+
or an xarray dataset
|
|
123
|
+
weights_for_regridder (str): Weights to be used for regridding to the WRF grid
|
|
124
|
+
regridder_save_path (str): Save path when generating a new regridder
|
|
125
|
+
driver (str): Either ESMF_RegridWeightGen or xESMF. When setting to ESMF_RegridWeightGen
|
|
126
|
+
the ESMF library is called directly
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Dictionary with a dictinoary of the WRF input arrays
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
import xesmf as xe
|
|
133
|
+
|
|
134
|
+
if n_cpus is None:
|
|
135
|
+
n_cpus = self.n_cpus
|
|
136
|
+
|
|
137
|
+
src_grid = make_xesmf_grid(self.sat_imgs.sat_img)
|
|
138
|
+
if isinstance(out_grid, dict):
|
|
139
|
+
ds_out = make_xesmf_grid(out_grid)
|
|
140
|
+
else:
|
|
141
|
+
ds_out = out_grid
|
|
142
|
+
|
|
143
|
+
if weights_for_regridder is None:
|
|
144
|
+
logger.info(
|
|
145
|
+
"Need to generate the weights for the regridder. This can be very slow and memory intensive"
|
|
146
|
+
)
|
|
147
|
+
if driver == "xEMSF":
|
|
148
|
+
regridder = xe.Regridder(src_grid, ds_out, interp_method)
|
|
149
|
+
if regridder_save_path is not None:
|
|
150
|
+
regridder.to_netcdf(regridder_save_path)
|
|
151
|
+
elif driver == "ESMF_RegridWeightGen":
|
|
152
|
+
if regridder_save_path is None:
|
|
153
|
+
logger.info(
|
|
154
|
+
"If you use ESMF_RegridWeightGen, a regridder_save_path needs to be given"
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
src_temp_path = os.path.join(
|
|
158
|
+
os.path.dirname(regridder_save_path),
|
|
159
|
+
"{}.nc".format(str(uuid.uuid4())),
|
|
160
|
+
)
|
|
161
|
+
dest_temp_path = os.path.join(
|
|
162
|
+
os.path.dirname(regridder_save_path),
|
|
163
|
+
"{}.nc".format(str(uuid.uuid4())),
|
|
164
|
+
)
|
|
165
|
+
src_grid_esmf = to_esmf_grid(self.sat_imgs.sat_img)
|
|
166
|
+
ds_out_esmf = to_esmf_grid(out_grid)
|
|
167
|
+
src_grid_esmf.to_netcdf(src_temp_path)
|
|
168
|
+
ds_out_esmf.to_netcdf(dest_temp_path)
|
|
169
|
+
exec_str = "ESMF_RegridWeightGen --source {} --destination {} --weight {} -m {} -r --netcdf4 –src_regional –dest_regional ".format(
|
|
170
|
+
src_temp_path,
|
|
171
|
+
dest_temp_path,
|
|
172
|
+
regridder_save_path,
|
|
173
|
+
regridder_options[interp_method],
|
|
174
|
+
)
|
|
175
|
+
if mpi is True:
|
|
176
|
+
exec_str = "mpirun -np {} ".format(n_cpus) + exec_str
|
|
177
|
+
if not logs:
|
|
178
|
+
exec_str += " --no_log "
|
|
179
|
+
logger.info(exec_str)
|
|
180
|
+
os.system(exec_str) # --no_log
|
|
181
|
+
# os.remove(src_temp_path)
|
|
182
|
+
# os.remove(dest_temp_path)
|
|
183
|
+
weights_for_regridder = regridder_save_path
|
|
184
|
+
else:
|
|
185
|
+
logger.info("Driver needs to be xEMSF or ESMF_RegridWeightGen")
|
|
186
|
+
if weights_for_regridder is not None:
|
|
187
|
+
regridder = xe.Regridder(
|
|
188
|
+
src_grid,
|
|
189
|
+
ds_out,
|
|
190
|
+
interp_method,
|
|
191
|
+
weights=weights_for_regridder,
|
|
192
|
+
reuse_weights=True,
|
|
193
|
+
)
|
|
194
|
+
veg_inds = np.unique(
|
|
195
|
+
[self.map_to_vprm_class[i] for i in self.map_to_vprm_class.keys()]
|
|
196
|
+
)
|
|
197
|
+
veg_inds = np.array(veg_inds, dtype=np.int32)
|
|
198
|
+
dims = [i for i in list(ds_out.dims.mapping.keys()) if "_b" not in i]
|
|
199
|
+
lcm = regridder(self.land_cover_type.sat_img)
|
|
200
|
+
lcm = lcm.to_dataset(name="vegetation_fraction_map")
|
|
201
|
+
lcm = lcm.rename({"y": "south_north", "x": "west_east"})
|
|
202
|
+
day_of_the_year = np.array(
|
|
203
|
+
self.sat_imgs.sat_img[self.time_key].values, dtype=np.int32
|
|
204
|
+
)
|
|
205
|
+
day_of_the_year += 1 - day_of_the_year[0]
|
|
206
|
+
kys = len(self.sat_imgs.sat_img[self.time_key].values)
|
|
207
|
+
final_array = []
|
|
208
|
+
for ky in range(kys):
|
|
209
|
+
sub_array = []
|
|
210
|
+
for v in veg_inds:
|
|
211
|
+
tres = self.sat_imgs.sat_img.isel({self.time_key: ky})["evi"].where(
|
|
212
|
+
self.land_cover_type.sat_img.sel({"vprm_classes": v}) > 0, np.nan
|
|
213
|
+
)
|
|
214
|
+
sub_array.append(regridder(tres.values, skipna=True))
|
|
215
|
+
final_array.append(sub_array)
|
|
216
|
+
out_dims = ["vprm_classes", "time"]
|
|
217
|
+
out_dims.extend(dims)
|
|
218
|
+
ds_t_evi = copy.deepcopy(ds_out)
|
|
219
|
+
ds_t_evi = ds_t_evi.assign({"evi": (out_dims, np.moveaxis(final_array, 0, 1))})
|
|
220
|
+
ds_t_evi = ds_t_evi.assign_coords({"time": day_of_the_year})
|
|
221
|
+
ds_t_evi = ds_t_evi.assign_coords({"vprm_classes": veg_inds})
|
|
222
|
+
ds_t_evi = ds_t_evi.rename({"y": "south_north", "x": "west_east"})
|
|
223
|
+
|
|
224
|
+
final_array = []
|
|
225
|
+
for ky in range(kys):
|
|
226
|
+
sub_array = []
|
|
227
|
+
for v in veg_inds:
|
|
228
|
+
tres = self.sat_imgs.sat_img.isel({self.time_key: ky})["lswi"].where(
|
|
229
|
+
self.land_cover_type.sat_img.sel({"vprm_classes": v}) > 0, np.nan
|
|
230
|
+
)
|
|
231
|
+
sub_array.append(regridder(tres.values, skipna=True))
|
|
232
|
+
final_array.append(sub_array)
|
|
233
|
+
ds_t_lswi = copy.deepcopy(ds_out)
|
|
234
|
+
ds_t_lswi = ds_t_lswi.assign(
|
|
235
|
+
{"lswi": (out_dims, np.moveaxis(final_array, 0, 1))}
|
|
236
|
+
)
|
|
237
|
+
ds_t_lswi = ds_t_lswi.assign_coords({"time": day_of_the_year})
|
|
238
|
+
ds_t_lswi = ds_t_lswi.assign_coords({"vprm_classes": veg_inds})
|
|
239
|
+
ds_t_lswi = ds_t_lswi.rename({"y": "south_north", "x": "west_east"})
|
|
240
|
+
|
|
241
|
+
out_dims = ["vprm_classes"]
|
|
242
|
+
out_dims.extend(dims)
|
|
243
|
+
ds_t_max_evi = copy.deepcopy(ds_out)
|
|
244
|
+
ds_t_max_evi = ds_t_max_evi.assign(
|
|
245
|
+
{"evi_max": (out_dims, np.nanmax(ds_t_evi["evi"], axis=1))}
|
|
246
|
+
)
|
|
247
|
+
ds_t_max_evi = ds_t_max_evi.assign_coords({"vprm_classes": veg_inds})
|
|
248
|
+
ds_t_max_evi = ds_t_max_evi.rename({"y": "south_north", "x": "west_east"})
|
|
249
|
+
|
|
250
|
+
ds_t_min_evi = copy.deepcopy(ds_out)
|
|
251
|
+
ds_t_min_evi = ds_t_min_evi.assign(
|
|
252
|
+
{"evi_min": (out_dims, np.nanmin(ds_t_evi["evi"], axis=1))}
|
|
253
|
+
)
|
|
254
|
+
ds_t_min_evi = ds_t_min_evi.assign_coords({"vprm_classes": veg_inds})
|
|
255
|
+
ds_t_min_evi = ds_t_min_evi.rename({"y": "south_north", "x": "west_east"})
|
|
256
|
+
|
|
257
|
+
ds_t_max_lswi = copy.deepcopy(ds_out)
|
|
258
|
+
ds_t_max_lswi = ds_t_max_lswi.assign(
|
|
259
|
+
{"lswi_max": (out_dims, np.nanmax(ds_t_lswi["lswi"], axis=1))}
|
|
260
|
+
)
|
|
261
|
+
ds_t_max_lswi = ds_t_max_lswi.assign_coords({"vprm_classes": veg_inds})
|
|
262
|
+
ds_t_max_lswi = ds_t_max_lswi.rename({"y": "south_north", "x": "west_east"})
|
|
263
|
+
|
|
264
|
+
ds_t_min_lswi = copy.deepcopy(ds_out)
|
|
265
|
+
ds_t_min_lswi = ds_t_min_lswi.assign(
|
|
266
|
+
{"lswi_min": (out_dims, np.nanmin(ds_t_lswi["lswi"], axis=1))}
|
|
267
|
+
)
|
|
268
|
+
ds_t_min_lswi = ds_t_min_lswi.assign_coords({"vprm_classes": veg_inds})
|
|
269
|
+
ds_t_min_lswi = ds_t_min_lswi.rename({"y": "south_north", "x": "west_east"})
|
|
270
|
+
|
|
271
|
+
ret_dict = {
|
|
272
|
+
"lswi": ds_t_lswi,
|
|
273
|
+
"evi": ds_t_evi,
|
|
274
|
+
"veg_fraction": lcm,
|
|
275
|
+
"lswi_max": ds_t_max_lswi,
|
|
276
|
+
"lswi_min": ds_t_min_lswi,
|
|
277
|
+
"evi_max": ds_t_max_evi,
|
|
278
|
+
"evi_min": ds_t_min_evi,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for key in ret_dict.keys():
|
|
282
|
+
ret_dict[key] = ret_dict[key].assign_attrs(
|
|
283
|
+
title="VPRM input data for WRF: {}".format(key),
|
|
284
|
+
# MODIS_version = '061',
|
|
285
|
+
software_version=pyVPRM.__version__,
|
|
286
|
+
software_github="https://github.com/tglauch/pyVPRM",
|
|
287
|
+
author="Dr. Theo Glauch",
|
|
288
|
+
institution1="Heidelberg University",
|
|
289
|
+
institution2="Deutsches Zentrum für Luft- und Raumfahrt (DLR)",
|
|
290
|
+
contact="theo.glauch@dlr.de",
|
|
291
|
+
date_created=str(datetime.now()),
|
|
292
|
+
comment="Used VPRM classes: 1 Evergreen forest, 2 Deciduous forest, 3 Mixed forest, 4 Shrubland, 5 Trees and grasses, 6 Cropland, 7 Grassland, 8 Barren, Urban and built-up, water, permanent snow and ice",
|
|
293
|
+
)
|
|
294
|
+
return ret_dict
|
|
295
|
+
|
|
296
|
+
def add_sat_img(
|
|
297
|
+
self,
|
|
298
|
+
handler,
|
|
299
|
+
b_nir=None,
|
|
300
|
+
b_red=None,
|
|
301
|
+
b_blue=None,
|
|
302
|
+
b_swir=None,
|
|
303
|
+
drop_bands=False,
|
|
304
|
+
which_evi=None,
|
|
305
|
+
timestamp_key=None,
|
|
306
|
+
mask_bad_pixels=True,
|
|
307
|
+
mask_clouds=True,
|
|
308
|
+
mask_snow=True,
|
|
309
|
+
):
|
|
310
|
+
"""
|
|
311
|
+
Add a new satellite image and calculate EVI and LSWI if desired
|
|
312
|
+
|
|
313
|
+
Parameters:
|
|
314
|
+
handler (satellite_data_manager): The satellite image
|
|
315
|
+
b_nir (str): Name of the near-infrared band
|
|
316
|
+
b_red (str): Name of the red band
|
|
317
|
+
b_blue (str): Name of the blue band
|
|
318
|
+
b_swir (str): Name of the short-wave infrared band
|
|
319
|
+
drop_bands (bool): If True drop the raw band information after
|
|
320
|
+
calculation of EVI and LSWI. Saves memory.
|
|
321
|
+
Can also be a list of keys to drop.
|
|
322
|
+
which_evi (str): Either evi or evi2. evi2 does not need a blue band.
|
|
323
|
+
timestamp_key (float): satellite data key containing a timestamp for each
|
|
324
|
+
single pixel - to be used with lowess
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
None
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
evi_params = {"g": 2.5, "c1": 6.0, "c2": 7.5, "l": 1}
|
|
331
|
+
evi2_params = {"g": 2.5, "l": 1, "c": 2.4}
|
|
332
|
+
|
|
333
|
+
if not isinstance(handler, satellite_data_manager):
|
|
334
|
+
logger.info(
|
|
335
|
+
"Satellite image needs to be an object of the satellite_data_manager class"
|
|
336
|
+
)
|
|
337
|
+
return
|
|
338
|
+
bands_to_mask = []
|
|
339
|
+
bands = [b_nir, b_red, b_blue, b_swir]
|
|
340
|
+
if which_evi == 'evi2':
|
|
341
|
+
bands = [b_nir, b_red, b_swir]
|
|
342
|
+
for btm in bands:
|
|
343
|
+
if btm is not None:
|
|
344
|
+
bands_to_mask.append(btm)
|
|
345
|
+
if mask_bad_pixels:
|
|
346
|
+
if bands_to_mask == []:
|
|
347
|
+
handler.mask_bad_pixels()
|
|
348
|
+
else:
|
|
349
|
+
handler.mask_bad_pixels(bands_to_mask)
|
|
350
|
+
|
|
351
|
+
if which_evi in ["evi", "evi2"]:
|
|
352
|
+
nir = handler.sat_img[b_nir]
|
|
353
|
+
red = handler.sat_img[b_red]
|
|
354
|
+
swir = handler.sat_img[b_swir]
|
|
355
|
+
if which_evi == "evi":
|
|
356
|
+
blue = handler.sat_img[b_blue]
|
|
357
|
+
temp_evi = (
|
|
358
|
+
evi_params["g"]
|
|
359
|
+
* (nir - red)
|
|
360
|
+
/ (
|
|
361
|
+
nir
|
|
362
|
+
+ evi_params["c1"] * red
|
|
363
|
+
- evi_params["c2"] * blue
|
|
364
|
+
+ evi_params["l"]
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
elif which_evi == "evi2":
|
|
368
|
+
temp_evi = (
|
|
369
|
+
evi2_params["g"]
|
|
370
|
+
* (nir - red)
|
|
371
|
+
/ (nir + evi2_params["c"] * red + evi2_params["l"])
|
|
372
|
+
)
|
|
373
|
+
temp_evi = xr.where((temp_evi <= 0) | (temp_evi > 1), np.nan, temp_evi)
|
|
374
|
+
temp_lswi = (nir - swir) / (nir + swir)
|
|
375
|
+
temp_lswi = xr.where((temp_lswi < -1) | (temp_lswi > 1), np.nan, temp_lswi)
|
|
376
|
+
handler.sat_img["evi"] = temp_evi
|
|
377
|
+
handler.sat_img["lswi"] = temp_lswi
|
|
378
|
+
if timestamp_key is not None:
|
|
379
|
+
handler.sat_img = handler.sat_img.rename({timestamp_key: "timestamps"})
|
|
380
|
+
|
|
381
|
+
bands_to_mask = []
|
|
382
|
+
if which_evi in ["evi", "evi2"]:
|
|
383
|
+
bands_to_mask = ["evi", "lswi"]
|
|
384
|
+
else:
|
|
385
|
+
for btm in [b_nir, b_red, b_blue, b_swir]:
|
|
386
|
+
if btm is not None:
|
|
387
|
+
bands_to_mask.append(btm)
|
|
388
|
+
if mask_snow:
|
|
389
|
+
if bands_to_mask == []:
|
|
390
|
+
handler.mask_snow()
|
|
391
|
+
else:
|
|
392
|
+
handler.mask_snow(bands_to_mask)
|
|
393
|
+
if mask_clouds:
|
|
394
|
+
if bands_to_mask == []:
|
|
395
|
+
handler.mask_clouds()
|
|
396
|
+
else:
|
|
397
|
+
handler.mask_clouds(bands_to_mask)
|
|
398
|
+
if drop_bands:
|
|
399
|
+
if isinstance(drop_bands, list):
|
|
400
|
+
drop_keys = drop_bands
|
|
401
|
+
handler.sat_img = handler.sat_img.drop(drop_keys)
|
|
402
|
+
else:
|
|
403
|
+
handler.drop_bands()
|
|
404
|
+
self.sat_imgs.append(handler)
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
def smearing(self, keys, kernel, sat_img=None, lonlats=None):
|
|
408
|
+
"""
|
|
409
|
+
By default performs a spatial smearing on the list of pre-loaded satellite images.
|
|
410
|
+
If sat_img is given the smearing is performed on that specific image.
|
|
411
|
+
|
|
412
|
+
Parameters:
|
|
413
|
+
kernel (tuple): The extension of the spatial smoothing
|
|
414
|
+
lonlats (str): If given the smearing is only performed at the
|
|
415
|
+
given lats and lons
|
|
416
|
+
keys (list): keys for the smoothign of the satellite images
|
|
417
|
+
Returns:
|
|
418
|
+
None
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
if isinstance(kernel, tuple):
|
|
422
|
+
arsz = int(3 * np.max(kernel))
|
|
423
|
+
kernel = np.expand_dims(
|
|
424
|
+
np.ones(shape=kernel) / np.sum(np.ones(shape=kernel)), 0
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
kernel = np.expand_dims(kernel.array, 0)
|
|
428
|
+
arsz = int(3 * np.max(np.shape(kernel)))
|
|
429
|
+
if lonlats is None:
|
|
430
|
+
for key in keys:
|
|
431
|
+
self.sat_imgs.sat_img[key][:, :] = convolve(
|
|
432
|
+
self.sat_imgs.sat_img[key].values[:, :, :],
|
|
433
|
+
kernel=kernel,
|
|
434
|
+
preserve_nan=True,
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
t = Transformer.from_crs(
|
|
438
|
+
"+proj=longlat +datum=WGS84", self.sat_imgs.sat_img.rio.crs
|
|
439
|
+
)
|
|
440
|
+
xs = self.sat_imgs.sat_img.coords["x"].values
|
|
441
|
+
ys = self.sat_imgs.sat_img.coords["y"].values
|
|
442
|
+
for ll in lonlats:
|
|
443
|
+
x, y = t.transform(ll[0], ll[1])
|
|
444
|
+
x_ind = np.argmin(np.abs(x - xs))
|
|
445
|
+
y_ind = np.argmin(np.abs(y - ys))
|
|
446
|
+
for key in keys:
|
|
447
|
+
logger.info(key)
|
|
448
|
+
self.sat_imgs.sat_img[key][
|
|
449
|
+
:, y_ind - arsz : y_ind + arsz, x_ind - arsz : x_ind + arsz
|
|
450
|
+
] = convolve(
|
|
451
|
+
self.sat_imgs.sat_img[key][
|
|
452
|
+
:, y_ind - arsz : y_ind + arsz, x_ind - arsz : x_ind + arsz
|
|
453
|
+
],
|
|
454
|
+
kernel=kernel,
|
|
455
|
+
preserve_nan=True,
|
|
456
|
+
)
|
|
457
|
+
return
|
|
458
|
+
|
|
459
|
+
def reduce_along_lat_lon(self):
|
|
460
|
+
self.sat_imgs.reduce_along_lon_lat(
|
|
461
|
+
lon=[i[0] for i in self.lonlats],
|
|
462
|
+
lat=[i[1] for i in self.lonlats],
|
|
463
|
+
new_dim_name="site_names",
|
|
464
|
+
interp_method="nearest",
|
|
465
|
+
)
|
|
466
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign_coords(
|
|
467
|
+
{"site_names": [i.get_site_name() for i in self.sites]}
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
def sort_and_merge_by_timestamp(self):
|
|
471
|
+
"""
|
|
472
|
+
Called after adding the satellite images with 'add_sat_img'. Sorts the satellite
|
|
473
|
+
images by timestamp and merges everything to one satellite_data_manager.
|
|
474
|
+
|
|
475
|
+
Parameters:
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
None
|
|
479
|
+
"""
|
|
480
|
+
if self.sites is None:
|
|
481
|
+
x_time_y = 0
|
|
482
|
+
for h in self.sat_imgs:
|
|
483
|
+
size_dict = dict(h.sat_img.sizes)
|
|
484
|
+
prod = np.prod([size_dict[i] for i in size_dict.keys()])
|
|
485
|
+
if prod > x_time_y:
|
|
486
|
+
biggest = h
|
|
487
|
+
x_time_y = prod
|
|
488
|
+
self.prototype = copy.deepcopy(biggest)
|
|
489
|
+
keys = list(self.prototype.sat_img.keys())
|
|
490
|
+
self.prototype.sat_img = self.prototype.sat_img.drop(keys)
|
|
491
|
+
# for h in self.sat_imgs:
|
|
492
|
+
# h.sat_img = h.sat_img.rio.reproject_match(self.prototype.sat_img, nodata=np.nan)
|
|
493
|
+
else:
|
|
494
|
+
self.prototype = copy.deepcopy(self.sat_imgs[0])
|
|
495
|
+
keys = list(self.prototype.sat_img.keys())
|
|
496
|
+
self.prototype.sat_img = self.prototype.sat_img.drop(keys)
|
|
497
|
+
self.sat_imgs = satellite_data_manager(
|
|
498
|
+
sat_img=xr.concat([k.sat_img for k in self.sat_imgs], "time")
|
|
499
|
+
)
|
|
500
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.sortby(self.sat_imgs.sat_img.time)
|
|
501
|
+
self.timestamps = self.sat_imgs.sat_img.time
|
|
502
|
+
self.timestamps = np.array(
|
|
503
|
+
[pd.Timestamp(i).to_pydatetime() for i in self.timestamps.values]
|
|
504
|
+
)
|
|
505
|
+
self.timestamp_start = self.timestamps[0]
|
|
506
|
+
self.timestamp_end = self.timestamps[-1]
|
|
507
|
+
self.tot_num_days = (self.timestamp_end - self.timestamp_start).days
|
|
508
|
+
logger.info(
|
|
509
|
+
"Loaded data from {} to {}".format(self.timestamp_start, self.timestamp_end)
|
|
510
|
+
)
|
|
511
|
+
day_steps = [i.days for i in (self.timestamps - self.timestamp_start)]
|
|
512
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign_coords({"time": day_steps})
|
|
513
|
+
self.prototype.sat_img = self.prototype.sat_img.assign_coords(
|
|
514
|
+
{"time": day_steps}
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if "timestamps" in list(self.sat_imgs.sat_img.keys()):
|
|
518
|
+
tismp = np.round(
|
|
519
|
+
np.array(
|
|
520
|
+
(
|
|
521
|
+
self.sat_imgs.sat_img["timestamps"].values
|
|
522
|
+
- np.datetime64(self.timestamp_start)
|
|
523
|
+
)
|
|
524
|
+
/ 1e9
|
|
525
|
+
/ (24 * 60 * 60),
|
|
526
|
+
dtype=float,
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
dims = list(self.sat_imgs.sat_img.data_vars["timestamps"].dims)
|
|
530
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
|
|
531
|
+
{"timestamps": (dims, tismp)}
|
|
532
|
+
)
|
|
533
|
+
self.time_key = "time"
|
|
534
|
+
if "evi" in list(self.sat_imgs.sat_img.data_vars):
|
|
535
|
+
self.sat_imgs.sat_img["evi"] = xr.where(
|
|
536
|
+
(self.sat_imgs.sat_img["evi"] == np.inf),
|
|
537
|
+
self.sat_imgs.sat_img["evi"].min(dim=self.time_key),
|
|
538
|
+
self.sat_imgs.sat_img["evi"],
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if "lswi" in list(self.sat_imgs.sat_img.data_vars):
|
|
542
|
+
self.sat_imgs.sat_img["lswi"] = xr.where(
|
|
543
|
+
(self.sat_imgs.sat_img["lswi"] == np.inf),
|
|
544
|
+
self.sat_imgs.sat_img["lswi"].min(dim=self.time_key),
|
|
545
|
+
self.sat_imgs.sat_img["lswi"],
|
|
546
|
+
)
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
def clip_to_box(self, sat_to_crop):
|
|
550
|
+
bounds = sat_to_crop.sat_img.rio.bounds()
|
|
551
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.rio.clip_box(
|
|
552
|
+
bounds[0], bounds[1], bounds[2], bounds[3]
|
|
553
|
+
)
|
|
554
|
+
keys = list(self.sat_imgs.sat_img.keys())
|
|
555
|
+
self.prototype = satellite_data_manager(
|
|
556
|
+
sat_img=self.sat_imgs.sat_img.drop(keys)
|
|
557
|
+
)
|
|
558
|
+
return
|
|
559
|
+
|
|
560
|
+
def add_land_cover_map(
|
|
561
|
+
self,
|
|
562
|
+
land_cover_map,
|
|
563
|
+
var_name="band_1",
|
|
564
|
+
save_path=None,
|
|
565
|
+
filter_size=None,
|
|
566
|
+
mode="fractional",
|
|
567
|
+
regridder_save_path=None,
|
|
568
|
+
n_cpus=None,
|
|
569
|
+
mpi=True,
|
|
570
|
+
logs=False,
|
|
571
|
+
):
|
|
572
|
+
"""
|
|
573
|
+
Add the land cover map. Either use a pre-calculated one or do the calculation on the fly.
|
|
574
|
+
|
|
575
|
+
Parameters:
|
|
576
|
+
land_cover_map (str or satimg instance): The input land cover map.
|
|
577
|
+
If string, assume it's a pre-generated map
|
|
578
|
+
var_name (str): Name of the land_cover_band in the xarray dataset
|
|
579
|
+
save_path (str): Path to save the map. Can be useful for re-using
|
|
580
|
+
filter_size (int): Number of pixels from which the land cover type is aggregated.
|
|
581
|
+
Returns:
|
|
582
|
+
None
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
if n_cpus is None:
|
|
586
|
+
n_cpus = self.n_cpus
|
|
587
|
+
if isinstance(land_cover_map, str):
|
|
588
|
+
logger.info("Load pre-generated land cover map: {}".format(land_cover_map))
|
|
589
|
+
self.land_cover_type = satellite_data_manager(sat_img=land_cover_map)
|
|
590
|
+
else:
|
|
591
|
+
logger.info("Generating satellite data compatible land cover map")
|
|
592
|
+
|
|
593
|
+
for key in self.map_to_vprm_class.keys():
|
|
594
|
+
land_cover_map.sat_img[var_name] = xr.where(
|
|
595
|
+
land_cover_map.sat_img[var_name] == key,
|
|
596
|
+
self.map_to_vprm_class[key],
|
|
597
|
+
land_cover_map.sat_img[var_name],
|
|
598
|
+
)
|
|
599
|
+
# land_cover_map.sat_img[var_name].values[land_cover_map.sat_img[var_name].values==key] = self.map_to_vprm_class[key]
|
|
600
|
+
|
|
601
|
+
if mode == "fractional":
|
|
602
|
+
import xesmf as xe
|
|
603
|
+
|
|
604
|
+
veg_inds = np.unique(
|
|
605
|
+
[self.map_to_vprm_class[i] for i in self.map_to_vprm_class.keys()]
|
|
606
|
+
)
|
|
607
|
+
if not os.path.exists(regridder_save_path):
|
|
608
|
+
src_grid = to_esmf_grid(land_cover_map.sat_img)
|
|
609
|
+
ds_out = to_esmf_grid(self.sat_imgs.sat_img)
|
|
610
|
+
src_temp_path = os.path.join(
|
|
611
|
+
os.path.dirname(regridder_save_path),
|
|
612
|
+
"{}.nc".format(str(uuid.uuid4())),
|
|
613
|
+
)
|
|
614
|
+
dest_temp_path = os.path.join(
|
|
615
|
+
os.path.dirname(regridder_save_path),
|
|
616
|
+
"{}.nc".format(str(uuid.uuid4())),
|
|
617
|
+
)
|
|
618
|
+
src_grid.to_netcdf(src_temp_path)
|
|
619
|
+
ds_out.to_netcdf(dest_temp_path)
|
|
620
|
+
exec_str = "ESMF_RegridWeightGen --source {} --destination {} --weight {} -m conserve -r --netcdf4 –src_regional –dest_regional ".format(
|
|
621
|
+
src_temp_path, dest_temp_path, regridder_save_path
|
|
622
|
+
)
|
|
623
|
+
if mpi is True:
|
|
624
|
+
exec_str = "mpirun -np {} ".format(n_cpus) + exec_str
|
|
625
|
+
if not logs:
|
|
626
|
+
exec_str += " --no_log "
|
|
627
|
+
logger.info("Run: {}".format(exec_str))
|
|
628
|
+
os.system(exec_str)
|
|
629
|
+
os.remove(src_temp_path)
|
|
630
|
+
os.remove(dest_temp_path)
|
|
631
|
+
grid1_xesmf = make_xesmf_grid(land_cover_map.sat_img)
|
|
632
|
+
grid2_xesmf = make_xesmf_grid(self.sat_imgs.sat_img)
|
|
633
|
+
for i in veg_inds:
|
|
634
|
+
land_cover_map.sat_img["veg_{}".format(i)] = (
|
|
635
|
+
["y", "x"],
|
|
636
|
+
xr.where(
|
|
637
|
+
land_cover_map.sat_img[var_name].values == i, 1.0, 0.0
|
|
638
|
+
),
|
|
639
|
+
)
|
|
640
|
+
regridder = xe.Regridder(
|
|
641
|
+
grid1_xesmf,
|
|
642
|
+
grid2_xesmf,
|
|
643
|
+
"conservative",
|
|
644
|
+
weights=regridder_save_path,
|
|
645
|
+
reuse_weights=True,
|
|
646
|
+
)
|
|
647
|
+
handler = regridder(land_cover_map.sat_img)
|
|
648
|
+
handler = handler.assign_coords(
|
|
649
|
+
{
|
|
650
|
+
"x": self.sat_imgs.sat_img.coords["x"].values,
|
|
651
|
+
"y": self.sat_imgs.sat_img.coords["y"].values,
|
|
652
|
+
}
|
|
653
|
+
)
|
|
654
|
+
self.land_cover_type = satellite_data_manager(sat_img=handler)
|
|
655
|
+
|
|
656
|
+
else:
|
|
657
|
+
if (
|
|
658
|
+
land_cover_map.sat_img.rio.crs.to_proj4()
|
|
659
|
+
!= self.sat_imgs.sat_img.rio.crs.to_proj4()
|
|
660
|
+
):
|
|
661
|
+
logger.info(
|
|
662
|
+
"Projection of land cover map and satellite images need to match. Reproject first."
|
|
663
|
+
)
|
|
664
|
+
return False
|
|
665
|
+
f_array = np.zeros(
|
|
666
|
+
np.shape(land_cover_map.sat_img[var_name].values), dtype=np.int16
|
|
667
|
+
)
|
|
668
|
+
count_array = np.zeros(
|
|
669
|
+
np.shape(land_cover_map.sat_img[var_name].values), dtype=np.int16
|
|
670
|
+
)
|
|
671
|
+
if filter_size is None:
|
|
672
|
+
filter_size = int(
|
|
673
|
+
np.ceil(
|
|
674
|
+
self.sat_imgs.sat_img.rio.resolution()[0]
|
|
675
|
+
/ land_cover_map.get_resolution()
|
|
676
|
+
)
|
|
677
|
+
)
|
|
678
|
+
logger.info("Filter size {}:".format(filter_size))
|
|
679
|
+
if filter_size <= 1:
|
|
680
|
+
filter_size = 1
|
|
681
|
+
for i in veg_inds:
|
|
682
|
+
mask = np.array(
|
|
683
|
+
land_cover_map.sat_img[var_name].values == i, dtype=np.float64
|
|
684
|
+
)
|
|
685
|
+
ta = scipy.ndimage.uniform_filter(
|
|
686
|
+
mask, size=(filter_size, filter_size)
|
|
687
|
+
) * (filter_size**2)
|
|
688
|
+
f_array[ta > count_array] = i
|
|
689
|
+
count_array[ta > count_array] = ta[ta > count_array]
|
|
690
|
+
f_array[f_array == 0] = (
|
|
691
|
+
8 # 8 is Category for nothing | alternatively np.nan?
|
|
692
|
+
)
|
|
693
|
+
land_cover_map.sat_img[var_name].values = f_array
|
|
694
|
+
del ta
|
|
695
|
+
del count_array
|
|
696
|
+
del f_array
|
|
697
|
+
del mask
|
|
698
|
+
t = (
|
|
699
|
+
land_cover_map.sat_img.sel(
|
|
700
|
+
x=self.sat_imgs.sat_img.x.values,
|
|
701
|
+
y=self.sat_imgs.sat_img.y.values,
|
|
702
|
+
method="nearest",
|
|
703
|
+
)
|
|
704
|
+
.to_array()
|
|
705
|
+
.values[0]
|
|
706
|
+
)
|
|
707
|
+
self.land_cover_type = copy.deepcopy(self.prototype)
|
|
708
|
+
self.land_cover_type.sat_img = self.land_cover_type.sat_img.assign(
|
|
709
|
+
{var_name: (["y", "x"], t)}
|
|
710
|
+
)
|
|
711
|
+
for i in veg_inds:
|
|
712
|
+
self.land_cover_type.sat_img["veg_{}".format(i)] = (
|
|
713
|
+
["y", "x"],
|
|
714
|
+
xr.where(mm.sat_img[var_name].values == i, 1.0, 0.0),
|
|
715
|
+
)
|
|
716
|
+
self.land_cover_type.sat_img = self.land_cover_type.sat_img.drop_vars(
|
|
717
|
+
[var_name]
|
|
718
|
+
)
|
|
719
|
+
var_list = list(dict(self.land_cover_type.sat_img.data_vars.dtypes).keys())
|
|
720
|
+
self.land_cover_type.sat_img = xr.concat(
|
|
721
|
+
[self.land_cover_type.sat_img[var] for var in var_list],
|
|
722
|
+
dim="vprm_classes",
|
|
723
|
+
)
|
|
724
|
+
self.land_cover_type.sat_img = self.land_cover_type.sat_img.assign_coords(
|
|
725
|
+
{"vprm_classes": [int(c.split("_")[1]) for c in list(var_list)]}
|
|
726
|
+
)
|
|
727
|
+
if save_path is not None:
|
|
728
|
+
self.land_cover_type.save(save_path)
|
|
729
|
+
return
|
|
730
|
+
|
|
731
|
+
def calc_min_max_evi_lswi(self):
|
|
732
|
+
"""
|
|
733
|
+
Calculate the minimim and maximum EVI and LSWI
|
|
734
|
+
Parameters:
|
|
735
|
+
None
|
|
736
|
+
Returns:
|
|
737
|
+
None
|
|
738
|
+
"""
|
|
739
|
+
self.max_lswi = copy.deepcopy(self.prototype)
|
|
740
|
+
self.min_lswi = copy.deepcopy(self.prototype)
|
|
741
|
+
self.min_max_evi = copy.deepcopy(self.prototype)
|
|
742
|
+
shortcut = self.sat_imgs.sat_img
|
|
743
|
+
# if self.sites is None:
|
|
744
|
+
self.min_lswi.sat_img["min_lswi"] = shortcut["lswi"].min(
|
|
745
|
+
self.time_key, skipna=True
|
|
746
|
+
)
|
|
747
|
+
self.min_max_evi.sat_img["min_evi"] = shortcut["evi"].min(
|
|
748
|
+
self.time_key, skipna=True
|
|
749
|
+
)
|
|
750
|
+
self.min_max_evi.sat_img["max_evi"] = shortcut["evi"].max(
|
|
751
|
+
self.time_key, skipna=True
|
|
752
|
+
)
|
|
753
|
+
# Set growing season threshold to 20% of the difference between max and min value. This should be studied in more detail
|
|
754
|
+
# self.max_lswi.sat_img['growing_season_th'] = shortcut['evi'].min(self.time_key, skipna=True) + 0.3 * ( shortcut['evi'].max(self.time_key, skipna=True) - shortcut['evi'].min(self.time_key, skipna=True))
|
|
755
|
+
self.min_max_evi.sat_img["th"] = shortcut["evi"].min(
|
|
756
|
+
self.time_key, skipna=True
|
|
757
|
+
) + 0.55 * (
|
|
758
|
+
shortcut["evi"].max(self.time_key, skipna=True)
|
|
759
|
+
- shortcut["evi"].min(self.time_key, skipna=True)
|
|
760
|
+
)
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
def lowess(self, keys, lonlats=None, times=False, frac=0.25, it=3, n_cpus=None):
|
|
764
|
+
"""
|
|
765
|
+
Performs the lowess smoothing
|
|
766
|
+
|
|
767
|
+
Parameters:
|
|
768
|
+
lonlats (str): If given the smearing is only performed at the
|
|
769
|
+
given lats and lons
|
|
770
|
+
Returns:
|
|
771
|
+
None
|
|
772
|
+
"""
|
|
773
|
+
self.sat_imgs.sat_img.load()
|
|
774
|
+
|
|
775
|
+
if n_cpus is None:
|
|
776
|
+
n_cpus = self.n_cpus
|
|
777
|
+
if isinstance(times, pd.core.indexes.datetimes.DatetimeIndex):
|
|
778
|
+
times = list(times)
|
|
779
|
+
if isinstance(times, list):
|
|
780
|
+
times = np.array(sorted(times))
|
|
781
|
+
if (times[-1] > self.timestamp_end) | (times[0] < self.timestamp_start):
|
|
782
|
+
logger.info(
|
|
783
|
+
"You have provied some timestamps that are not covered from satellite images.\
|
|
784
|
+
They will be ignored in the following, to avoid unreliable results"
|
|
785
|
+
)
|
|
786
|
+
times = times[
|
|
787
|
+
(times <= self.timestamp_end) & (times >= self.timestamp_start)
|
|
788
|
+
]
|
|
789
|
+
xvals = [
|
|
790
|
+
int(
|
|
791
|
+
np.round(
|
|
792
|
+
(i - self.timestamp_start).total_seconds() / (24 * 60 * 60)
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
for i in times
|
|
796
|
+
]
|
|
797
|
+
elif isinstance(times, str):
|
|
798
|
+
if times == "daily":
|
|
799
|
+
xvals = np.arange(self.tot_num_days)
|
|
800
|
+
else:
|
|
801
|
+
logger.info("{} is not a valid str for times".format(times))
|
|
802
|
+
return
|
|
803
|
+
else:
|
|
804
|
+
xvals = self.sat_imgs.sat_img["time"]
|
|
805
|
+
logger.info("Lowess timestamps {}".format(xvals))
|
|
806
|
+
|
|
807
|
+
if self.sites is not None: # Is flux tower sites are given
|
|
808
|
+
if "timestamps" in list(self.sat_imgs.sat_img.data_vars):
|
|
809
|
+
for key in keys:
|
|
810
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
|
|
811
|
+
{
|
|
812
|
+
key: (
|
|
813
|
+
["time_gap_filled", "site_names"],
|
|
814
|
+
np.array(
|
|
815
|
+
[
|
|
816
|
+
do_lowess_smoothing(
|
|
817
|
+
self.sat_imgs.sat_img.sel(site_names=i)[
|
|
818
|
+
key
|
|
819
|
+
].values,
|
|
820
|
+
timestamps=self.sat_imgs.sat_img.sel(
|
|
821
|
+
site_names=i
|
|
822
|
+
)["timestamps"].values,
|
|
823
|
+
xvals=xvals,
|
|
824
|
+
frac=frac,
|
|
825
|
+
it=it,
|
|
826
|
+
)
|
|
827
|
+
for i in self.sat_imgs.sat_img.site_names.values
|
|
828
|
+
]
|
|
829
|
+
).T,
|
|
830
|
+
)
|
|
831
|
+
}
|
|
832
|
+
)
|
|
833
|
+
else:
|
|
834
|
+
for key in keys:
|
|
835
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
|
|
836
|
+
{
|
|
837
|
+
key: (
|
|
838
|
+
["time_gap_filled", "site_names"],
|
|
839
|
+
np.array(
|
|
840
|
+
[
|
|
841
|
+
do_lowess_smoothing(
|
|
842
|
+
self.sat_imgs.sat_img.sel(site_names=i)[
|
|
843
|
+
key
|
|
844
|
+
].values,
|
|
845
|
+
timestamps=self.sat_imgs.sat_img[
|
|
846
|
+
"time"
|
|
847
|
+
].values,
|
|
848
|
+
xvals=xvals,
|
|
849
|
+
frac=frac,
|
|
850
|
+
it=it,
|
|
851
|
+
)
|
|
852
|
+
for i in self.sat_imgs.sat_img.site_names.values
|
|
853
|
+
]
|
|
854
|
+
).T,
|
|
855
|
+
)
|
|
856
|
+
}
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
elif lonlats is None: # If smoothing the entire array
|
|
860
|
+
if "timestamps" in list(self.sat_imgs.sat_img.data_vars):
|
|
861
|
+
for key in keys:
|
|
862
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
|
|
863
|
+
{
|
|
864
|
+
key: (
|
|
865
|
+
["time_gap_filled", "y", "x"],
|
|
866
|
+
np.array(
|
|
867
|
+
Parallel(n_jobs=n_cpus, max_nbytes=None)(
|
|
868
|
+
delayed(do_lowess_smoothing)(
|
|
869
|
+
self.sat_imgs.sat_img[key][:, :, i].values,
|
|
870
|
+
timestamps=self.sat_imgs.sat_img[
|
|
871
|
+
"timestamps"
|
|
872
|
+
][:, :, i].values,
|
|
873
|
+
xvals=xvals,
|
|
874
|
+
frac=frac,
|
|
875
|
+
it=it,
|
|
876
|
+
)
|
|
877
|
+
for i, x_coord in enumerate(
|
|
878
|
+
self.sat_imgs.sat_img.x.values
|
|
879
|
+
)
|
|
880
|
+
)
|
|
881
|
+
).T,
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
)
|
|
885
|
+
else:
|
|
886
|
+
for key in keys:
|
|
887
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
|
|
888
|
+
{
|
|
889
|
+
key: (
|
|
890
|
+
["time_gap_filled", "y", "x"],
|
|
891
|
+
np.array(
|
|
892
|
+
Parallel(n_jobs=n_cpus, max_nbytes=None)(
|
|
893
|
+
delayed(do_lowess_smoothing)(
|
|
894
|
+
self.sat_imgs.sat_img[key][:, :, i].values,
|
|
895
|
+
timestamps=self.sat_imgs.sat_img[
|
|
896
|
+
"time"
|
|
897
|
+
].values,
|
|
898
|
+
xvals=xvals,
|
|
899
|
+
frac=frac,
|
|
900
|
+
it=it,
|
|
901
|
+
)
|
|
902
|
+
for i, x_coord in enumerate(
|
|
903
|
+
self.sat_imgs.sat_img.x.values
|
|
904
|
+
)
|
|
905
|
+
)
|
|
906
|
+
).T,
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
else:
|
|
912
|
+
logger.info("Not implemented")
|
|
913
|
+
# Originally had a function to smooth only at specific lat/long.
|
|
914
|
+
# That doesn't make sense anymore, because the time dimension will change through lowess smoothing.
|
|
915
|
+
# If this is your plan then try to crop the sat image first and then do the lowess filtering.
|
|
916
|
+
|
|
917
|
+
self.time_key = "time_gap_filled"
|
|
918
|
+
self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign_coords(
|
|
919
|
+
{"time_gap_filled": list(xvals)}
|
|
920
|
+
)
|
|
921
|
+
return
|
|
922
|
+
|
|
923
|
+
def clip_values(self, key, min_val, max_val):
|
|
924
|
+
self.sat_imgs.sat_img[key].values[
|
|
925
|
+
self.sat_imgs.sat_img[key].values < min_val
|
|
926
|
+
] = min_val
|
|
927
|
+
self.sat_imgs.sat_img[key].values[
|
|
928
|
+
self.sat_imgs.sat_img[key].values > max_val
|
|
929
|
+
] = max_val
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
def clip_non_finite(self, data_var, val, sel):
|
|
933
|
+
t = self.sat_imgs.sat_img[data_var].loc[sel].values
|
|
934
|
+
t[~np.isfinite(t)] = val
|
|
935
|
+
self.sat_imgs.sat_img[data_var].loc[sel] = t
|
|
936
|
+
return
|
|
937
|
+
|
|
938
|
+
def get_current_timestamp(self):
|
|
939
|
+
return self.timestamps[self.counter]
|
|
940
|
+
|
|
941
|
+
def get_evi(self, lon=None, lat=None, site_name=None):
|
|
942
|
+
"""
|
|
943
|
+
Get EVI for the current satellite image (see self.counter)
|
|
944
|
+
|
|
945
|
+
Parameters:
|
|
946
|
+
lon (float): longitude
|
|
947
|
+
lat (float): latitude
|
|
948
|
+
Returns:
|
|
949
|
+
EVI array
|
|
950
|
+
"""
|
|
951
|
+
|
|
952
|
+
# if (self.new is False) & ('evi' in self.buffer.keys()):
|
|
953
|
+
# return self.buffer['evi']
|
|
954
|
+
if site_name is not None:
|
|
955
|
+
self.buffer["evi"] = float(
|
|
956
|
+
self.sat_imgs.sat_img.sel(site_names=site_name).isel(
|
|
957
|
+
{self.time_key: self.counter}
|
|
958
|
+
)["evi"]
|
|
959
|
+
)
|
|
960
|
+
elif lon is not None:
|
|
961
|
+
self.buffer["evi"] = self.sat_imgs.value_at_lonlat(
|
|
962
|
+
lon, lat, as_array=False, key="evi", isel={self.time_key: self.counter}
|
|
963
|
+
)
|
|
964
|
+
else:
|
|
965
|
+
self.buffer["evi"] = self.sat_imgs.sat_img["evi"].isel(
|
|
966
|
+
{self.time_key: self.counter}
|
|
967
|
+
)
|
|
968
|
+
return self.buffer["evi"]
|
|
969
|
+
|
|
970
|
+
def get_lswi(self, lon=None, lat=None, site_name=None):
|
|
971
|
+
"""
|
|
972
|
+
Get LSWI for the current satellite image (see self.counter)
|
|
973
|
+
|
|
974
|
+
Parameters:
|
|
975
|
+
lon (float): longitude
|
|
976
|
+
lat (float): latitude
|
|
977
|
+
Returns:
|
|
978
|
+
LSWI array
|
|
979
|
+
"""
|
|
980
|
+
|
|
981
|
+
# if (self.new is False) & ('lswi' in self.buffer.keys()):
|
|
982
|
+
# return self.buffer['lswi']
|
|
983
|
+
if site_name is not None:
|
|
984
|
+
self.buffer["lswi"] = float(
|
|
985
|
+
self.sat_imgs.sat_img.sel(site_names=site_name).isel(
|
|
986
|
+
{self.time_key: self.counter}
|
|
987
|
+
)["lswi"]
|
|
988
|
+
)
|
|
989
|
+
elif lon is not None:
|
|
990
|
+
self.buffer["lswi"] = self.sat_imgs.value_at_lonlat(
|
|
991
|
+
lon, lat, as_array=False, key="lswi", isel={self.time_key: self.counter}
|
|
992
|
+
)
|
|
993
|
+
else:
|
|
994
|
+
self.buffer["lswi"] = self.sat_imgs.sat_img["lswi"].isel(
|
|
995
|
+
{self.time_key: self.counter}
|
|
996
|
+
)
|
|
997
|
+
return self.buffer["lswi"]
|
|
998
|
+
|
|
999
|
+
def get_sat_img_values_from_key(self, key, lon=None, lat=None, counter_range=None):
|
|
1000
|
+
"""
|
|
1001
|
+
Get EVI for the current satellite image (see self.counter)
|
|
1002
|
+
|
|
1003
|
+
Parameters:
|
|
1004
|
+
lon (float): longitude
|
|
1005
|
+
lat (float): latitude
|
|
1006
|
+
Returns:
|
|
1007
|
+
EVI array
|
|
1008
|
+
"""
|
|
1009
|
+
|
|
1010
|
+
if self.new is False:
|
|
1011
|
+
return self.buffer[key]
|
|
1012
|
+
if counter_range is None:
|
|
1013
|
+
select_dict = {self.time_key: self.counter}
|
|
1014
|
+
else:
|
|
1015
|
+
select_dict = {self.time_key: counter_range}
|
|
1016
|
+
|
|
1017
|
+
if lon is not None:
|
|
1018
|
+
self.buffer[key] = self.sat_imgs.value_at_lonlat(
|
|
1019
|
+
lon, lat, as_array=False, key=key, isel=select_dict
|
|
1020
|
+
).values.flatten()
|
|
1021
|
+
else:
|
|
1022
|
+
self.buffer[key] = self.sat_imgs.sat_img[key].isel(select_dict)
|
|
1023
|
+
return self.sat_img_buffer[key]
|
|
1024
|
+
|
|
1025
|
+
def get_sat_img_values_for_all_keys(self, lon=None, lat=None, counter_range=None):
|
|
1026
|
+
"""
|
|
1027
|
+
Get EVI for the current satellite image (see self.counter)
|
|
1028
|
+
|
|
1029
|
+
Parameters:
|
|
1030
|
+
lon (float): longitude
|
|
1031
|
+
lat (float): latitude
|
|
1032
|
+
Returns:
|
|
1033
|
+
EVI array
|
|
1034
|
+
"""
|
|
1035
|
+
|
|
1036
|
+
if self.new is False:
|
|
1037
|
+
return self.buffer["all_sat_keys"]
|
|
1038
|
+
|
|
1039
|
+
if counter_range is None:
|
|
1040
|
+
select_dict = {self.time_key: self.counter}
|
|
1041
|
+
else:
|
|
1042
|
+
select_dict = {self.time_key: counter_range}
|
|
1043
|
+
|
|
1044
|
+
if lon is not None:
|
|
1045
|
+
self.buffer["all_sat_keys"] = self.sat_imgs.value_at_lonlat(
|
|
1046
|
+
lon, lat, as_array=True, isel=select_dict
|
|
1047
|
+
)
|
|
1048
|
+
else:
|
|
1049
|
+
self.buffer["all_sat_keys"] = self.sat_imgs.sat_img.isel(select_dict)
|
|
1050
|
+
return self.buffer["all_sat_keys"]
|
|
1051
|
+
|
|
1052
|
+
def _set_sat_img_counter(self, datetime_utc):
|
|
1053
|
+
days_after_first_image = (
|
|
1054
|
+
datetime_utc - self.timestamp_start
|
|
1055
|
+
).total_seconds() / (24 * 60 * 60)
|
|
1056
|
+
counter_new = np.argmin(
|
|
1057
|
+
np.abs(self.sat_imgs.sat_img[self.time_key].values - days_after_first_image)
|
|
1058
|
+
)
|
|
1059
|
+
if (days_after_first_image < 0) | (
|
|
1060
|
+
days_after_first_image > self.sat_imgs.sat_img[self.time_key][-1]
|
|
1061
|
+
):
|
|
1062
|
+
# logger.info('No data for {}'.format(datetime_utc))
|
|
1063
|
+
self.counter = 0
|
|
1064
|
+
return False
|
|
1065
|
+
elif counter_new != self.counter:
|
|
1066
|
+
self.new = True
|
|
1067
|
+
self.counter = counter_new
|
|
1068
|
+
return
|
|
1069
|
+
else:
|
|
1070
|
+
self.new = False
|
|
1071
|
+
return # Still same satellite image
|
|
1072
|
+
|
|
1073
|
+
def _set_prototype_lat_lon(self):
|
|
1074
|
+
src_x = self.prototype.sat_img.coords["x"].values
|
|
1075
|
+
src_y = self.prototype.sat_img.coords["y"].values
|
|
1076
|
+
X, Y = np.meshgrid(src_x, src_y)
|
|
1077
|
+
t = Transformer.from_crs(
|
|
1078
|
+
self.prototype.sat_img.rio.crs, "+proj=longlat +datum=WGS84"
|
|
1079
|
+
)
|
|
1080
|
+
x_long, y_lat = t.transform(X, Y)
|
|
1081
|
+
self.prototype_lat_lon = xr.Dataset(
|
|
1082
|
+
{
|
|
1083
|
+
"lon": (["y", "x"], x_long, {"units": "degrees_east"}),
|
|
1084
|
+
"lat": (["y", "x"], y_lat, {"units": "degrees_north"}),
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
self.prototype_lat_lon = self.prototype_lat_lon.set_coords(["lon", "lat"])
|
|
1088
|
+
return
|
|
1089
|
+
|
|
1090
|
+
def save(self, save_path):
|
|
1091
|
+
"""
|
|
1092
|
+
Save the LSWI and EVI satellite image. ToDo
|
|
1093
|
+
"""
|
|
1094
|
+
self.sat_imgs.save(save_path)
|
|
1095
|
+
return
|
|
1096
|
+
|
|
1097
|
+
def is_disjoint(self, this_sat_img):
|
|
1098
|
+
bounds = self.prototype.sat_img.rio.transform_bounds(
|
|
1099
|
+
this_sat_img.sat_img.rio.crs
|
|
1100
|
+
)
|
|
1101
|
+
dj = rasterio.coords.disjoint_bounds(bounds, this_sat_img.sat_img.rio.bounds())
|
|
1102
|
+
return dj
|
|
1103
|
+
|
|
1104
|
+
def add_vprm_insts(self, vprm_insts, allow_reproject=True):
|
|
1105
|
+
# Add Check that timestamps align before merging
|
|
1106
|
+
if isinstance(self.sat_imgs, satellite_data_manager):
|
|
1107
|
+
self.sat_imgs.add_tile(
|
|
1108
|
+
[v.sat_imgs for v in vprm_insts], reproject=allow_reproject
|
|
1109
|
+
)
|
|
1110
|
+
keys = list(self.sat_imgs.sat_img.keys())
|
|
1111
|
+
self.prototype = satellite_data_manager(
|
|
1112
|
+
sat_img=self.sat_imgs.sat_img.drop(keys)
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
if self.land_cover_type is not None:
|
|
1116
|
+
self.land_cover_type.add_tile(
|
|
1117
|
+
[v.land_cover_type for v in vprm_insts], reproject=False
|
|
1118
|
+
)
|