pyVPRM 3.0__tar.gz → 5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {pyvprm-3.0/pyVPRM.egg-info → pyvprm-5.1}/PKG-INFO +13 -2
  2. {pyvprm-3.0 → pyvprm-5.1}/README.md +29 -8
  3. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/VPRM.py +105 -50
  4. pyvprm-5.1/pyVPRM/__init__.py +2 -0
  5. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/lib/flux_tower_class.py +41 -14
  6. pyvprm-5.1/pyVPRM/lib/functions.py +495 -0
  7. pyvprm-5.1/pyVPRM/lib/reproject_fiona.py +64 -0
  8. pyvprm-5.1/pyVPRM/meteorologies/era5_land_destinE.py +200 -0
  9. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/meteorologies/era5_monthly_xr.py +44 -4
  10. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/base_manager.py +13 -8
  11. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/modis.py +18 -0
  12. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/sentinel2.py +12 -0
  13. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/viirs.py +5 -0
  14. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_configs/copernicus_land_cover.yaml +32 -32
  15. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_configs/esa_world_cover.yaml +38 -22
  16. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_models/vprm_base.py +7 -4
  17. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_models/vprm_base_no_xeric.py +2 -2
  18. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_models/vprm_modified.py +2 -2
  19. pyvprm-5.1/pyVPRM/vprm_models/vprm_nn.py +149 -0
  20. {pyvprm-3.0 → pyvprm-5.1/pyVPRM.egg-info}/PKG-INFO +13 -2
  21. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM.egg-info/SOURCES.txt +2 -0
  22. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM.egg-info/requires.txt +1 -0
  23. {pyvprm-3.0 → pyvprm-5.1}/setup.py +2 -1
  24. pyvprm-3.0/pyVPRM/__init__.py +0 -1
  25. pyvprm-3.0/pyVPRM/lib/functions.py +0 -471
  26. pyvprm-3.0/pyVPRM/vprm_models/vprm_nn.py +0 -130
  27. {pyvprm-3.0 → pyvprm-5.1}/LICENSE +0 -0
  28. {pyvprm-3.0 → pyvprm-5.1}/MANIFEST.in +0 -0
  29. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/lib/__init__.py +0 -0
  30. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/lib/downmodis.py +0 -0
  31. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/lib/fancy_plot.py +0 -0
  32. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/lib/fluxnet_info/fluxnet_sites.pkl +0 -0
  33. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/lib/fluxnet_info/site_infos.txt +0 -0
  34. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/meteorologies/__init__.py +0 -0
  35. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/meteorologies/era5_class_dkrz.py +0 -0
  36. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/meteorologies/era5_class_draft.py +0 -0
  37. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/meteorologies/met_base_class.py +0 -0
  38. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/meteorologies/met_local_measurement.py +0 -0
  39. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/__init__.py +0 -0
  40. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/city.py +0 -0
  41. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/copernicus.py +0 -0
  42. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/esa_world_cover.py +0 -0
  43. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/mapbiomas.py +0 -0
  44. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/proba_v.py +0 -0
  45. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/synmap.py +0 -0
  46. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/sat_managers/viirs09ga.py +0 -0
  47. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_configs/__init__.py +0 -0
  48. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_configs/synmap.yaml +0 -0
  49. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_models/__init__.py +0 -0
  50. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM/vprm_models/model_params/__init__.py +0 -0
  51. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM.egg-info/dependency_links.txt +0 -0
  52. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM.egg-info/not-zip-safe +0 -0
  53. {pyvprm-3.0 → pyvprm-5.1}/pyVPRM.egg-info/top_level.txt +0 -0
  54. {pyvprm-3.0 → pyvprm-5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyVPRM
3
- Version: 3.0
3
+ Version: 5.1
4
4
  Summary: Vegetation Photosynthesis and Respiration Model
5
5
  Home-page: https://github.com/tglauch/pyVPRM/
6
6
  Author: Theo Glauch
@@ -36,5 +36,16 @@ Requires-Dist: lxml
36
36
  Requires-Dist: requests
37
37
  Requires-Dist: statsmodels
38
38
  Requires-Dist: loguru
39
+ Requires-Dist: fiona
40
+ Dynamic: author
41
+ Dynamic: author-email
42
+ Dynamic: classifier
43
+ Dynamic: description
44
+ Dynamic: home-page
45
+ Dynamic: keywords
46
+ Dynamic: license
47
+ Dynamic: license-file
48
+ Dynamic: requires-dist
49
+ Dynamic: summary
39
50
 
40
51
  A tool to calculate the CO2 exchange flux between atmosphere and terrestrial biosphere using the Vegetation Photosynthesis and Respiration Model
@@ -1,13 +1,36 @@
1
1
  [![DOI](https://zenodo.org/badge/626435494.svg)](https://doi.org/10.5281/zenodo.14216613)
2
2
 
3
+ <figure>
4
+ <img src="https://github.com/tglauch/pyVPRM/assets/29706254/ba2565e6-1434-4a95-8086-936462f8d05d", height=150pt>
5
+ </figure>
6
+
7
+ A description of the model is published in Geoscientific Model Development (GMD) https://gmd.copernicus.org/articles/18/4713/2025/. If you use this package for you scientific work please cite as follows
8
+
9
+ ```
10
+ Glauch, T., Marshall, J., Gerbig, C., Botía, S., Gałkowski, M., Vardag, S. N., & Butz, A. (2025). pyVPRM: A next-generation vegetation photosynthesis and respiration model for the post-MODIS era. Geoscientific Model Development, 18(14), 4713–4742. https://doi.org/10.5194/gmd-18-4713-2025
11
+ ```
3
12
 
13
+ or using BibTeX:
14
+ ```
15
+ @Article{gmd-18-4713-2025,
16
+ AUTHOR = {Glauch, T. and Marshall, J. and Gerbig, C. and Bot\'{\i}a, S. and Ga{\l}kowski, M. and Vardag, S. N. and Butz, A.},
17
+ TITLE = {\textit{pyVPRM}: a next-generation vegetation photosynthesis and respiration model for the post-MODIS era},
18
+ JOURNAL = {Geoscientific Model Development},
19
+ VOLUME = {18},
20
+ YEAR = {2025},
21
+ NUMBER = {14},
22
+ PAGES = {4713--4742},
23
+ URL = {https://gmd.copernicus.org/articles/18/4713/2025/},
24
+ DOI = {10.5194/gmd-18-4713-2025}
25
+ }
26
+ ```
4
27
 
5
- <img src="https://github.com/tglauch/pyVPRM/assets/29706254/ba2565e6-1434-4a95-8086-936462f8d05d" width=45% height=50%>
28
+ In case of any questions please write an E-Mail to theo.glauch@dlr.de.
6
29
 
7
30
  # About
8
31
 
9
32
 
10
- `pyVPRM` is a software package to calculate the CO2 exchange flux between atmosphere and terrestrial biosphere using the Vegetation Photosynthesis and Respriation Model (VPRM). It takes into account both, the primary productivity (GPP) as well as the respiration. The net flux between both of them is the net ecosystem exchange (NEE). The implementation is flexible and can be run with different satellite products (Sentinel-2, MODIS, VIIRS,...), land cover products (Copernicus Land Cover Service, ESA 10-m World Cover Map, MapBiomas) and climate models like the ECMWFS ERA5 Reanalysis. Through its modular structure it is also easily extendable.
33
+ `pyVPRM` is a software package to calculate the CO2 exchange flux between atmosphere and terrestrial biosphere using the Vegetation Photosynthesis and Respriation Model (VPRM). It takes into account both, the primary productivity (GPP) as well as the respiration. The net flux between both of them is the net ecosystem exchange (NEE). The implementation is flexible and can be run with different satellite products (Sentinel-2, MODIS, VIIRS,...), land cover products (Copernicus Land Cover Service, ESA 10-m World Cover Map, MapBiomas) and meteorological drivers like ECMWFS ERA5 Reanalysis. Through its modular structure it is also easily extendable.
11
34
 
12
35
  Among others it can be used for
13
36
 
@@ -15,16 +38,14 @@ Among others it can be used for
15
38
  2. Making CO2 flux predictions for a given region of interest
16
39
  3. Generating input files to run VPRM in the Weather Research and Forecasting Model (WRF)
17
40
 
18
- In case of any questions please write an E-Mail to theo.glauch@dlr.de.
19
-
20
41
  ## Example Net Ecosystem Fluxes generated with pyVPRM
21
42
 
22
43
  <figure>
23
- <img src="https://github.com/user-attachments/assets/def61395-f90e-421b-9264-f0608161bd7a", height=300pt>
44
+ <img src="https://github.com/user-attachments/assets/c099ef1f-6c5a-445c-bc6e-f6697c47b641", height=300pt>
24
45
  </figure>
25
46
 
26
47
  <figure>
27
- <img src="https://github.com/user-attachments/assets/48c412b2-cf5f-4573-9db8-e60909893218", height=300pt>
48
+ <img src="https://github.com/user-attachments/assets/26913805-f188-477a-9a85-08911a165b1e", height=300pt>
28
49
  </figure>
29
50
 
30
51
 
@@ -47,7 +68,7 @@ conda install esmpy
47
68
  Afterwards install ```pyVPRM``` using pip via
48
69
 
49
70
  ```
50
- pip install git+https://github.com/tglauch/pyVPRM.git
71
+ pip install pyVPRM
51
72
  ```
52
73
 
53
74
  ## Start your own project
@@ -63,7 +84,7 @@ In order to start your own project you need to at least follow theses steps:
63
84
  Remarks:
64
85
  - If there is not yet an interface for your satellite data or land cover map, implement a new subclass in `pyVPRM/sat_managers/`
65
86
  - For new land cover maps you need to additionaly provide a mapping of the land cover classes to the VPRM classes in a config file which is stored in `pyVPRM/vprm_configs`
66
- - Open access to land cover maps: Copernicus: https://lcviewer.vito.be/2019 | ESA World Cover: https://viewer.esa-worldcover.org
87
+ - Open access to land cover maps: Copernicus: https://land.copernicus.eu/en/products/global-dynamic-land-cover/copernicus-global-land-service-land-cover-100m-collection-3-epoch-2019-globe#download | ESA World Cover: https://viewer.esa-worldcover.org
67
88
  - Open access to satellite data: MODIS,VIIRS: https://e4ftl01.cr.usgs.gov | Sentinel-2: https://scihub.copernicus.eu/
68
89
 
69
90
  ## Examples
@@ -12,6 +12,7 @@ from pyVPRM.lib.functions import (
12
12
  do_lowess_smoothing,
13
13
  make_xesmf_grid,
14
14
  to_esmf_grid,
15
+ replace_inf_runs_ignore_nans
15
16
  )
16
17
  from scipy.ndimage import uniform_filter
17
18
  from pyproj import Transformer
@@ -38,11 +39,11 @@ regridder_options["conservative"] = "conserve"
38
39
 
39
40
  class vprm:
40
41
  """
41
- Class for the Vegetation Photosynthesis and Respiration Model
42
+ Main class for the Vegetation Photosynthesis and Respiration Model
42
43
  """
43
44
 
44
45
  def __init__(
45
- self, vprm_config_path, land_cover_map=None, verbose=False, n_cpus=1, sites=None
46
+ self, vprm_config_path, land_cover_map=None, verbose=False, n_cpus=1, flux_tower_instances=None
46
47
  ):
47
48
  """
48
49
  Initialize a class instance
@@ -51,7 +52,7 @@ class vprm:
51
52
  land_cover_map (xarray): A pre calculated map with the land cover types
52
53
  verbose (bool): Set true for additional output when debugging
53
54
  n_cpus: Number of CPUs
54
- sites: For fitting. Provide a list of sites.
55
+ flux_tower_instances: For fitting. Provide a list of flux_tower_instances.
55
56
 
56
57
  Returns:
57
58
  The lowess smoothed array
@@ -60,13 +61,10 @@ class vprm:
60
61
  logger.info("Running with pyVPRM version {}".format(pyVPRM.__version__))
61
62
  self.sat_imgs = []
62
63
 
63
- self.sites = sites
64
- if self.sites is not None:
65
- self.lonlats = [i.get_lonlat() for i in sites]
64
+ self.set_flux_tower_inst(flux_tower_instances)
66
65
  self.n_cpus = n_cpus
67
66
  self.counter = 0
68
67
  self.fit_params_dict = None
69
- self.res = None
70
68
 
71
69
  self.new = True
72
70
  self.timestamps = []
@@ -85,30 +83,36 @@ class vprm:
85
83
 
86
84
  with open(vprm_config_path, "r") as stream:
87
85
  try:
88
- cfg = yaml.safe_load(stream)
86
+ self.vprm_cfg = yaml.safe_load(stream)
89
87
  except yaml.YAMLError as exc:
90
88
  logger.info(exc)
91
89
 
92
90
  self.temp_coefficients = dict()
93
91
  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"],
92
+ for key in self.vprm_cfg:
93
+ if "tmin" in self.vprm_cfg[key].keys():
94
+ self.temp_coefficients[self.vprm_cfg[key]["vprm_class"]] = [
95
+ self.vprm_cfg[key]["tmin"],
96
+ self.vprm_cfg[key]["topt"],
97
+ self.vprm_cfg[key]["tmax"],
98
+ self.vprm_cfg[key]["tlow"],
101
99
  ]
102
- for c in cfg[key]["class_numbers"]:
103
- self.map_to_vprm_class[c] = cfg[key]["vprm_class"]
100
+ for c in self.vprm_cfg[key]["class_numbers"]:
101
+ self.map_to_vprm_class[c] = self.vprm_cfg[key]["vprm_class"]
104
102
  return
105
103
 
104
+ def set_flux_tower_inst(self, flux_tower_instances):
105
+ self.flux_tower_instances = flux_tower_instances
106
+ if self.flux_tower_instances is not None:
107
+ self.lonlats = [i.get_lonlat() for i in flux_tower_instances]
108
+ return
109
+
106
110
  def to_wrf_output(
107
111
  self,
108
112
  out_grid,
109
113
  weights_for_regridder=None,
110
114
  regridder_save_path=None,
111
- driver="xEMSF",
115
+ driver="xESMF",
112
116
  interp_method="conservative",
113
117
  n_cpus=None,
114
118
  mpi=True,
@@ -144,7 +148,7 @@ class vprm:
144
148
  logger.info(
145
149
  "Need to generate the weights for the regridder. This can be very slow and memory intensive"
146
150
  )
147
- if driver == "xEMSF":
151
+ if driver == "xESMF":
148
152
  regridder = xe.Regridder(src_grid, ds_out, interp_method)
149
153
  if regridder_save_path is not None:
150
154
  regridder.to_netcdf(regridder_save_path)
@@ -182,7 +186,7 @@ class vprm:
182
186
  # os.remove(dest_temp_path)
183
187
  weights_for_regridder = regridder_save_path
184
188
  else:
185
- logger.info("Driver needs to be xEMSF or ESMF_RegridWeightGen")
189
+ logger.info("Driver needs to be xESMF or ESMF_RegridWeightGen")
186
190
  if weights_for_regridder is not None:
187
191
  regridder = xe.Regridder(
188
192
  src_grid,
@@ -277,7 +281,12 @@ class vprm:
277
281
  "evi_max": ds_t_max_evi,
278
282
  "evi_min": ds_t_min_evi,
279
283
  }
280
-
284
+
285
+ veg_classes_def = 'VPRM class definition: '
286
+ for k in self.vprm_cfg.keys():
287
+ veg_classes_def += '{} {}, '.format(self.vprm_cfg[k]['vprm_class'], k)
288
+ veg_classes_def = veg_classes_def[:-2]
289
+
281
290
  for key in ret_dict.keys():
282
291
  ret_dict[key] = ret_dict[key].assign_attrs(
283
292
  title="VPRM input data for WRF: {}".format(key),
@@ -289,7 +298,7 @@ class vprm:
289
298
  institution2="Deutsches Zentrum für Luft- und Raumfahrt (DLR)",
290
299
  contact="theo.glauch@dlr.de",
291
300
  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",
301
+ comment=veg_classes_def,
293
302
  )
294
303
  return ret_dict
295
304
 
@@ -302,10 +311,12 @@ class vprm:
302
311
  b_swir=None,
303
312
  drop_bands=False,
304
313
  which_evi=None,
314
+ add_ndvi=False,
305
315
  timestamp_key=None,
306
316
  mask_bad_pixels=True,
307
317
  mask_clouds=True,
308
318
  mask_snow=True,
319
+ mask_water=True,
309
320
  ):
310
321
  """
311
322
  Add a new satellite image and calculate EVI and LSWI if desired
@@ -375,12 +386,20 @@ class vprm:
375
386
  temp_lswi = xr.where((temp_lswi < -1) | (temp_lswi > 1), np.nan, temp_lswi)
376
387
  handler.sat_img["evi"] = temp_evi
377
388
  handler.sat_img["lswi"] = temp_lswi
389
+ if add_ndvi:
390
+ nir = handler.sat_img[b_nir]
391
+ red = handler.sat_img[b_red]
392
+ temp_ndvi = (nir-red)/(nir+red)
393
+ handler.sat_img['ndvi'] = temp_ndvi
394
+
378
395
  if timestamp_key is not None:
379
396
  handler.sat_img = handler.sat_img.rename({timestamp_key: "timestamps"})
380
397
 
381
398
  bands_to_mask = []
382
399
  if which_evi in ["evi", "evi2"]:
383
400
  bands_to_mask = ["evi", "lswi"]
401
+ if add_ndvi:
402
+ bands_to_mask.append('ndvi')
384
403
  else:
385
404
  for btm in [b_nir, b_red, b_blue, b_swir]:
386
405
  if btm is not None:
@@ -390,6 +409,11 @@ class vprm:
390
409
  handler.mask_snow()
391
410
  else:
392
411
  handler.mask_snow(bands_to_mask)
412
+ if mask_water:
413
+ if bands_to_mask == []:
414
+ handler.mask_water()
415
+ else:
416
+ handler.mask_water(bands_to_mask)
393
417
  if mask_clouds:
394
418
  if bands_to_mask == []:
395
419
  handler.mask_clouds()
@@ -443,6 +467,7 @@ class vprm:
443
467
  x, y = t.transform(ll[0], ll[1])
444
468
  x_ind = np.argmin(np.abs(x - xs))
445
469
  y_ind = np.argmin(np.abs(y - ys))
470
+ arsz = 3 # Hard Coded. Means only 3 by 3 pixes around the lonlat location are smoothed
446
471
  for key in keys:
447
472
  logger.info(key)
448
473
  self.sat_imgs.sat_img[key][
@@ -464,20 +489,21 @@ class vprm:
464
489
  interp_method="nearest",
465
490
  )
466
491
  self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign_coords(
467
- {"site_names": [i.get_site_name() for i in self.sites]}
492
+ {"site_names": [i.get_site_name() for i in self.flux_tower_instances]}
468
493
  )
469
494
 
470
- def sort_and_merge_by_timestamp(self):
495
+ def sort_and_merge_by_timestamp(self, min_lenght_snow_period=21):
471
496
  """
472
497
  Called after adding the satellite images with 'add_sat_img'. Sorts the satellite
473
498
  images by timestamp and merges everything to one satellite_data_manager.
474
499
 
475
500
  Parameters:
476
-
501
+ min_lenght_snow_period defines how long a period should be until gaps are
502
+ filled to stabilize lowess fits
477
503
  Returns:
478
504
  None
479
505
  """
480
- if self.sites is None:
506
+ if self.flux_tower_instances is None:
481
507
  x_time_y = 0
482
508
  for h in self.sat_imgs:
483
509
  size_dict = dict(h.sat_img.sizes)
@@ -531,19 +557,33 @@ class vprm:
531
557
  {"timestamps": (dims, tismp)}
532
558
  )
533
559
  self.time_key = "time"
560
+
561
+ N = int(min_lenght_snow_period / np.diff(self.sat_imgs.sat_img['time']).mean())
534
562
  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
- )
563
+ self.sat_imgs.sat_img["evi"] = replace_inf_runs_ignore_nans(self.sat_imgs.sat_img["evi"],
564
+ N = N,
565
+ time_dim = self.time_key)
566
+ # self.sat_imgs.sat_img["evi"] = xr.where(
567
+ # (self.sat_imgs.sat_img["evi"] == np.inf),
568
+ # self.sat_imgs.sat_img["evi"].min(dim=self.time_key),
569
+ # self.sat_imgs.sat_img["evi"],
570
+ # )
540
571
 
541
572
  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
- )
573
+ self.sat_imgs.sat_img["lswi"] = replace_inf_runs_ignore_nans(self.sat_imgs.sat_img["lswi"],
574
+ N = N,
575
+ time_dim = self.time_key)
576
+ # self.sat_imgs.sat_img["lswi"] = xr.where(
577
+ # (self.sat_imgs.sat_img["lswi"] == np.inf),
578
+ # self.sat_imgs.sat_img["lswi"].min(dim=self.time_key),
579
+ # self.sat_imgs.sat_img["lswi"],
580
+ # )
581
+
582
+ if "ndvi" in list(self.sat_imgs.sat_img.data_vars):
583
+ self.sat_imgs.sat_img["ndvi"] = replace_inf_runs_ignore_nans(self.sat_imgs.sat_img["ndvi"],
584
+ N = N,
585
+ time_dim = self.time_key)
586
+
547
587
  return
