pyVPRM 5.0__tar.gz → 5.2__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-5.0/pyVPRM.egg-info → pyvprm-5.2}/PKG-INFO +13 -2
  2. {pyvprm-5.0 → pyvprm-5.2}/README.md +33 -7
  3. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/VPRM.py +122 -72
  4. pyvprm-5.2/pyVPRM/__init__.py +3 -0
  5. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/flux_tower_class.py +41 -14
  6. pyvprm-5.2/pyVPRM/lib/functions.py +495 -0
  7. pyvprm-5.2/pyVPRM/meteorologies/era5_land_destinE.py +200 -0
  8. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/base_manager.py +13 -8
  9. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/modis.py +38 -0
  10. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/sentinel2.py +12 -0
  11. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/viirs.py +18 -12
  12. pyvprm-5.0/pyVPRM/vprm_models/vprm_base.py → pyvprm-5.2/pyVPRM/vprm_models/vprm_base_model.py +11 -8
  13. pyvprm-5.0/pyVPRM/vprm_models/vprm_base_no_xeric.py → pyvprm-5.2/pyVPRM/vprm_models/vprm_base_model_no_xeric.py +6 -6
  14. pyvprm-5.0/pyVPRM/vprm_models/vprm_modified.py → pyvprm-5.2/pyVPRM/vprm_models/vprm_modified_model.py +7 -7
  15. pyvprm-5.2/pyVPRM/vprm_models/vprm_nn_model.py +149 -0
  16. {pyvprm-5.0 → pyvprm-5.2/pyVPRM.egg-info}/PKG-INFO +13 -2
  17. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM.egg-info/SOURCES.txt +5 -4
  18. {pyvprm-5.0 → pyvprm-5.2}/setup.py +2 -1
  19. pyvprm-5.0/pyVPRM/__init__.py +0 -1
  20. pyvprm-5.0/pyVPRM/lib/functions.py +0 -471
  21. pyvprm-5.0/pyVPRM/vprm_models/vprm_nn.py +0 -130
  22. {pyvprm-5.0 → pyvprm-5.2}/LICENSE +0 -0
  23. {pyvprm-5.0 → pyvprm-5.2}/MANIFEST.in +0 -0
  24. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/__init__.py +0 -0
  25. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/downmodis.py +0 -0
  26. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/fancy_plot.py +0 -0
  27. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/fluxnet_info/fluxnet_sites.pkl +0 -0
  28. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/fluxnet_info/site_infos.txt +0 -0
  29. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/lib/reproject_fiona.py +0 -0
  30. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/meteorologies/__init__.py +0 -0
  31. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/meteorologies/era5_class_dkrz.py +0 -0
  32. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/meteorologies/era5_class_draft.py +0 -0
  33. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/meteorologies/era5_monthly_xr.py +0 -0
  34. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/meteorologies/met_base_class.py +0 -0
  35. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/meteorologies/met_local_measurement.py +0 -0
  36. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/__init__.py +0 -0
  37. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/city.py +0 -0
  38. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/copernicus.py +0 -0
  39. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/esa_world_cover.py +0 -0
  40. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/mapbiomas.py +0 -0
  41. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/proba_v.py +0 -0
  42. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/synmap.py +0 -0
  43. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/sat_managers/viirs09ga.py +0 -0
  44. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/vprm_configs/__init__.py +0 -0
  45. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/vprm_configs/copernicus_land_cover.yaml +0 -0
  46. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/vprm_configs/esa_world_cover.yaml +0 -0
  47. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/vprm_configs/synmap.yaml +0 -0
  48. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/vprm_models/__init__.py +0 -0
  49. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM/vprm_models/model_params/__init__.py +0 -0
  50. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM.egg-info/dependency_links.txt +0 -0
  51. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM.egg-info/not-zip-safe +0 -0
  52. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM.egg-info/requires.txt +0 -0
  53. {pyvprm-5.0 → pyvprm-5.2}/pyVPRM.egg-info/top_level.txt +0 -0
  54. {pyvprm-5.0 → pyvprm-5.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: pyVPRM
3
- Version: 5.0
3
+ Version: 5.2
4
4
  Summary: Vegetation Photosynthesis and Respiration Model
5
5
  Home-page: https://github.com/tglauch/pyVPRM/
6
6
  Author: Theo Glauch
@@ -12,6 +12,7 @@ Classifier: Environment :: Console
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
15
+ Classifier: Programming Language :: Python :: 3.14
15
16
  License-File: LICENSE
16
17
  Requires-Dist: numpy
17
18
  Requires-Dist: scipy
@@ -37,5 +38,15 @@ Requires-Dist: requests
37
38
  Requires-Dist: statsmodels
38
39
  Requires-Dist: loguru
39
40
  Requires-Dist: fiona
41
+ Dynamic: author
42
+ Dynamic: author-email
43
+ Dynamic: classifier
44
+ Dynamic: description
45
+ Dynamic: home-page
46
+ Dynamic: keywords
47
+ Dynamic: license
48
+ Dynamic: license-file
49
+ Dynamic: requires-dist
50
+ Dynamic: summary
40
51
 
41
52
  A tool to calculate the CO2 exchange flux between atmosphere and terrestrial biosphere using the Vegetation Photosynthesis and Respiration Model
@@ -1,13 +1,41 @@
1
1
  [![DOI](https://zenodo.org/badge/626435494.svg)](https://doi.org/10.5281/zenodo.14216613)
2
+ [![PyPI version](https://img.shields.io/pypi/v/pyVPRM.svg)](https://pypi.org/project/pyVPRM/)
3
+ [![License](https://img.shields.io/pypi/l/pyVPRM.svg)](https://pypi.org/project/pyVPRM/)
4
+ [![Downloads](https://img.shields.io/pypi/dm/pyVPRM.svg)](https://pypi.org/project/pyVPRM/)
5
+ ![GitHub stars](https://img.shields.io/github/stars/tglauch/pyVPRM?style=social)
2
6
 
3
7
 
8
+ <figure>
9
+ <img src="https://github.com/tglauch/pyVPRM/assets/29706254/ba2565e6-1434-4a95-8086-936462f8d05d", height=150pt>
10
+ </figure>
11
+
12
+ 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
4
13
 
5
- <img src="https://github.com/tglauch/pyVPRM/assets/29706254/ba2565e6-1434-4a95-8086-936462f8d05d" width=45% height=50%>
14
+ ```
15
+ 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
16
+ ```
17
+
18
+ or using BibTeX:
19
+ ```
20
+ @Article{gmd-18-4713-2025,
21
+ 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.},
22
+ TITLE = {\textit{pyVPRM}: a next-generation vegetation photosynthesis and respiration model for the post-MODIS era},
23
+ JOURNAL = {Geoscientific Model Development},
24
+ VOLUME = {18},
25
+ YEAR = {2025},
26
+ NUMBER = {14},
27
+ PAGES = {4713--4742},
28
+ URL = {https://gmd.copernicus.org/articles/18/4713/2025/},
29
+ DOI = {10.5194/gmd-18-4713-2025}
30
+ }
31
+ ```
32
+
33
+ In case of any questions please write an E-Mail to theo.glauch@dlr.de.
6
34
 
7
35
  # About
8
36
 
9
37
 
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.
38
+ `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
39
 
12
40
  Among others it can be used for
13
41
 
@@ -15,16 +43,14 @@ Among others it can be used for
15
43
  2. Making CO2 flux predictions for a given region of interest
16
44
  3. Generating input files to run VPRM in the Weather Research and Forecasting Model (WRF)
17
45
 
18
- In case of any questions please write an E-Mail to theo.glauch@dlr.de.
19
-
20
46
  ## Example Net Ecosystem Fluxes generated with pyVPRM
21
47
 
22
48
  <figure>
23
- <img src="https://github.com/user-attachments/assets/def61395-f90e-421b-9264-f0608161bd7a", height=300pt>
49
+ <img src="https://github.com/user-attachments/assets/c099ef1f-6c5a-445c-bc6e-f6697c47b641", height=300pt>
24
50
  </figure>
25
51
 
26
52
  <figure>
27
- <img src="https://github.com/user-attachments/assets/48c412b2-cf5f-4573-9db8-e60909893218", height=300pt>
53
+ <img src="https://github.com/user-attachments/assets/26913805-f188-477a-9a85-08911a165b1e", height=300pt>
28
54
  </figure>
29
55
 
30
56
 
@@ -63,7 +89,7 @@ In order to start your own project you need to at least follow theses steps:
63
89
  Remarks:
64
90
  - If there is not yet an interface for your satellite data or land cover map, implement a new subclass in `pyVPRM/sat_managers/`
65
91
  - 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
92
+ - 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
93
  - Open access to satellite data: MODIS,VIIRS: https://e4ftl01.cr.usgs.gov | Sentinel-2: https://scihub.copernicus.eu/
68
94
 
69
95
  ## 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
@@ -36,13 +37,13 @@ regridder_options = dict()
36
37
  regridder_options["conservative"] = "conserve"
37
38
 
38
39
 
39
- class vprm:
40
+ class vprm_preprocessor:
40
41
  """
41
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,55 +61,57 @@ 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 = []
73
71
  self.t2m = None
74
72
 
75
73
  # self.target_shape = None
76
-
77
- self.sat_img_buffer = dict()
74
+
78
75
  self.buffer = dict()
79
76
  self.buffer["cur_lat"] = None
80
77
  self.buffer["cur_lon"] = None
81
- self.prototype_lat_lon = None
78
+ self.empty_xr_lat_lon_grid = None
82
79
 
83
80
  self.land_cover_type = land_cover_map
84
81
  # land_cover_type: tmin, topt, tmax
85
82
 
86
83
  with open(vprm_config_path, "r") as stream:
87
84
  try:
88
- cfg = yaml.safe_load(stream)
85
+ self.vprm_cfg = yaml.safe_load(stream)
89
86
  except yaml.YAMLError as exc:
90
87
  logger.info(exc)
91
88
 
92
89
  self.temp_coefficients = dict()
93
90
  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"],
91
+ for key in self.vprm_cfg:
92
+ if "tmin" in self.vprm_cfg[key].keys():
93
+ self.temp_coefficients[self.vprm_cfg[key]["vprm_class"]] = [
94
+ self.vprm_cfg[key]["tmin"],
95
+ self.vprm_cfg[key]["topt"],
96
+ self.vprm_cfg[key]["tmax"],
97
+ self.vprm_cfg[key]["tlow"],
101
98
  ]
102
- for c in cfg[key]["class_numbers"]:
103
- self.map_to_vprm_class[c] = cfg[key]["vprm_class"]
99
+ for c in self.vprm_cfg[key]["class_numbers"]:
100
+ self.map_to_vprm_class[c] = self.vprm_cfg[key]["vprm_class"]
104
101
  return
105
102
 
103
+ def set_flux_tower_inst(self, flux_tower_instances):
104
+ self.flux_tower_instances = flux_tower_instances
105
+ if self.flux_tower_instances is not None:
106
+ self.lonlats = [i.get_lonlat() for i in flux_tower_instances]
107
+ return
108
+
106
109
  def to_wrf_output(
107
110
  self,
108
111
  out_grid,
109
112
  weights_for_regridder=None,
110
113
  regridder_save_path=None,
111
- driver="xEMSF",
114
+ driver="xESMF",
112
115
  interp_method="conservative",
113
116
  n_cpus=None,
114
117
  mpi=True,
@@ -144,7 +147,7 @@ class vprm:
144
147
  logger.info(
145
148
  "Need to generate the weights for the regridder. This can be very slow and memory intensive"
146
149
  )
147
- if driver == "xEMSF":
150
+ if driver == "xESMF":
148
151
  regridder = xe.Regridder(src_grid, ds_out, interp_method)
149
152
  if regridder_save_path is not None:
150
153
  regridder.to_netcdf(regridder_save_path)
@@ -182,7 +185,7 @@ class vprm:
182
185
  # os.remove(dest_temp_path)
183
186
  weights_for_regridder = regridder_save_path
184
187
  else:
185
- logger.info("Driver needs to be xEMSF or ESMF_RegridWeightGen")
188
+ logger.info("Driver needs to be xESMF or ESMF_RegridWeightGen")
186
189
  if weights_for_regridder is not None:
187
190
  regridder = xe.Regridder(
188
191
  src_grid,
@@ -277,7 +280,12 @@ class vprm:
277
280
  "evi_max": ds_t_max_evi,
278
281
  "evi_min": ds_t_min_evi,
279
282
  }
280
-
283
+
284
+ veg_classes_def = 'VPRM class definition: '
285
+ for k in self.vprm_cfg.keys():
286
+ veg_classes_def += '{} {}, '.format(self.vprm_cfg[k]['vprm_class'], k)
287
+ veg_classes_def = veg_classes_def[:-2]
288
+
281
289
  for key in ret_dict.keys():
282
290
  ret_dict[key] = ret_dict[key].assign_attrs(
283
291
  title="VPRM input data for WRF: {}".format(key),
@@ -289,7 +297,7 @@ class vprm:
289
297
  institution2="Deutsches Zentrum für Luft- und Raumfahrt (DLR)",
290
298
  contact="theo.glauch@dlr.de",
291
299
  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",
300
+ comment=veg_classes_def,
293
301
  )
294
302
  return ret_dict
295
303
 
@@ -302,10 +310,12 @@ class vprm:
302
310
  b_swir=None,
303
311
  drop_bands=False,
304
312
  which_evi=None,
313
+ add_ndvi=False,
305
314
  timestamp_key=None,
306
315
  mask_bad_pixels=True,
307
316
  mask_clouds=True,
308
317
  mask_snow=True,
318
+ mask_water=True,
309
319
  ):
310
320
  """
311
321
  Add a new satellite image and calculate EVI and LSWI if desired
@@ -375,12 +385,20 @@ class vprm:
375
385
  temp_lswi = xr.where((temp_lswi < -1) | (temp_lswi > 1), np.nan, temp_lswi)
376
386
  handler.sat_img["evi"] = temp_evi
377
387
  handler.sat_img["lswi"] = temp_lswi
388
+ if add_ndvi:
389
+ nir = handler.sat_img[b_nir]
390
+ red = handler.sat_img[b_red]
391
+ temp_ndvi = (nir-red)/(nir+red)
392
+ handler.sat_img['ndvi'] = temp_ndvi
393
+
378
394
  if timestamp_key is not None:
379
395
  handler.sat_img = handler.sat_img.rename({timestamp_key: "timestamps"})
380
396
 
381
397
  bands_to_mask = []
382
398
  if which_evi in ["evi", "evi2"]:
383
399
  bands_to_mask = ["evi", "lswi"]
400
+ if add_ndvi:
401
+ bands_to_mask.append('ndvi')
384
402
  else:
385
403
  for btm in [b_nir, b_red, b_blue, b_swir]:
386
404
  if btm is not None:
@@ -390,6 +408,11 @@ class vprm:
390
408
  handler.mask_snow()
391
409
  else:
392
410
  handler.mask_snow(bands_to_mask)
411
+ if mask_water:
412
+ if bands_to_mask == []:
413
+ handler.mask_water()
414
+ else:
415
+ handler.mask_water(bands_to_mask)
393
416
  if mask_clouds:
394
417
  if bands_to_mask == []:
395
418
  handler.mask_clouds()
@@ -443,6 +466,7 @@ class vprm:
443
466
  x, y = t.transform(ll[0], ll[1])
444
467
  x_ind = np.argmin(np.abs(x - xs))
445
468
  y_ind = np.argmin(np.abs(y - ys))
469
+ arsz = 3 # Hard Coded. Means only 3 by 3 pixes around the lonlat location are smoothed
446
470
  for key in keys:
447
471
  logger.info(key)
448
472
  self.sat_imgs.sat_img[key][
@@ -464,20 +488,21 @@ class vprm:
464
488
  interp_method="nearest",
465
489
  )
466
490
  self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign_coords(
467
- {"site_names": [i.get_site_name() for i in self.sites]}
491
+ {"site_names": [i.get_site_name() for i in self.flux_tower_instances]}
468
492
  )
469
493
 
470
- def sort_and_merge_by_timestamp(self):
494
+ def sort_and_merge_by_timestamp(self, min_lenght_snow_period=21):
471
495
  """
472
496
  Called after adding the satellite images with 'add_sat_img'. Sorts the satellite
473
497
  images by timestamp and merges everything to one satellite_data_manager.
474
498
 
475
499
  Parameters:
476
-
500
+ min_lenght_snow_period defines how long a period should be until gaps are
501
+ filled to stabilize lowess fits
477
502
  Returns:
478
503
  None
479
504
  """
480
- if self.sites is None:
505
+ if self.flux_tower_instances is None:
481
506
  x_time_y = 0
482
507
  for h in self.sat_imgs:
483
508
  size_dict = dict(h.sat_img.sizes)
@@ -485,15 +510,15 @@ class vprm:
485
510
  if prod > x_time_y:
486
511
  biggest = h
487
512
  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)
513
+ self.prototype_satellite_manager = copy.deepcopy(biggest)
514
+ keys = list(self.prototype_satellite_manager.sat_img.keys())
515
+ self.prototype_satellite_manager.sat_img = self.prototype_satellite_manager.sat_img.drop(keys)
491
516
  # for h in self.sat_imgs:
492
- # h.sat_img = h.sat_img.rio.reproject_match(self.prototype.sat_img, nodata=np.nan)
517
+ # h.sat_img = h.sat_img.rio.reproject_match(self.prototype_satellite_manager.sat_img, nodata=np.nan)
493
518
  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)
519
+ self.prototype_satellite_manager = copy.deepcopy(self.sat_imgs[0])
520
+ keys = list(self.prototype_satellite_manager.sat_img.keys())
521
+ self.prototype_satellite_manager.sat_img = self.prototype_satellite_manager.sat_img.drop(keys)
497
522
  self.sat_imgs = satellite_data_manager(
498
523
  sat_img=xr.concat([k.sat_img for k in self.sat_imgs], "time")
499
524
  )
@@ -510,7 +535,7 @@ class vprm:
510
535
  )
511
536
  day_steps = [i.days for i in (self.timestamps - self.timestamp_start)]
512
537
  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(
538
+ self.prototype_satellite_manager.sat_img = self.prototype_satellite_manager.sat_img.assign_coords(
514
539
  {"time": day_steps}
515
540
  )
516
541
 
@@ -531,19 +556,33 @@ class vprm:
531
556
  {"timestamps": (dims, tismp)}
532
557
  )
533
558
  self.time_key = "time"
559
+
560
+ N = int(min_lenght_snow_period / np.diff(self.sat_imgs.sat_img['time']).mean())
534
561
  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
- )
562
+ self.sat_imgs.sat_img["evi"] = replace_inf_runs_ignore_nans(self.sat_imgs.sat_img["evi"],
563
+ N = N,
564
+ time_dim = self.time_key)
565
+ # self.sat_imgs.sat_img["evi"] = xr.where(
566
+ # (self.sat_imgs.sat_img["evi"] == np.inf),
567
+ # self.sat_imgs.sat_img["evi"].min(dim=self.time_key),
568
+ # self.sat_imgs.sat_img["evi"],
569
+ # )
540
570
 
541
571
  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
- )
572
+ self.sat_imgs.sat_img["lswi"] = replace_inf_runs_ignore_nans(self.sat_imgs.sat_img["lswi"],
573
+ N = N,
574
+ time_dim = self.time_key)
575
+ # self.sat_imgs.sat_img["lswi"] = xr.where(
576
+ # (self.sat_imgs.sat_img["lswi"] == np.inf),
577
+ # self.sat_imgs.sat_img["lswi"].min(dim=self.time_key),
578
+ # self.sat_imgs.sat_img["lswi"],
579
+ # )
580
+
581
+ if "ndvi" in list(self.sat_imgs.sat_img.data_vars):
582
+ self.sat_imgs.sat_img["ndvi"] = replace_inf_runs_ignore_nans(self.sat_imgs.sat_img["ndvi"],
583
+ N = N,
584
+ time_dim = self.time_key)
585
+
547
586
  return
548
587
 
549
588
  def clip_to_box(self, sat_to_crop):
@@ -552,7 +591,7 @@ class vprm:
552
591
  bounds[0], bounds[1], bounds[2], bounds[3]
553
592
  )
554
593
  keys = list(self.sat_imgs.sat_img.keys())
555
- self.prototype = satellite_data_manager(
594
+ self.prototype_satellite_manager = satellite_data_manager(
556
595
  sat_img=self.sat_imgs.sat_img.drop(keys)
557
596
  )
558
597
  return
@@ -708,7 +747,7 @@ class vprm:
708
747
  .to_array()
709
748
  .values[0]
710
749
  )
711
- self.land_cover_type = copy.deepcopy(self.prototype)
750
+ self.land_cover_type = copy.deepcopy(self.prototype_satellite_manager)
712
751
  self.land_cover_type.sat_img = self.land_cover_type.sat_img.assign(
713
752
  {var_name: (["y", "x"], t)}
714
753
  )
@@ -740,11 +779,11 @@ class vprm:
740
779
  Returns:
741
780
  None
742
781
  """
743
- self.max_lswi = copy.deepcopy(self.prototype)
744
- self.min_lswi = copy.deepcopy(self.prototype)
745
- self.min_max_evi = copy.deepcopy(self.prototype)
782
+ self.max_lswi = copy.deepcopy(self.prototype_satellite_manager)
783
+ self.min_lswi = copy.deepcopy(self.prototype_satellite_manager)
784
+ self.min_max_evi = copy.deepcopy(self.prototype_satellite_manager)
746
785
  shortcut = self.sat_imgs.sat_img
747
- # if self.sites is None:
786
+ # if self.flux_tower_instances is None:
748
787
  self.min_lswi.sat_img["min_lswi"] = shortcut["lswi"].min(
749
788
  self.time_key, skipna=True
750
789
  )
@@ -764,7 +803,8 @@ class vprm:
764
803
  )
765
804
  return
766
805
 
767
- def lowess(self, keys, lonlats=None, times=False, frac=0.25, it=3, n_cpus=None):
806
+ def lowess(self, keys, lonlats=None, times=False, frac=0.25, it=3,
807
+ n_cpus=None, smooth_all=False):
768
808
  """
769
809
  Performs the lowess smoothing
770
810
 
@@ -808,7 +848,7 @@ class vprm:
808
848
  xvals = self.sat_imgs.sat_img["time"]
809
849
  logger.info("Lowess timestamps {}".format(xvals))
810
850
 
811
- if self.sites is not None: # Is flux tower sites are given
851
+ if (self.flux_tower_instances is not None) and (smooth_all is False): # Is flux tower sites are given
812
852
  if "timestamps" in list(self.sat_imgs.sat_img.data_vars):
813
853
  for key in keys:
814
854
  self.sat_imgs.sat_img = self.sat_imgs.sat_img.assign(
@@ -924,13 +964,21 @@ class vprm:
924
964
  )
925
965
  return
926
966
 
927
- def clip_values(self, key, min_val, max_val):
928
- self.sat_imgs.sat_img[key].values[
929
- self.sat_imgs.sat_img[key].values < min_val
930
- ] = min_val
931
- self.sat_imgs.sat_img[key].values[
932
- self.sat_imgs.sat_img[key].values > max_val
933
- ] = max_val
967
+ def clip_values(self, key, min_val, max_val, to_nan=False):
968
+ if to_nan:
969
+ self.sat_imgs.sat_img[key].values[
970
+ self.sat_imgs.sat_img[key].values < min_val
971
+ ] = np.nan
972
+ self.sat_imgs.sat_img[key].values[
973
+ self.sat_imgs.sat_img[key].values > max_val
974
+ ] = np.nan
975
+ else:
976
+ self.sat_imgs.sat_img[key].values[
977
+ self.sat_imgs.sat_img[key].values < min_val
978
+ ] = min_val
979
+ self.sat_imgs.sat_img[key].values[
980
+ self.sat_imgs.sat_img[key].values > max_val
981
+ ] = max_val
934
982
  return
935
983
 
936
984
  def clip_non_finite(self, data_var, val, sel):
@@ -1024,7 +1072,7 @@ class vprm:
1024
1072
  ).values.flatten()
1025
1073
  else:
1026
1074
  self.buffer[key] = self.sat_imgs.sat_img[key].isel(select_dict)
1027
- return self.sat_img_buffer[key]
1075
+ return self.buffer[key]
1028
1076
 
1029
1077
  def get_sat_img_values_for_all_keys(self, lon=None, lat=None, counter_range=None):
1030
1078
  """
@@ -1057,9 +1105,11 @@ class vprm:
1057
1105
  days_after_first_image = (
1058
1106
  datetime_utc - self.timestamp_start
1059
1107
  ).total_seconds() / (24 * 60 * 60)
1108
+ # Calculate a list of time differences between the xarray time and the datetime_utc both with respect to timestamp_start.
1109
+ # Take the index (argmin) with smallest difference
1060
1110
  counter_new = np.argmin(
1061
1111
  np.abs(self.sat_imgs.sat_img[self.time_key].values - days_after_first_image)
1062
- )
1112
+ )
1063
1113
  if (days_after_first_image < 0) | (
1064
1114
  days_after_first_image > self.sat_imgs.sat_img[self.time_key][-1]
1065
1115
  ):
@@ -1074,21 +1124,21 @@ class vprm:
1074
1124
  self.new = False
1075
1125
  return # Still same satellite image
1076
1126
 
1077
- def _set_prototype_lat_lon(self):
1078
- src_x = self.prototype.sat_img.coords["x"].values
1079
- src_y = self.prototype.sat_img.coords["y"].values
1127
+ def _set_empty_xr_lat_lon_grid(self):
1128
+ src_x = self.prototype_satellite_manager.sat_img.coords["x"].values
1129
+ src_y = self.prototype_satellite_manager.sat_img.coords["y"].values
1080
1130
  X, Y = np.meshgrid(src_x, src_y)
1081
1131
  t = Transformer.from_crs(
1082
- self.prototype.sat_img.rio.crs, "+proj=longlat +datum=WGS84"
1132
+ self.prototype_satellite_manager.sat_img.rio.crs, "+proj=longlat +datum=WGS84"
1083
1133
  )
1084
1134
  x_long, y_lat = t.transform(X, Y)
1085
- self.prototype_lat_lon = xr.Dataset(
1135
+ self.empty_xr_lat_lon_grid = xr.Dataset(
1086
1136
  {
1087
1137
  "lon": (["y", "x"], x_long, {"units": "degrees_east"}),
1088
1138
  "lat": (["y", "x"], y_lat, {"units": "degrees_north"}),
1089
1139
  }
1090
1140
  )
1091
- self.prototype_lat_lon = self.prototype_lat_lon.set_coords(["lon", "lat"])
1141
+ self.empty_xr_lat_lon_grid = self.empty_xr_lat_lon_grid.set_coords(["lon", "lat"])
1092
1142
  return
1093
1143
 
1094
1144
  def save(self, save_path):
@@ -1099,7 +1149,7 @@ class vprm:
1099
1149
  return
1100
1150
 
1101
1151
  def is_disjoint(self, this_sat_img):
1102
- bounds = self.prototype.sat_img.rio.transform_bounds(
1152
+ bounds = self.prototype_satellite_manager.sat_img.rio.transform_bounds(
1103
1153
  this_sat_img.sat_img.rio.crs
1104
1154
  )
1105
1155
  dj = rasterio.coords.disjoint_bounds(bounds, this_sat_img.sat_img.rio.bounds())
@@ -1112,7 +1162,7 @@ class vprm:
1112
1162
  [v.sat_imgs for v in vprm_insts], reproject=allow_reproject
1113
1163
  )
1114
1164
  keys = list(self.sat_imgs.sat_img.keys())
1115
- self.prototype = satellite_data_manager(
1165
+ self.prototype_satellite_manager = satellite_data_manager(
1116
1166
  sat_img=self.sat_imgs.sat_img.drop(keys)
1117
1167
  )
1118
1168
 
@@ -0,0 +1,3 @@
1
+ __version__ = "5.2"
2
+
3
+
@@ -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(