STIC-JPL 1.2.1__py3-none-any.whl → 1.2.2__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.

Potentially problematic release.


This version of STIC-JPL might be problematic. Click here for more details.

STIC_JPL/FVC_from_NDVI.py CHANGED
@@ -11,16 +11,39 @@ MAX_LAI = 10.0
11
11
 
12
12
  def FVC_from_NDVI(NDVI: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
13
13
  """
14
- Convert Normalized Difference Vegetation Index (NDVI) to Fractional Vegetation Cover (FVC).
14
+ Estimate Fractional Vegetation Cover (FVC) from Normalized Difference Vegetation Index (NDVI)
15
+ using a scaled NDVI approach.
16
+
17
+ This method linearly scales NDVI values between two endmembers:
18
+ - NDVIs: NDVI value for bare soil (typically ~0.04 ± 0.03)
19
+ - NDVIv: NDVI value for full vegetation (typically ~0.52 ± 0.03)
20
+
21
+ The resulting Fractional Vegetation Cover (FVC) is calculated as:
22
+
23
+ FVC = clip((NDVI - NDVIs) / (NDVIv - NDVIs), 0.0, 1.0)
24
+
25
+ This approach is based on the assumption that NDVI increases linearly with vegetation cover
26
+ between these two extremes, and is well-supported in the literature.
27
+
28
+ References:
29
+ Carlson, T. N., & Ripley, D. A. (1997). On the relation between NDVI, fractional vegetation cover,
30
+ and leaf area index. Remote Sensing of Environment, 62(3), 241–252.
31
+ https://doi.org/10.1016/S0034-4257(97)00104-1
32
+
33
+ Gutman, G., & Ignatov, A. (1998). The derivation of the green vegetation fraction from NOAA/AVHRR
34
+ data for use in numerical weather prediction models. International Journal of Remote Sensing,
35
+ 19(8), 1533–1543. https://doi.org/10.1080/014311698215333
15
36
 
16
37
  Parameters:
17
38
  NDVI (Union[Raster, np.ndarray]): Input NDVI data.
18
39
 
19
40
  Returns:
20
- Union[Raster, np.ndarray]: Converted FVC data.
41
+ Union[Raster, np.ndarray]: Estimated Fractional Vegetation Cover (FVC).
21
42
  """
22
- NDVIv = 0.52 # +- 0.03
23
- NDVIs = 0.04 # +- 0.03
43
+ NDVIv = 0.52 # NDVI for fully vegetated pixel
44
+ NDVIs = 0.04 # NDVI for bare soil pixel
45
+
46
+ # Scale NDVI to FVC using a linear model and clip to [0, 1]
24
47
  FVC = rt.clip((NDVI - NDVIs) / (NDVIv - NDVIs), 0.0, 1.0)
25
48
 
26
- return FVC
49
+ return FVC
STIC_JPL/LAI_from_NDVI.py CHANGED
@@ -3,7 +3,8 @@ import numpy as np
3
3
  import rasters as rt
4
4
  from rasters import Raster
5
5
 
6
- KPAR = 0.5
6
+ # Constants
7
+ KPAR = 0.5 # Extinction coefficient for PAR, assumed average for broadleaf canopies (Weiss & Baret, 2010)
7
8
  MIN_FIPAR = 0.0
8
9
  MAX_FIPAR = 1.0
9
10
  MIN_LAI = 0.0
@@ -16,16 +17,45 @@ def LAI_from_NDVI(
16
17
  min_LAI: float = MIN_LAI,
17
18
  max_LAI: float = MAX_LAI) -> Union[Raster, np.ndarray]:
18
19
  """
19
- Convert Normalized Difference Vegetation Index (NDVI) to Leaf Area Index (LAI).
20
+ Estimate Leaf Area Index (LAI) from NDVI using a simplified two-step empirical model.
21
+
22
+ This method first approximates the fraction of absorbed photosynthetically active radiation (fIPAR)
23
+ from NDVI, and then estimates LAI using the Beer–Lambert Law. The extinction coefficient for PAR (KPAR)
24
+ is assumed to be 0.5, which is typical for broadleaf canopies under diffuse light conditions.
25
+
26
+ Steps:
27
+ 1. fIPAR ≈ NDVI - 0.05 (empirical offset to account for soil background and sensor noise)
28
+ - Based on observed relationships in Myneni & Williams (1994)
29
+ 2. LAI = -ln(1 - fIPAR) / KPAR (Beer–Lambert Law)
30
+ - From Sellers (1985)
31
+
32
+ All outputs are clipped to user-defined minimum and maximum thresholds to ensure physical realism.
20
33
 
21
34
  Parameters:
22
35
  NDVI (Union[Raster, np.ndarray]): Input NDVI data.
36
+ min_fIPAR (float): Minimum fIPAR value for clipping (default 0.0).
37
+ max_fIPAR (float): Maximum fIPAR value for clipping (default 1.0).
38
+ min_LAI (float): Minimum LAI value for clipping (default 0.0).
39
+ max_LAI (float): Maximum LAI value for clipping (default 10.0).
23
40
 
24
41
  Returns:
25
- Union[Raster, np.ndarray]: Converted LAI data.
42
+ Union[Raster, np.ndarray]: Estimated LAI values.
43
+
44
+ References:
45
+ - Sellers, P. J. (1985). Canopy reflectance, photosynthesis and transpiration.
46
+ *International Journal of Remote Sensing*, 6(8), 1335–1372.
47
+ - Myneni, R. B., & Williams, D. L. (1994). On the relationship between FAPAR and NDVI.
48
+ *Remote Sensing of Environment*, 49(3), 200–211.
49
+ - Weiss, M., & Baret, F. (2010). CAN-EYE V6.1 User Manual. INRA-CSE.
50
+
26
51
  """
52
+ # Empirical conversion from NDVI to fIPAR (adjusted for background noise)
27
53
  fIPAR = rt.clip(NDVI - 0.05, min_fIPAR, max_fIPAR)
54
+
55
+ # Avoid division by zero or log of 0 by masking zero fIPAR values
28
56
  fIPAR = np.where(fIPAR == 0, np.nan, fIPAR)
57
+
58
+ # Apply Beer–Lambert law to estimate LAI
29
59
  LAI = rt.clip(-np.log(1 - fIPAR) * (1 / KPAR), min_LAI, max_LAI)
30
60
 
31
- return LAI
61
+ return LAI
STIC_JPL/model.py CHANGED
@@ -5,8 +5,11 @@ from os.path import join, abspath, expanduser
5
5
  from typing import Dict, List
6
6
  import numpy as np
7
7
  import warnings
8
- from .diagnostic import diagnostic
8
+
9
+ from pytictoc import TicToc
10
+
9
11
  import colored_logging as cl
12
+ from check_distribution import check_distribution
10
13
  import rasters as rt
11
14
  from GEOS5FP import GEOS5FP
12
15
  from solar_apparent_time import solar_day_of_year_for_area, solar_hour_of_day_for_area
@@ -29,8 +32,6 @@ from .FVC_from_NDVI import FVC_from_NDVI
29
32
  from .LAI_from_NDVI import LAI_from_NDVI
30
33
  from .celcius_to_kelvin import celcius_to_kelvin
31
34
 
32
- from .timer import Timer
33
-
34
35
  __author__ = 'Kaniska Mallick, Madeleine Pascolini-Campbell, Gregory Halverson'
35
36
 
36
37
  logger = logging.getLogger(__name__)
@@ -180,7 +181,7 @@ def STIC_JPL(
180
181
  G_method = DEFAULT_G_METHOD, # method for calculating soil heat flux
181
182
  )
182
183
 
183
- diagnostic(Ms, "Ms", **diag_kwargs)
184
+ check_distribution(Ms, "Ms")
184
185
 
185
186
  # STIC analytical equations (convergence on LE)
186
187
  gB_ms, gS_ms, dT_C, EF = STIC_closure(
@@ -227,7 +228,9 @@ def STIC_JPL(
227
228
  PT_Wm2 = None
228
229
  iteration = 1
229
230
  LE_Wm2_max_change = 0
230
- t = Timer()
231
+
232
+ t = TicToc()
233
+ t.tic()
231
234
 
232
235
  while (np.nanmax(LE_Wm2_change) >= LE_convergence_target and iteration <= max_iterations):
233
236
  logger.info(f"running STIC iteration {cl.val(iteration)} / {cl.val(max_iterations)}")
@@ -330,11 +333,11 @@ def STIC_JPL(
330
333
  LE_Wm2_old = LE_Wm2_new
331
334
  LE_Wm2_max_change = np.nanmax(LE_Wm2_change)
332
335
  logger.info(
333
- f"completed STIC iteration {cl.val(iteration)} / {cl.val(max_iterations)} with max LE change: {cl.val(np.round(LE_Wm2_max_change, 3))} ({t} seconds)")
336
+ f"completed STIC iteration {cl.val(iteration)} / {cl.val(max_iterations)} with max LE change: {cl.val(np.round(LE_Wm2_max_change, 3))} ({t.tocvalue()} seconds)")
334
337
 
335
- diagnostic(SM, f"SM_{iteration}", **diag_kwargs)
336
- diagnostic(G, f"G_{iteration}", **diag_kwargs)
337
- diagnostic(LE_Wm2_new, f"LE_{iteration}", **diag_kwargs)
338
+ check_distribution(SM, f"SM_{iteration}")
339
+ check_distribution(G, f"G_{iteration}")
340
+ check_distribution(LE_Wm2_new, f"LE_{iteration}")
338
341
 
339
342
  if LE_Wm2_max_change <= LE_convergence_target:
340
343
  logger.info(f"max LE change {cl.val(np.round(LE_Wm2_max_change, 3))} within convergence target {cl.val(np.round(LE_convergence_target, 3))} with {cl.val(iteration)} iteration{'s' if iteration > 1 else ''}")
STIC_JPL/version.txt CHANGED
@@ -1 +1 @@
1
- 1.2.1
1
+ 1.2.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: STIC-JPL
3
- Version: 1.2.1
3
+ Version: 1.2.2
4
4
  Summary: Surface Temperature Initiated Closure (STIC) Evapotranspiration Model Python Implementation
5
5
  Author-email: Gregory Halverson <gregory.h.halverson@jpl.nasa.gov>, Kaniska Mallick <kaniska.mallick@list.lu>, Madeleine Pascolini-Campbell <madeleine.a.pascolini-campbell@jpl.nasa.gov>, "Claire S. Villanueva-Weeks" <claire.s.villanueva-weeks@jpl.gov>
6
6
  Project-URL: Homepage, https://github.com/JPL-Evapotranspiration-Algorithms/STIC-JPL
@@ -8,6 +8,7 @@ Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Operating System :: OS Independent
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
+ Requires-Dist: check-distribution
11
12
  Requires-Dist: colored-logging
12
13
  Requires-Dist: ECOv002-CMR>=1.0.5
13
14
  Requires-Dist: ECOv002-granules>=1.0.3
@@ -16,6 +17,7 @@ Requires-Dist: GEOS5FP>=1.1.1
16
17
  Requires-Dist: monte-carlo-sensitivity
17
18
  Requires-Dist: numpy
18
19
  Requires-Dist: pandas
20
+ Requires-Dist: pytictoc
19
21
  Requires-Dist: rasters>=1.4.6
20
22
  Requires-Dist: seaborn
21
23
  Requires-Dist: SEBAL-soil-heat-flux
@@ -1,27 +1,25 @@
1
- STIC_JPL/FVC_from_NDVI.py,sha256=NCGMu4-XzvYqg7UOqKqNkfqwmV73zipoc09Q1IubNm0,638
2
- STIC_JPL/LAI_from_NDVI.py,sha256=iqlCkuH53iDrAz_VbMs4y4EwHcNuGGo5nJfJtnmsO70,833
1
+ STIC_JPL/FVC_from_NDVI.py,sha256=QiALDwRVoOmG9XLfQHZQhAfi8B7QuKc2cae6RIi9yYI,1852
2
+ STIC_JPL/LAI_from_NDVI.py,sha256=XgBIEtFG_sSAzNpb-v2E3rQ1HFoxw4gHhviQoiTVAPU,2529
3
3
  STIC_JPL/STIC_JPL.py,sha256=qqxz7JOEHgmP_hoz1ZT_66v0WNs9R0SITRt-60rYb3I,127
4
4
  STIC_JPL/__init__.py,sha256=FmTp-Ir0Wbae0SbZgxlNIxq48TDrWCev17Gv1hWskxU,216
5
5
  STIC_JPL/canopy_air_stream.py,sha256=UYp3l7mt0XH4SqvCGZhRzCIL-v7-Rvxxfm-C81lheSU,1637
6
6
  STIC_JPL/celcius_to_kelvin.py,sha256=QZ1gA8BHBHbdlgn1v8tEMG17yY2r-SCBnaHrGXySItg,316
7
7
  STIC_JPL/closure.py,sha256=AzWoLnpoZ1MlQAKq0CgpmCMfDGVsp8-hYZ50ry33cic,4031
8
8
  STIC_JPL/constants.py,sha256=xIVTsdc3_Rf3EPSgSk1QPjWiojFGn4lf1Swffov3oTM,595
9
- STIC_JPL/diagnostic.py,sha256=RXjtZbCEJMiTyRuiMGUvekDgO_GcrhXVXvorR0WlAsk,2613
10
9
  STIC_JPL/generate_STIC_inputs.py,sha256=Vwuqq-PLiH2IBd1v0Vt2TVbdIRK8D1zU_uMgReCefmQ,2382
11
10
  STIC_JPL/initialize_with_solar.py,sha256=P-22Wn8-sdvh2o4LGb9XfbCdXGWcOxdWwMzEYfx2Nfo,3759
12
11
  STIC_JPL/initialize_without_solar.py,sha256=M_7QfP2pr2w5sQO7UOW1GPqxpmQkjmsQ526BgYVVALk,4675
13
12
  STIC_JPL/iterate_with_solar.py,sha256=T9DezL59xaV63o6f7_ouCGcQwPi1dIvhlJB26TMGuDo,6373
14
13
  STIC_JPL/iterate_without_solar.py,sha256=09RERpN6tdZr6ORDhMwEFXwx8941vyn1HMJQ-Q_bY6s,6140
15
- STIC_JPL/model.py,sha256=PwN4T7Ic-TMz6c0Br39H0QGX4yK4IdEo2Ww0VP-nD0w,17323
14
+ STIC_JPL/model.py,sha256=W-4MPm4SHcQNGJGRV6xf823pbauFoDooY-YA-yg_I_w,17339
16
15
  STIC_JPL/net_radiation.py,sha256=Uwudsazul8V-x5t8KQLi3wpYi-wjMwT__eqZCRV2bIw,1187
17
16
  STIC_JPL/process_STIC_table.py,sha256=5s-fxbchyVnPCzUG5KZX-RrHZDpN4JVcwu42s821Kdw,1712
18
17
  STIC_JPL/root_zone_initialization.py,sha256=3JVKNDt3ebIiGVfuhBawjKs-BicNwkbfMprkzSxd4Cg,1581
19
18
  STIC_JPL/root_zone_iteration.py,sha256=1XOMFE3-TdJZzUU5vouflUO4NKaZwAW6s1KJrNez3Es,3787
20
19
  STIC_JPL/soil_moisture_initialization.py,sha256=wWhAmvNT8tAIm_Sul8mEj6VdNuj7K14TBGUjKfqJrso,5610
21
20
  STIC_JPL/soil_moisture_iteration.py,sha256=QJXOPMxxwIIskpx9zLkXUPfuhWgFPUBcRnGhZo2UjAw,6493
22
- STIC_JPL/timer.py,sha256=tn5e3NQmsh55Jp9Fstjf-8KJW4F8UIJs-d_ZLooFYE8,1610
23
- STIC_JPL/version.txt,sha256=IGYgplfxzMivcrhm_c7yDmQcSPOc4n8zjOgxOUnGPlM,5
24
- stic_jpl-1.2.1.dist-info/METADATA,sha256=jKGqTkUD0wSUa1LCmVP3fBSC7XMiLJbPYOAgnBw8C7o,5731
25
- stic_jpl-1.2.1.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
26
- stic_jpl-1.2.1.dist-info/top_level.txt,sha256=9NkchxttzACJcGcAaWzMaZarzX40OXQ216hERNA9LIo,9
27
- stic_jpl-1.2.1.dist-info/RECORD,,
21
+ STIC_JPL/version.txt,sha256=xipcxhrEUlk1dT9ewoTAoFKksdpLOjWA3OK313ohVK4,6
22
+ stic_jpl-1.2.2.dist-info/METADATA,sha256=bTOMDANHZrwipZlIVEgzKx-trZnQhi0T-px1i6rjgf0,5789
23
+ stic_jpl-1.2.2.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
24
+ stic_jpl-1.2.2.dist-info/top_level.txt,sha256=9NkchxttzACJcGcAaWzMaZarzX40OXQ216hERNA9LIo,9
25
+ stic_jpl-1.2.2.dist-info/RECORD,,
STIC_JPL/diagnostic.py DELETED
@@ -1,70 +0,0 @@
1
- from typing import Union
2
- from os.path import join
3
- from datetime import date
4
- import numpy as np
5
- import logging
6
-
7
- import colored_logging as cl
8
- from rasters import Raster
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
- def diagnostic(values: Union[Raster, np.ndarray], variable: str, show_distributions: bool = True, output_directory: str = None):
13
- if isinstance(values, Raster) and output_directory is not None:
14
- filename = join(output_directory, f"{variable}.tif")
15
- logger.info(filename)
16
- values.to_geotiff(filename)
17
-
18
- if show_distributions:
19
- unique = np.unique(values)
20
- nan_proportion = np.count_nonzero(np.isnan(values)) / np.size(values)
21
-
22
- if len(unique) < 10:
23
- logger.info(f"variable {cl.name(variable)} ({values.dtype}) has {cl.val(unique)} unique values")
24
-
25
- for value in unique:
26
- if np.isnan(value):
27
- count = np.count_nonzero(np.isnan(values))
28
- else:
29
- count = np.count_nonzero(values == value)
30
-
31
- if value == 0 or np.isnan(value):
32
- logger.info(f"* {cl.colored(value, 'red')}: {cl.colored(count, 'red')}")
33
- else:
34
- logger.info(f"* {cl.val(value)}: {cl.val(count)}")
35
- else:
36
- minimum = np.nanmin(values)
37
-
38
- if minimum < 0:
39
- minimum_string = cl.colored(f"{minimum:0.3f}", "red")
40
- else:
41
- minimum_string = cl.val(f"{minimum:0.3f}")
42
-
43
- maximum = np.nanmax(values)
44
-
45
- if maximum <= 0:
46
- maximum_string = cl.colored(f"{maximum:0.3f}", "red")
47
- else:
48
- maximum_string = cl.val(f"{maximum:0.3f}")
49
-
50
- if nan_proportion > 0.5:
51
- nan_proportion_string = cl.colored(f"{(nan_proportion * 100):0.2f}%", "yellow")
52
- elif nan_proportion == 1:
53
- nan_proportion_string = cl.colored(f"{(nan_proportion * 100):0.2f}%", "red")
54
- else:
55
- nan_proportion_string = cl.val(f"{(nan_proportion * 100):0.2f}%")
56
-
57
- message = "variable " + cl.name(variable) + \
58
- " min: " + minimum_string + \
59
- " mean: " + cl.val(f"{np.nanmean(values):0.3f}") + \
60
- " max: " + maximum_string + \
61
- " nan: " + nan_proportion_string
62
-
63
- if np.all(values == 0):
64
- message += " all zeros"
65
- logger.warning(message)
66
- else:
67
- logger.info(message)
68
-
69
- if nan_proportion == 1:
70
- raise ValueError(f"variable {variable} is blank")
STIC_JPL/timer.py DELETED
@@ -1,77 +0,0 @@
1
- """
2
- This is a minimalistic performance timer.
3
- """
4
- import time
5
-
6
- __author__ = "Gregory Halverson"
7
-
8
- DEFAULT_FORMAT = "0.2f"
9
-
10
- class Timer(object):
11
- """
12
- This is a minimalistic performance timer.
13
- """
14
-
15
- def __init__(self):
16
- self._start_time = None
17
- self._end_time = None
18
- self.start()
19
-
20
- def __enter__(self, *args, **kwargs):
21
- self.start()
22
- return self
23
-
24
- def __exit__(self, *args, **kwargs):
25
- self.end()
26
-
27
- def __repr__(self):
28
- # print("Timer.__repr__")
29
- return self.__format__(format_string=DEFAULT_FORMAT)
30
-
31
- def __str__(self):
32
- # print("Timer.__str__")
33
- return self.__repr__()
34
-
35
- def __format__(self, format_string=DEFAULT_FORMAT):
36
- if format_string is None or format_string == "":
37
- format_string = DEFAULT_FORMAT
38
-
39
- return format(self.duration, format_string)
40
-
41
- @property
42
- def now(self):
43
- # return datetime.now()
44
- return time.perf_counter()
45
-
46
- def start(self):
47
- self._start_time = self.now
48
-
49
- return self.start_time
50
-
51
- @property
52
- def start_time(self):
53
- return self._start_time
54
-
55
- def end(self):
56
- self._end_time = self.now
57
-
58
- return self.end_time
59
-
60
- @property
61
- def end_time(self):
62
- return self._end_time
63
-
64
- @property
65
- def duration(self):
66
- if self.start_time is None:
67
- raise Exception("timer never started")
68
-
69
- if self.end_time is None:
70
- end_time = self.now
71
- else:
72
- end_time = self.end_time
73
-
74
- duration = end_time - self.start_time
75
-
76
- return duration
77
-