548
588
 
549
589
  def clip_to_box(self, sat_to_crop):
@@ -617,9 +657,13 @@ class vprm:
617
657
  )
618
658
  src_grid.to_netcdf(src_temp_path)
619
659
  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
- )
660
+ logger.debug(f"wrote source regridder: {src_temp_path}")
661
+ logger.debug(f"wrote dest regridder: {dest_temp_path}")
662
+ exec_str = (
663
+ "ESMF_RegridWeightGen --source {} --destination {} "
664
+ "--weight {} -m conserve -r --netcdf4 --src_regional"
665
+ " --dest_regional --ignore_unmapped"
666
+ ).format(src_temp_path, dest_temp_path, regridder_save_path)
623
667
  if mpi is True:
624
668
  exec_str = "mpirun -np {} ".format(n_cpus) + exec_str
625
669
  if not logs:
@@ -740,7 +784,7 @@ class vprm:
740
784
  self.min_lswi = copy.deepcopy(self.prototype)
741
785
  self.min_max_evi = copy.deepcopy(self.prototype)
742
786
  shortcut = self.sat_imgs.sat_img
743
- # if self.sites is None:
787
+ # if self.flux_tower_instances is None:
744
788
  self.min_lswi.sat_img["min_lswi"] = shortcut["lswi"].min(
745
789
  self.time_key, skipna=True
746
790
  )
