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.
Files changed (42) hide show
  1. pyVPRM/VPRM.py +1118 -0
  2. pyVPRM/__init__.py +1 -0
  3. pyVPRM/lib/__init__.py +0 -0
  4. pyVPRM/lib/downmodis.py +1012 -0
  5. pyVPRM/lib/fancy_plot.py +75 -0
  6. pyVPRM/lib/flux_tower_class.py +444 -0
  7. pyVPRM/lib/fluxnet_info/fluxnet_sites.pkl +0 -0
  8. pyVPRM/lib/fluxnet_info/site_infos.txt +218 -0
  9. pyVPRM/lib/functions.py +471 -0
  10. pyVPRM/meteorologies/__init__.py +0 -0
  11. pyVPRM/meteorologies/era5_class_dkrz.py +279 -0
  12. pyVPRM/meteorologies/era5_class_draft.py +60 -0
  13. pyVPRM/meteorologies/era5_monthly_xr.py +149 -0
  14. pyVPRM/meteorologies/met_base_class.py +67 -0
  15. pyVPRM/meteorologies/met_local_measurement.py +56 -0
  16. pyVPRM/sat_managers/__init__.py +0 -0
  17. pyVPRM/sat_managers/base_manager.py +430 -0
  18. pyVPRM/sat_managers/city.py +21 -0
  19. pyVPRM/sat_managers/copernicus.py +29 -0
  20. pyVPRM/sat_managers/esa_world_cover.py +21 -0
  21. pyVPRM/sat_managers/mapbiomas.py +35 -0
  22. pyVPRM/sat_managers/modis.py +266 -0
  23. pyVPRM/sat_managers/proba_v.py +86 -0
  24. pyVPRM/sat_managers/sentinel2.py +192 -0
  25. pyVPRM/sat_managers/synmap.py +21 -0
  26. pyVPRM/sat_managers/viirs.py +232 -0
  27. pyVPRM/sat_managers/viirs09ga.py +173 -0
  28. pyVPRM/vprm_configs/__init__.py +0 -0
  29. pyVPRM/vprm_configs/copernicus_land_cover.yaml +94 -0
  30. pyVPRM/vprm_configs/esa_world_cover.yaml +71 -0
  31. pyVPRM/vprm_configs/synmap.yaml +106 -0
  32. pyVPRM/vprm_models/__init__.py +1 -0
  33. pyVPRM/vprm_models/model_params/__init__.py +0 -0
  34. pyVPRM/vprm_models/vprm_base.py +691 -0
  35. pyVPRM/vprm_models/vprm_base_no_xeric.py +585 -0
  36. pyVPRM/vprm_models/vprm_modified.py +530 -0
  37. pyVPRM/vprm_models/vprm_nn.py +130 -0
  38. pyVPRM-3.0.dist-info/LICENSE +21 -0
  39. pyVPRM-3.0.dist-info/METADATA +40 -0
  40. pyVPRM-3.0.dist-info/RECORD +42 -0
  41. pyVPRM-3.0.dist-info/WHEEL +5 -0
  42. 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
+ )