STIC-JPL 1.6.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.
@@ -0,0 +1,144 @@
1
+ from typing import Union, Tuple
2
+ import numpy as np
3
+
4
+ import rasters as rt
5
+
6
+ from rasters import Raster
7
+
8
+ from SEBAL_soil_heat_flux import calculate_SEBAL_soil_heat_flux
9
+
10
+ from .constants import *
11
+ from .canopy_air_stream import calculate_canopy_air_stream_vapor_pressure
12
+ from .soil_moisture_iteration import iterate_soil_moisture
13
+
14
+
15
+ def iterate_with_solar(
16
+ seconds_of_day: Union[Raster, np.ndarray], # Seconds of the day
17
+ ST_C: Union[Raster, np.ndarray], # Soil temperature (°C)
18
+ NDVI: Union[Raster, np.ndarray], # Normalized Difference Vegetation Index
19
+ albedo: Union[Raster, np.ndarray], # Albedo
20
+ gB_ms: Union[Raster, np.ndarray], # boundary layer conductance (m/s)
21
+ gS_ms: Union[Raster, np.ndarray], # stomatal conductance (m/s)
22
+ LE_Wm2: Union[Raster, np.ndarray], # latent heat flux (W/m^2)
23
+ Rg_Wm2: Union[Raster, np.ndarray], # Incoming solar radiation (W/m^2)
24
+ Rn_Wm2: Union[Raster, np.ndarray], # Net radiation (W/m^2)
25
+ LWnet_Wm2: Union[Raster, np.ndarray], # Net longwave radiation (W/m^2)
26
+ Ta_C: Union[Raster, np.ndarray], # Air temperature (°C)
27
+ dTS_C: Union[Raster, np.ndarray], # Change in soil temperature (°C)
28
+ Td_C: Union[Raster, np.ndarray], # Dew point temperature (°C)
29
+ Tsd_C: Union[Raster, np.ndarray], # Soil dew point temperature (°C)
30
+ Ea_hPa: Union[Raster, np.ndarray], # actual vapor pressure (hPa)
31
+ Estar_hPa: Union[Raster, np.ndarray], # saturation vapor pressure at surface temperature (hPa)
32
+ VPD_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit (hPa)
33
+ SVP_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure (hPa)
34
+ delta_hPa: Union[Raster, np.ndarray], # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
35
+ phi_Wm2: Union[Raster, np.ndarray], # Net radiation minus soil heat flux (W/m^2)
36
+ Es_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure (hPa)
37
+ s1: Union[Raster, np.ndarray], # Soil moisture parameter
38
+ s3: Union[Raster, np.ndarray], # Soil moisture parameter
39
+ FVC: Union[Raster, np.ndarray], # Fractional canopy cover
40
+ T0_C: Union[Raster, np.ndarray], # Reference temperature (°C)
41
+ gB_by_gS: Union[Raster, np.ndarray], # Ratio of boundary layer conductance to stomatal conductance
42
+ rho_kgm3: Union[Raster, np.ndarray, float] = RHO_KGM3, # Air density (kg/m^3)
43
+ Cp_Jkg: Union[Raster, np.ndarray, float] = CP_JKG, # Specific heat at constant pressure (J/kg/K)
44
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA, # Psychrometric constant (hPa/°C)
45
+ G_method: str = "santanello" # Method for calculating soil heat flux
46
+ ) -> Tuple[Union[Raster, np.ndarray]]:
47
+ """
48
+ This function calculates the canopy air stream vapor pressures, vapor pressure deficit at source,
49
+ vapor pressure at the reference height, soil moisture, soil heat flux, and recompute phi.
50
+
51
+ Parameters:
52
+ seconds_of_day (np.ndarray): Seconds of the day
53
+ ST_C (np.ndarray): Soil temperature (°C)
54
+ NDVI (np.ndarray): Normalized Difference Vegetation Index
55
+ albedo (np.ndarray): Albedo
56
+ gB (np.ndarray): Boundary layer conductance (m/s)
57
+ gS (np.ndarray): Stomatal conductance (m/s)
58
+ LE (np.ndarray): Latent heat flux (W/m^2)
59
+ Rg (np.ndarray): Incoming solar radiation (W/m^2)
60
+ Rn (np.ndarray): Net radiation (W/m^2)
61
+ LWnet (np.ndarray): Net longwave radiation (W/m^2)
62
+ Ta_C (np.ndarray): Air temperature (°C)
63
+ dTS (np.ndarray): Change in soil temperature (°C)
64
+ Td_C (np.ndarray): Dew point temperature (°C)
65
+ Tsd_C (np.ndarray): Soil dew point temperature (°C)
66
+ Ea_hPa (np.ndarray): Actual vapor pressure (hPa)
67
+ Estar (np.ndarray): Saturation vapor pressure at surface temperature (hPa)
68
+ VPD_hPa (np.ndarray): Vapor pressure deficit (hPa)
69
+ SVP_hPa (np.ndarray): Saturation vapor pressure (hPa)
70
+ delta (np.ndarray): Slope of the saturation vapor pressure-temperature curve (hPa/°C)
71
+ phi (np.ndarray): Net radiation minus soil heat flux (W/m^2)
72
+ Es (np.ndarray): Saturation vapor pressure (hPa)
73
+ s1 (np.ndarray): Soil moisture parameter
74
+ s3 (np.ndarray): Soil moisture parameter
75
+ fc (np.ndarray): Fractional canopy cover
76
+ T0 (np.ndarray): Reference temperature (°C)
77
+ gB_by_gS (np.ndarray): Ratio of boundary layer conductance to stomatal conductance
78
+ gamma (np.ndarray): Psychrometric constant (hPa/°C)
79
+ rho (np.ndarray): Air density (kg/m^3)
80
+ cp (np.ndarray): Specific heat at constant pressure (J/kg/K)
81
+ G_method (str): Method for calculating soil heat flux (santanello or SEBAL)
82
+
83
+ Returns:
84
+ e0star (np.ndarray): Canopy air stream vapor pressures
85
+ D0 (np.ndarray): Vapor pressure deficit at source
86
+ e0 (np.ndarray): Vapor pressure at the reference height
87
+ SM (np.ndarray): Soil moisture
88
+ G (np.ndarray): Soil heat flux
89
+ """
90
+ # canopy air stream vapor pressures
91
+ e0star = calculate_canopy_air_stream_vapor_pressure(
92
+ LE=LE_Wm2,
93
+ Ea_hPa=Ea_hPa,
94
+ Estar=Estar_hPa,
95
+ gB=gB_ms,
96
+ gS=gS_ms
97
+ )
98
+
99
+ # vapor pressure deficit at source
100
+ D0 = VPD_hPa + (delta_hPa * phi_Wm2 - (delta_hPa + gamma_hPa) * LE_Wm2) / (rho_kgm3 * Cp_Jkg * gB_ms)
101
+
102
+ # Vapor pressure at the reference height (hPa)
103
+ e0 = e0star - D0
104
+ e0 = rt.where(e0 < 0, Es_hPa, e0)
105
+ e0 = rt.where(e0 > e0star, e0star, e0)
106
+
107
+ # calculate soil moisture
108
+ SM = iterate_soil_moisture(
109
+ delta_hPa=delta_hPa,
110
+ s1=s1,
111
+ s3=s3,
112
+ ST_C=ST_C,
113
+ Ta_C=Ta_C,
114
+ dTS_C=dTS_C,
115
+ Td_C=Td_C,
116
+ Tsd_C=Tsd_C,
117
+ Rg_Wm2=Rg_Wm2,
118
+ Rn_Wm2=Rn_Wm2,
119
+ LWnet_Wm2=LWnet_Wm2,
120
+ FVC=FVC,
121
+ VPD_hPa=VPD_hPa,
122
+ D0_hPa=D0,
123
+ SVP_hPa=SVP_hPa,
124
+ Ea_hPa=Ea_hPa,
125
+ T0=T0_C,
126
+ gamma_hPa=gamma_hPa
127
+ )
128
+
129
+ # calculate soil heat flux
130
+ G = calculate_SEBAL_soil_heat_flux(
131
+ ST_C=ST_C,
132
+ NDVI=NDVI,
133
+ albedo=albedo,
134
+ Rn=Rn_Wm2
135
+ )
136
+
137
+ # recompute phi
138
+ phi_Wm2 = Rn_Wm2 - G
139
+
140
+ #
141
+ alphaN = ((gS_ms * (e0star - Ea_hPa) * (2 * delta_hPa + 2 * gamma_hPa + gamma_hPa * gB_by_gS * (1 + SM))) / (
142
+ 2 * delta_hPa * (gamma_hPa * (T0_C - Ta_C) * (gB_ms + gS_ms) + gS_ms * (e0star - Ea_hPa))))
143
+
144
+ return SM, G, e0, e0star, D0, alphaN
@@ -0,0 +1,121 @@
1
+ from typing import Union, Tuple
2
+ import numpy as np
3
+
4
+ import rasters as rt
5
+ from rasters import Raster
6
+
7
+ from .constants import *
8
+ from .canopy_air_stream import calculate_canopy_air_stream_vapor_pressure
9
+ from .root_zone_initialization import calculate_root_zone_moisture
10
+
11
+ def iterate_without_solar(
12
+ LE: Union[Raster, np.ndarray], # Latent heat flux (W/m^2)
13
+ PET: Union[Raster, np.ndarray], # Potential evapotranspiration (W/m^2)
14
+ SM: Union[Raster, np.ndarray],
15
+ ST_C: Union[Raster, np.ndarray], # Surface temperature (°C)
16
+ Ta_C: Union[Raster, np.ndarray], # Air temperature (°C)
17
+ dTS: Union[Raster, np.ndarray], # Surface-air temperature difference (°C)
18
+ T0: Union[Raster, np.ndarray], # Reference temperature (°C)
19
+ gB: Union[Raster, np.ndarray], # Boundary layer conductance (m/s)
20
+ gS: Union[Raster, np.ndarray], # Stomatal conductance (m/s)
21
+ Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure (hPa)
22
+ Td_C: Union[Raster, np.ndarray], # Dew point temperature (°C)
23
+ VPD_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit (hPa)
24
+ Estar: Union[Raster, np.ndarray], # Saturation vapor pressure at surface temperature (hPa)
25
+ delta: Union[Raster, np.ndarray], # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
26
+ phi: Union[Raster, np.ndarray], # available energy (W/m^2)
27
+ Ds: Union[Raster, np.ndarray], # Vapor pressure deficit at source (hPa)
28
+ Es: Union[Raster, np.ndarray], # Saturation vapor pressure (hPa)
29
+ s3: Union[Raster, np.ndarray], # Slope of the saturation vapor pressure and temperature
30
+ s4: Union[Raster, np.ndarray], # Slope of the saturation vapor pressure and temperature
31
+ gB_by_gS: Union[Raster, np.ndarray], # Ratio of boundary layer conductance to stomatal conductance
32
+ gamma_hPa: Union[Raster, np.ndarray] = GAMMA_HPA, # Psychrometric constant (hPa/°C)
33
+ rho_kgm3: Union[Raster, np.ndarray] = RHO_KGM3, # Air density (kg/m^3)
34
+ Cp_Jkg: Union[Raster, np.ndarray] = CP_JKG, # Specific heat at constant pressure (J/kg/K)
35
+ ) -> Tuple[Union[Raster, np.ndarray]]:
36
+ """
37
+ Iterate STIC model without knowing solar radiation.
38
+
39
+ Parameters:
40
+ LE (np.ndarray): Latent heat flux (W/m^2)
41
+ PET (np.ndarray): Potential evapotranspiration (W/m^2)
42
+ ST_C (np.ndarray): Surface temperature (°C)
43
+ Ta_C (np.ndarray): Air temperature (°C)
44
+ dTS (np.ndarray): Surface-air temperature difference (°C)
45
+ T0 (np.ndarray): Reference temperature (°C)
46
+ gB (np.ndarray): Boundary layer conductance (m/s)
47
+ gS (np.ndarray): Stomatal conductance (m/s)
48
+ Ea_hPa (np.ndarray): Actual vapor pressure (hPa)
49
+ Td_C (np.ndarray): Dew point temperature (°C)
50
+ VPD_hPa (np.ndarray): Vapor pressure deficit (hPa)
51
+ Estar (np.ndarray): Saturation vapor pressure at surface temperature (hPa)
52
+ delta (np.ndarray): Slope of the saturation vapor pressure-temperature curve (hPa/°C)
53
+ phi (np.ndarray): available energy (W/m^2)
54
+ Ds (np.ndarray): Vapor pressure deficit at source (hPa)
55
+ Es (np.ndarray): Saturation vapor pressure (hPa)
56
+ s3 (np.ndarray): Slope of the saturation vapor pressure and temperature
57
+ s4 (np.ndarray): Soil moisture parameter
58
+ gB_by_gS (np.ndarray): Ratio of boundary layer conductance to stomatal conductance
59
+ gamma (np.ndarray): Psychrometric constant (hPa/°C)
60
+ rho (np.ndarray): Air density (kg/m^3)
61
+ cp (np.ndarray): Specific heat at constant pressure (J/kg/K)
62
+
63
+ Returns:
64
+ SM (np.ndarray): soil moisture (m³/m³)
65
+ SMrz (np.ndarray): Root zone moisture (m³/m³)
66
+ Ms (np.ndarray): Surface Moisture
67
+ s1 (np.ndarray): lope of the saturation vapor pressure and temperature
68
+ e0 (np.ndarray): Vapor pressure at the reference height (hPa)
69
+ e0star (np.ndarray): Canopy air stream vapor pressures (hPa)
70
+ Tsd_C (np.ndarray): Soil dew point temperature (°C)
71
+ D0 (np.ndarray): Vapor pressure deficit at source (hPa)
72
+ alphaN (np.ndarray): Alpha parameter
73
+ """
74
+
75
+ # canopy air stream vapor pressures
76
+ e0star = calculate_canopy_air_stream_vapor_pressure(
77
+ LE=LE, # latent heat flux (W/m^2)
78
+ Ea_hPa=Ea_hPa, # actual vapor pressure (hPa)
79
+ Estar=Estar, # saturation vapor pressure at surface temperature (hPa)
80
+ gB=gB, # boundary layer conductance (m/s)
81
+ gS=gS # stomatal conductance (m/s),
82
+
83
+ )
84
+
85
+ # vapor pressure deficit at source
86
+ D0 = VPD_hPa + (delta * phi - (delta + gamma_hPa) * LE) / (rho_kgm3 * Cp_Jkg * gB)
87
+ D0 = rt.where(D0 < 0, Ds, D0)
88
+
89
+ # Vapor pressure at the reference height (hPa)
90
+ e0 = rt.clip(e0star - D0, Es, e0star)
91
+
92
+ # re-estimating M (direct LST feedback into M computation)
93
+ s1 = (45.03 + 3.014 * Td_C + 0.05345 * Td_C ** 2 + 0.00224 * Td_C ** 3) * 1e-2 # Soil moisture parameter
94
+ Tsd_C = Td_C + (gamma_hPa * LE) / (rho_kgm3 * Cp_Jkg * gB * s1) # Soil dew point temperature (°C)
95
+
96
+ # Surface Moisture Ms
97
+ Ms = rt.clip(s1 * (Tsd_C - Td_C) / (s3 * (ST_C - Td_C)), 0, 1)
98
+
99
+ # Root zone moisture Mrz
100
+ SMrz = calculate_root_zone_moisture(
101
+ delta_hPa=delta, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
102
+ ST_C=ST_C, # Surface temperature (°C)
103
+ Ta_C=Ta_C, # Air temperature (°C)
104
+ Td_C=Td_C, # Dew point temperature (°C)
105
+ s11=s1, # Soil moisture parameter
106
+ s33=s3, # Soil moisture parameter
107
+ s44=s4, # Soil moisture parameter
108
+ Tsd_C=Tsd_C # Soil dew point temperature (°C)
109
+ )
110
+
111
+ # combining hysteresis logic to differentiate surface vs. rootzone water control
112
+ SM = rt.where((D0 > VPD_hPa) & (PET > phi) & (dTS > 0), SMrz, SM)
113
+ SM = rt.where((phi > 0) & (dTS > 0) & (Td_C <= 0), SMrz, SM)
114
+ SM = rt.clip(SM, 0, 1)
115
+
116
+ # checking convergence
117
+ # re-estimating alpha
118
+ alphaN = ((gS * (e0star - Ea_hPa) * (2 * delta + 2 * gamma_hPa + gamma_hPa * gB_by_gS * (1 + SM))) / (
119
+ 2 * delta * (gamma_hPa * (T0 - Ta_C) * (gB + gS) + gS * (e0star - Ea_hPa))))
120
+
121
+ return SM, SMrz, Ms, s1, e0, e0star, Tsd_C, D0, alphaN
STIC_JPL/model.py ADDED
@@ -0,0 +1,420 @@
1
+ from typing import Union, Callable
2
+ import logging
3
+ from datetime import datetime, timedelta
4
+ from os.path import join, abspath, expanduser
5
+ from typing import Dict, List
6
+ import numpy as np
7
+ import warnings
8
+ from verma_net_radiation import daylight_Rn_integration_verma
9
+ from sun_angles import calculate_daylight
10
+
11
+ from pytictoc import TicToc
12
+
13
+ import colored_logging as cl
14
+ from check_distribution import check_distribution
15
+ import rasters as rt
16
+ from GEOS5FP import GEOS5FP
17
+ from solar_apparent_time import calculate_solar_day_of_year, calculate_solar_hour_of_day
18
+ from SEBAL_soil_heat_flux import calculate_SEBAL_soil_heat_flux
19
+
20
+ from rasters import Raster, RasterGeometry
21
+
22
+ from daylight_evapotranspiration import daylight_ET_from_instantaneous_LE
23
+
24
+ from .constants import *
25
+ from .exceptions import MissingOfflineParameter
26
+ from .closure import STIC_closure
27
+ from .soil_moisture_initialization import initialize_soil_moisture
28
+ from .soil_moisture_iteration import iterate_soil_moisture
29
+ from .net_radiation import calculate_net_longwave_radiation
30
+ from .initialize_with_solar import initialize_with_solar
31
+ from .canopy_air_stream import calculate_canopy_air_stream_vapor_pressure
32
+ from .initialize_without_solar import initialize_without_solar
33
+ from .iterate_with_solar import iterate_with_solar
34
+ from .iterate_without_solar import iterate_without_solar
35
+ from .root_zone_initialization import calculate_root_zone_moisture
36
+ from .FVC_from_NDVI import FVC_from_NDVI
37
+ from .LAI_from_NDVI import LAI_from_NDVI
38
+ from .celcius_to_kelvin import celcius_to_kelvin
39
+
40
+ __author__ = 'Kaniska Mallick, Madeleine Pascolini-Campbell, Gregory Halverson'
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ def STIC_JPL(
45
+ ST_C: Union[Raster, np.ndarray],
46
+ emissivity: Union[Raster, np.ndarray],
47
+ NDVI: Union[Raster, np.ndarray],
48
+ albedo: Union[Raster, np.ndarray],
49
+ Rn_Wm2: Union[Raster, np.ndarray],
50
+ geometry: RasterGeometry = None,
51
+ time_UTC: datetime = None,
52
+ hour_of_day: np.ndarray = None,
53
+ day_of_year: np.ndarray = None,
54
+ GEOS5FP_connection: GEOS5FP = None,
55
+ Ta_C: Union[Raster, np.ndarray] = None,
56
+ RH: Union[Raster, np.ndarray] = None,
57
+ G_Wm2: Union[Raster, np.ndarray] = None,
58
+ G_method: str = DEFAULT_G_METHOD,
59
+ SM: Union[Raster, np.ndarray] = None,
60
+ SWin_Wm2: Union[Raster, np.ndarray] = None,
61
+ FVC: Union[Raster, np.ndarray] = None,
62
+ LAI: Union[Raster, np.ndarray] = None,
63
+ elevation_m: Union[Raster, np.ndarray] = None,
64
+ delta_hPa: Union[Raster, np.ndarray] = None,
65
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA,
66
+ rho_kgm3: Union[Raster, np.ndarray] = RHO_KGM3,
67
+ Cp_Jkg: Union[Raster, np.ndarray] = CP_JKG,
68
+ alpha: float = PT_ALPHA,
69
+ LE_convergence_target: float = LE_CONVERGENCE_TARGET_WM2,
70
+ max_iterations: int = MAX_ITERATIONS,
71
+ show_distributions: bool = SHOW_DISTRIBUTIONS,
72
+ use_variable_alpha: bool = USE_VARIABLE_ALPHA,
73
+ upscale_to_daylight: bool = UPSCALE_TO_DAYLIGHT,
74
+ constrain_negative_LE: bool = CONSTRAIN_NEGATIVE_LE,
75
+ resampling: str = RESAMPLING,
76
+ offline_mode: bool = False) -> Dict[str, Union[Raster, np.ndarray]]:
77
+ results = {}
78
+ # For daily upscaling
79
+ Rn_daily_Wm2 = None
80
+ EF = None
81
+ LE_daylight_Wm2 = None
82
+ ET_daily_kg = None
83
+
84
+ if geometry is None and isinstance(ST_C, Raster):
85
+ geometry = ST_C.geometry
86
+
87
+ if GEOS5FP_connection is None:
88
+ GEOS5FP_connection = GEOS5FP()
89
+
90
+ if (day_of_year is None or hour_of_day is None) and time_UTC is not None and geometry is not None:
91
+ day_of_year = calculate_solar_day_of_year(time_UTC=time_UTC, geometry=geometry)
92
+ hour_of_day = calculate_solar_hour_of_day(time_UTC=time_UTC, geometry=geometry)
93
+
94
+ if time_UTC is None and day_of_year is None and hour_of_day is None:
95
+ raise ValueError("no time given between time_UTC, day_of_year, and hour_of_day")
96
+
97
+ seconds_of_day = hour_of_day * 3600.0
98
+
99
+ # Check for missing variables in offline mode before any GEOS-5 FP retrievals
100
+ if offline_mode:
101
+ missing_vars = []
102
+
103
+ if Ta_C is None:
104
+ missing_vars.append("Ta_C")
105
+ if RH is None:
106
+ missing_vars.append("RH")
107
+
108
+ if missing_vars:
109
+ raise MissingOfflineParameter(
110
+ f"missing STIC-JPL inputs in offline mode: {', '.join(missing_vars)}"
111
+ )
112
+
113
+ # load air temperature in Celsius if not provided
114
+ if Ta_C is None:
115
+ Ta_C = GEOS5FP_connection.Ta_C(time_UTC=time_UTC, geometry=geometry, resampling=resampling)
116
+
117
+ # load relative humidity if not provided
118
+ if RH is None:
119
+ RH = GEOS5FP_connection.RH(time_UTC=time_UTC, geometry=geometry, resampling=resampling)
120
+
121
+ # calculate fraction of vegetation cover if it's not given
122
+ if FVC is None:
123
+ FVC = FVC_from_NDVI(NDVI)
124
+
125
+ # calculate leaf area index if it's not given
126
+ if LAI is None:
127
+ LAI = LAI_from_NDVI(NDVI)
128
+
129
+ # saturation air pressure in hPa
130
+ SVP_hPa = 6.13753 * (np.exp((17.27 * Ta_C) / (Ta_C + 237.3)))
131
+
132
+ # calculate delta term if it's not given
133
+ if delta_hPa is None:
134
+ # slope of saturation vapor pressure to air temperature (hpa/K)
135
+ delta_hPa = 4098 * SVP_hPa / (Ta_C + 237.3) ** 2
136
+
137
+ Ta_K = celcius_to_kelvin(Ta_C)
138
+
139
+ # actual vapor pressure at TA (hpa/K)
140
+ Ea_hPa = SVP_hPa * (RH)
141
+ Ea_Pa = Ea_hPa * 100.0
142
+
143
+ # vapor pressure deficit (hPa)
144
+ VPD_hPa = SVP_hPa - Ea_hPa
145
+
146
+ # swapping in the dew-point calculation from PT-JPL
147
+ Td_C = Ta_C - ((100 - RH * 100) / 5.0)
148
+
149
+ # difference between surface and air temperature (Celsius)
150
+ dTS_C = ST_C - Ta_C
151
+
152
+ # saturation vapor pressure at surface temperature (hPa/K)
153
+ Estar_hPa = 6.13753 * np.exp((17.27 * ST_C) / (ST_C + 237.3))
154
+
155
+ if SWin_Wm2 is None:
156
+ # if G is None and SM is None:
157
+ # raise ValueError("soil heat flux or soil moisture prior required if solar radiation is not given")
158
+
159
+ if G_Wm2 is None:
160
+ G_Wm2 = calculate_SEBAL_soil_heat_flux(
161
+ ST_C=ST_C,
162
+ NDVI=NDVI,
163
+ albedo=albedo,
164
+ Rn=Rn_Wm2,
165
+ )
166
+
167
+ phi_Wm2 = Rn_Wm2 - G_Wm2
168
+
169
+ # initialize without solar radiation
170
+ SM, SMrz, s1, s3, s33, s44, Ms, Tsd_C, Es_hPa, Ds = initialize_without_solar(
171
+ ST_C = ST_C, # Surface temperature in Celsius
172
+ Ta_C = Ta_C, # Air temperature in Celsius
173
+ dTS = dTS_C, # Temperature difference between surface and air in Celsius
174
+ Td_C = Td_C, # Dewpoint temperature in Celsius
175
+ Ea_hPa = Ea_hPa, # Actual vapor pressure in hPa
176
+ Estar_hPa = Estar_hPa, # Saturation vapor pressure at surface temperature (hPa/K)
177
+ SVP_hPa = SVP_hPa, # Saturation vapor pressure at the surface in hPa
178
+ delta_hPa = delta_hPa, # Slope of the saturation vapor pressure-temperature curve in hPa/K
179
+ phi_Wm2 = phi_Wm2, # Available energy in W/m2
180
+ gamma_hPa = gamma_hPa, # Psychrometric constant in hPa/°C
181
+ alpha = alpha # Priestley-Taylor alpha
182
+ )
183
+ else:
184
+ SM, SMrz, Ms, s1, s3, Ep_PT, Rnsoil, LWnet_Wm2, G_Wm2, Tsd_C, Ds, Es_hPa, phi_Wm2 = initialize_with_solar(
185
+ seconds_of_day = seconds_of_day, # time of day in seconds since midnight
186
+ Rg_Wm2 = SWin_Wm2, # solar radiation (W/m^2)
187
+ Rn_Wm2 = Rn_Wm2, # net radiation (W/m^2)
188
+ ST_C = ST_C, # surface temperature (Celsius)
189
+ emissivity = emissivity, # emissivity of the surface
190
+ Ta_C = Ta_C, # air temperature (Celsius)
191
+ dTS_C = dTS_C, # surface air temperature difference (Celsius)
192
+ Td_C = Td_C, # dew point temperature (Celsius)
193
+ VPD_hPa = VPD_hPa, # vapor pressure deficit (hPa)
194
+ SVP_hPa = SVP_hPa, # saturation vapor pressure at given air temperature (hPa)
195
+ Ea_hPa = Ea_hPa, # actual vapor pressure at air temperature (hPa)
196
+ Estar_hPa = Estar_hPa, # saturation vapor pressure at surface temperature (hPa)
197
+ delta_hPa = delta_hPa, # slope of saturation vapor pressure to air temperature (hpa/K)
198
+ NDVI=NDVI, # normalized difference vegetation index
199
+ FVC = FVC, # fractional vegetation cover
200
+ LAI = LAI, # leaf area index
201
+ albedo = albedo, # albedo of the surface
202
+ gamma_hPa=gamma_hPa, # psychrometric constant (hPa/°C)
203
+ G_method = DEFAULT_G_METHOD, # method for calculating soil heat flux
204
+ )
205
+
206
+ check_distribution(Ms, "Ms")
207
+
208
+ # STIC analytical equations (convergence on LE)
209
+ gB_ms, gS_ms, dT_C, EF = STIC_closure(
210
+ delta_hPa=delta_hPa,
211
+ phi_Wm2=phi_Wm2,
212
+ Es_hPa=Es_hPa,
213
+ Ea_hPa=Ea_hPa,
214
+ Estar_hPa=Estar_hPa,
215
+ SM=SM,
216
+ gamma_hPa=gamma_hPa,
217
+ rho_kgm3=rho_kgm3,
218
+ Cp_Jkg=Cp_Jkg,
219
+ alpha=alpha
220
+ )
221
+
222
+ gBB = gB_ms
223
+ gSS = gS_ms
224
+ gBB_by_gSS = rt.where(gSS == 0, 0, gBB / gSS)
225
+ gB_by_gS = rt.where(gS_ms == 0, 0, gB_ms / gS_ms)
226
+ dT_C = dT_C
227
+ T0_C = dT_C + Ta_C
228
+
229
+ PET_Wm2 = ((delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa)) # Penman potential evaporation
230
+
231
+ gR = (4 * SB_SIGMA * (Ta_C + 273) ** 3 * emissivity) / (rho_kgm3 * Cp_Jkg)
232
+ omega = ((delta_hPa / gamma_hPa) + 1) / ((delta_hPa / gamma_hPa) + 1 + gB_by_gS)
233
+ LE_eq = (phi_Wm2 * (delta_hPa / gamma_hPa)) / ((delta_hPa / gamma_hPa) + 1)
234
+ LE_imp = (Cp_Jkg * 0.0289644 / gamma_hPa) * gS_ms * 40 * VPD_hPa
235
+ LE_init = omega * LE_eq + (1 - omega) * LE_imp
236
+ dry = (Ds > VPD_hPa) & (PET_Wm2 > phi_Wm2) & (dTS_C > 0) & (Td_C <= 0)
237
+ omega = rt.where(dry,
238
+ ((delta_hPa / gamma_hPa) + 1 + gR / gB_ms) / ((delta_hPa / gamma_hPa) + 1 + gB_ms / gS_ms + gR / gS_ms + gR / gB_ms),
239
+ omega)
240
+ LE_eq = rt.where(dry, (phi_Wm2 * (delta_hPa / gamma_hPa)) / ((delta_hPa / gamma_hPa) + 1 + gR / gB_ms), LE_eq)
241
+ LE_init = rt.where(dry, omega * LE_eq + (1 - omega * LE_imp), LE_init)
242
+
243
+ # sensible heat flux
244
+ H_Wm2 = ((gamma_hPa * phi_Wm2 * (1 + gB_by_gS) - rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa * (1 + (gB_by_gS))))
245
+
246
+ LE_Wm2_new = LE_init
247
+ LE_Wm2_change = LE_convergence_target
248
+ LE_Wm2_old = LE_Wm2_new
249
+ LE_canopy_Wm2 = None
250
+ PT_Wm2 = None
251
+ iteration = 1
252
+ LE_Wm2_max_change = 0
253
+
254
+ t = TicToc()
255
+ t.tic()
256
+
257
+ while (np.nanmax(LE_Wm2_change) >= LE_convergence_target and iteration <= max_iterations):
258
+ logger.info(f"running STIC iteration {cl.val(iteration)} / {cl.val(max_iterations)}")
259
+
260
+ if SWin_Wm2 is None:
261
+ SM, SMrz, Ms, s1, e0, e0star, Tsd_C, D0, alphaN = iterate_without_solar(
262
+ LE = LE_Wm2_new, # Latent heat flux (W/m^2)
263
+ PET = PET_Wm2, # Potential evapotranspiration (W/m^2)
264
+ SM = SM,
265
+ ST_C = ST_C, # Surface temperature (°C)
266
+ Ta_C = Ta_C, # Air temperature (°C)
267
+ dTS = dTS_C, # Surface-air temperature difference (°C)
268
+ T0 = T0_C, # Reference temperature (°C)
269
+ gB = gB_ms, # Boundary layer conductance (m/s)
270
+ gS = gS_ms, # Stomatal conductance (m/s)
271
+ Ea_hPa = Ea_hPa, # Actual vapor pressure (hPa)
272
+ Td_C = Td_C, # Dew point temperature (°C)
273
+ VPD_hPa = VPD_hPa, # Vapor pressure deficit (hPa)
274
+ Estar = Estar_hPa, # Saturation vapor pressure at surface temperature (hPa)
275
+ delta = delta_hPa, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
276
+ phi = phi_Wm2, # available energy (W/m^2)
277
+ Ds = Ds, # Vapor pressure deficit at source (hPa)
278
+ Es = Es_hPa, # Saturation vapor pressure (hPa)
279
+ s3 = s3, # Slope of the saturation vapor pressure and temperature
280
+ s4 = s44, # Slope of the saturation vapor pressure and temperature
281
+ gB_by_gS = gB_by_gS, # Ratio of boundary layer conductance to stomatal conductance
282
+ gamma_hPa = gamma_hPa, # Psychrometric constant (hPa/°C)
283
+ rho_kgm3 = rho_kgm3, # Air density (kg/m^3)
284
+ Cp_Jkg = Cp_Jkg # Specific heat at constant pressure (J/kg/K)
285
+ )
286
+ else:
287
+ SM, G_Wm2, e0, e0star, D0, alphaN = iterate_with_solar(
288
+ seconds_of_day = seconds_of_day, # Seconds of the day
289
+ ST_C = ST_C, # Soil temperature (°C)
290
+ NDVI = NDVI, # Normalized Difference Vegetation Index
291
+ albedo = albedo, # Albedo
292
+ gB_ms = gB_ms, # boundary layer conductance (m/s)
293
+ gS_ms = gS_ms, # stomatal conductance (m/s)
294
+ LE_Wm2 = LE_Wm2_new, # latent heat flux (W/m^2)
295
+ Rg_Wm2 = SWin_Wm2, # Incoming solar radiation (W/m^2)
296
+ Rn_Wm2 = Rn_Wm2, # Net radiation (W/m^2)
297
+ LWnet_Wm2 = LWnet_Wm2, # Net longwave radiation (W/m^2)
298
+ Ta_C = Ta_C, # Air temperature (°C)
299
+ dTS_C = dTS_C, # Change in soil temperature (°C)
300
+ Td_C = Td_C, # Dew point temperature (°C)
301
+ Tsd_C = Tsd_C, # Soil dew point temperature (°C)
302
+ Ea_hPa = Ea_hPa, # actual vapor pressure (hPa)
303
+ Estar_hPa = Estar_hPa, # saturation vapor pressure at surface temperature (hPa)
304
+ VPD_hPa = VPD_hPa, # Vapor pressure deficit (hPa)
305
+ SVP_hPa = SVP_hPa, # Saturation vapor pressure (hPa)
306
+ delta_hPa = delta_hPa, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
307
+ phi_Wm2 = phi_Wm2, # Net radiation minus soil heat flux (W/m^2)
308
+ Es_hPa = Es_hPa, # Saturation vapor pressure (hPa)
309
+ s1 = s1, # Soil moisture parameter
310
+ s3 = s3, # Soil moisture parameter
311
+ FVC = FVC, # Fractional canopy cover
312
+ T0_C = T0_C, # Reference temperature (°C)
313
+ gB_by_gS = gB_by_gS, # Ratio of boundary layer conductance to stomatal conductance
314
+ gamma_hPa = gamma_hPa, # Psychrometric constant (hPa/°C)
315
+ rho_kgm3 = rho_kgm3, # Air density (kg/m^3)
316
+ Cp_Jkg = Cp_Jkg, # Specific heat at constant pressure (J/kg/K)
317
+ G_method = "santanello" # Method for calculating soil heat flux
318
+ )
319
+
320
+ if use_variable_alpha:
321
+ alpha = alphaN
322
+ logger.info(f"using variable Priestley-Taylor alpha with mean: {cl.val(np.round(np.nanmean(alpha), 3))}")
323
+
324
+ # re-estimated conductances and states
325
+ gB_ms, gS_ms, dT_C, EF = STIC_closure(
326
+ delta_hPa=delta_hPa, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
327
+ phi_Wm2=phi_Wm2, # available energy (W/m^2)
328
+ Es_hPa=Es_hPa, # Vapor pressure at the reference height (hPa)
329
+ Ea_hPa=Ea_hPa, # Actual vapor pressure (hPa)
330
+ Estar_hPa=Estar_hPa, # Saturation vapor pressure at the reference height (hPa)
331
+ SM=SM, # Soil moisture (m³/m³)
332
+ gamma_hPa=gamma_hPa, # Psychrometric constant (hPa/°C)
333
+ rho_kgm3=rho_kgm3, # Air density (kg/m³)
334
+ Cp_Jkg=Cp_Jkg, # Specific heat capacity of air (J/kg/°C)
335
+ alpha=alpha # Stability correction factor for conductance
336
+ )
337
+
338
+ gB_by_gS = rt.where(gS_ms == 0, 0, gB_ms / gS_ms)
339
+ T0_C = dT_C + Ta_C
340
+ # latent heat flux
341
+ LE_Wm2_new = ((delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa * (1 + gB_by_gS)))
342
+ LE_Wm2_new = rt.where(LE_Wm2_new > phi_Wm2, phi_Wm2, LE_Wm2_new)
343
+
344
+ if constrain_negative_LE:
345
+ LE_Wm2_new = rt.where(LE_Wm2_new < 0, 0, LE_Wm2_new)
346
+
347
+ # Sensible Heat Flux
348
+ H_Wm2 = ((gamma_hPa * phi_Wm2 * (1 + gB_by_gS) - rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa * (1 + (gB_by_gS))))
349
+ # potential evaporation (Penman)
350
+ PET_Wm2 = ((delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa))
351
+ # Potential Transpiration
352
+ PT_Wm2 = (delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa * (1 + SM * gB_by_gS)) # potential transpiration
353
+ # ET PARTITIONING
354
+ LE_soil_Wm2 = rt.clip(SM * PET_Wm2, 0, None)
355
+ LE_canopy_Wm2 = rt.clip(LE_Wm2_new - LE_soil_Wm2, 0, None)
356
+ # change in latent heat flux estimate
357
+ LE_Wm2_change = np.abs(LE_Wm2_old - LE_Wm2_new)
358
+ LE_Wm2_new = rt.where(np.isnan(LE_Wm2_new), LE_Wm2_old, LE_Wm2_new)
359
+ LE_Wm2_old = LE_Wm2_new
360
+ LE_Wm2_max_change = np.nanmax(LE_Wm2_change)
361
+ logger.info(
362
+ 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)")
363
+
364
+ check_distribution(SM, f"SM_{iteration}")
365
+ check_distribution(G_Wm2, f"G_{iteration}")
366
+ check_distribution(LE_Wm2_new, f"LE_{iteration}")
367
+
368
+ if LE_Wm2_max_change <= LE_convergence_target:
369
+ 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 ''}")
370
+
371
+ iteration += 1
372
+
373
+ iteration -= 1
374
+ results["LE_max_change"] = LE_Wm2_max_change
375
+ results["iteration"] = iteration
376
+
377
+ LE_Wm2 = LE_Wm2_new
378
+
379
+ results["LE_Wm2"] = LE_Wm2
380
+ results["LE_change"] = LE_Wm2_change
381
+ results["LE_soil_Wm2"] = LE_soil_Wm2
382
+ results["LE_canopy_Wm2"] = LE_canopy_Wm2
383
+ results["PT"] = PT_Wm2
384
+ results["PET_Wm2"] = PET_Wm2
385
+ results["G_Wm2"] = G_Wm2
386
+
387
+ if isinstance(geometry, RasterGeometry):
388
+ for name, array in results.items():
389
+ try:
390
+ results[name] = Raster(array.reshape(geometry.shape), geometry=geometry)
391
+ except Exception as e:
392
+ pass
393
+
394
+ results["LE_Wm2"].cmap = ET_COLORMAP
395
+ results["PET_Wm2"].cmap = ET_COLORMAP
396
+
397
+ warnings.resetwarnings()
398
+
399
+ # --- Daily Upscaling (if requested) ---
400
+ if upscale_to_daylight and time_UTC is not None:
401
+ logger.info("started daily ET upscaling (STIC-JPL)")
402
+ t_et = TicToc()
403
+ t_et.tic()
404
+
405
+ # Use new upscaling function from daylight_evapotranspiration
406
+ daylight_results = daylight_ET_from_instantaneous_LE(
407
+ LE_instantaneous_Wm2=LE_Wm2,
408
+ Rn_instantaneous_Wm2=Rn_Wm2,
409
+ G_instantaneous_Wm2=G_Wm2,
410
+ day_of_year=day_of_year,
411
+ time_UTC=time_UTC,
412
+ geometry=geometry
413
+ )
414
+ # Add all returned daylight results to output
415
+ results.update(daylight_results)
416
+
417
+ elapsed_et = t_et.tocvalue()
418
+ logger.info(f"completed daily ET upscaling (elapsed: {elapsed_et:.2f} seconds)")
419
+
420
+ return results