@@ -760,7 +804,8 @@ class vprm:
760
804
  )
761
805
  return
762
806
 
763
- def lowess(self, keys, lonlats=None, times=False, frac=0.25, it=3, n_cpus=None):
807
+ def lowess(self, keys, lonlats=None, times=False, frac=0.25, it=3,
808
+ n_cpus=None, smooth_all=False):
764
809
  """
765
810
  Performs the lowess smoothing
766
811
 
@@ -804,7 +849,7 @@ class vprm:
804
849
  xvals = self.sat_imgs.sat_img["time"]
805
850
  logger.info("Lowess timestamps {}".format(xvals))
806
851
 
807
- if self.sites is not None: # Is flux tower sites are given
852
+ if (self.flux_tower_instances is not None) and (smooth_all is False): # Is flux tower sites are given
808
853
  if "timestamps" in list(self.sat_imgs.sat_img.data_vars):
809
854
  for key in keys:
810
855
  self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
@@ -920,13 +965,21 @@ class vprm:
920
965
  )
921
966
  return
922
967
 
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
968
+ def clip_values(self, key, min_val, max_val, to_nan=False):
969
+ if to_nan:
970
+ self.sat_imgs.sat_img[key].values[
971
+ self.sat_imgs.sat_img[key].values < min_val
972
+ ] = np.nan
973
+ self.sat_imgs.sat_img[key].values[
974
+ self.sat_imgs.sat_img[key].values > max_val
975
+ ] = np.nan
976
+ else:
977
+ self.sat_imgs.sat_img[key].values[
978
+ self.sat_imgs.sat_img[key].values < min_val
979
+ ] = min_val
980
+ self.sat_imgs.sat_img[key].values[
981
+ self.sat_imgs.sat_img[key].values > max_val
982
+ ] = max_val
930
983
  return
931
984
 
932
985
  def clip_non_finite(self, data_var, val, sel):
@@ -1053,9 +1106,11 @@ class vprm:
1053
1106
  days_after_first_image = (
1054
1107
  datetime_utc - self.timestamp_start
1055
1108
  ).total_seconds() / (24 * 60 * 60)
1109
+ # Calculate a list of time differences between the xarray time and the datetime_utc both with respect to timestamp_start.
1110
+ # Take the index (argmin) with smallest difference
1056
1111
  counter_new = np.argmin(
1057
1112
  np.abs(self.sat_imgs.sat_img[self.time_key].values - days_after_first_image)
1058
- )
1113
+ )
1059
1114
  if (days_after_first_image < 0) | (
1060
1115
  days_after_first_image > self.sat_imgs.sat_img[self.time_key][-1]
1061
1116
  ):
@@ -0,0 +1,2 @@
1
+ __version__ = "5.1"
2
+
@@ -103,7 +103,7 @@ class brazil_flux_data(flux_tower_data):
103
103
  "par_fill",
104
104
  "ust",
105
105
  ]
106
- if use_vars is "all":
106
+ if use_vars == "all":
107
107
  self.vars = "all"
108
108
  site_info_dict = dict()
109
109
  site_info_dict["K67"] = dict(lat=-2.857, lon=-54.959, veg_class="EF")
@@ -188,7 +188,7 @@ class ameri_fluxnet(flux_tower_data):
188
188
  self.data_path = data_path
189
189
 
190
190
  super().__init__(t_start, t_stop, ssrd_key, t2m_key, site_name)
191
- if (use_vars is None) | (use_vars is "all"):
191
+ if (use_vars is None) | (use_vars == "all"):
192
192
  self.vars = "all"
193
193
  else:
194
194
  self.vars = use_vars
@@ -205,7 +205,7 @@ class ameri_fluxnet(flux_tower_data):
205
205
  return
206
206
 
207
207
  def add_tower_data(self):
208
- if self.vars is "all":
208
+ if self.vars == "all":
209
209
  idata = pd.read_csv(
210
210
  glob.glob(os.path.join(self.data_path, "*.csv"))[0], skiprows=2
211
211
  )
@@ -286,7 +286,7 @@ class fluxnet(flux_tower_data):
286
286
  "TA_F_QC",
287
287
  "SW_IN_F_QC",
288
288
  ]
289
- elif use_vars is "all":
289
+ elif use_vars == "all":
290
290
  self.vars = "all"
291
291
  else:
292
292
  self.vars = use_vars
@@ -351,6 +351,7 @@ class icos(flux_tower_data):
351
351
  use_vars=None,
352
352
  t_start=None,
353
353
  t_stop=None,
354
+ icos_2025=True,
354
355
  ):
355
356
 
356
357
  self.data_path = data_path
@@ -380,18 +381,44 @@ class icos(flux_tower_data):
380
381
  "TA_F_QC",
381
382
  "SW_IN_F_QC",
382
383
  ]
383
- elif use_vars is "all":
384
+ elif use_vars == "all":
384
385
  self.vars = "all"
385
386
  else:
386
387
  self.vars = use_vars
387
-
388
- site_info = pd.read_csv(
389
- os.path.join(
390
- os.path.dirname(self.data_path),
391
- "ICOSETC_{}_SITEINFO_L2.csv".format(self.site_name),
392
- ),
393
- on_bad_lines="skip",
394
- )
388
+ if icos_2025:
389
+ site_info = pd.read_csv(
390
+ os.path.join(
391
+ os.path.dirname(self.data_path).replace('FLUXNET_HH_L2', 'ARCHIVE_L2'),
392
+ "ICOSETC_{}_SITEINFO_L2.csv".format(self.site_name),
393
+ ),
394
+ on_bad_lines="skip",
395
+ )
396
+ self.footprint = pd.read_csv(
397
+ os.path.join(
398
+ os.path.dirname(self.data_path).replace('FLUXNET_HH_L2', 'ARCHIVE_L2'),
399
+ "ICOSETC_{}_FLUXES_L2.csv".format(self.site_name),
400
+ ),
401
+ on_bad_lines="skip",
402
+ )
403
+ if 'FETCH_50' in self.footprint.keys():
404
+ self.fetch50 = np.percentile(self.footprint['FETCH_50'][self.footprint['FETCH_50']>0], 90)
405
+ else:
406
+ self.fetch50 = None
407
+
408
+ if 'FETCH_90' in self.footprint.keys():
409
+ self.fetch90 = np.percentile(self.footprint['FETCH_90'][self.footprint['FETCH_90']>0], 50)
410
+ else:
411
+ self.fetch90 = None
412
+ else:
413
+ site_info = pd.read_csv(
414
+ os.path.join(
415
+ os.path.dirname(self.data_path),
416
+ "ICOSETC_{}_SITEINFO_L2.csv".format(self.site_name),
417
+ ),
418
+ on_bad_lines="skip",
419
+ )
420
+ self.fetch90 = None # Not yet implemented
421
+
395
422
  self.land_cover_type = site_info.loc[site_info["VARIABLE"] == "IGBP"][
396
423
  "DATAVALUE"
397
424
  ].values[0]
@@ -405,7 +432,7 @@ class icos(flux_tower_data):
405
432
  return
406
433
 
407
434
  def add_tower_data(self):
408
- if self.vars is "all":
435
+ if self.vars == "all":
409
436
  idata = pd.read_csv(self.data_path, on_bad_lines="skip")
410
437
  else:
411
438
  idata = pd.read_csv(