STIC-JPL 1.1.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.

Potentially problematic release.


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

STIC_JPL/STIC_JPL.py ADDED
@@ -0,0 +1,370 @@
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 .diagnostic import diagnostic
9
+ import colored_logging as cl
10
+ from .meteorology_conversion import calculate_air_density, calculate_specific_heat, calculate_specific_humidity, calculate_surface_pressure, celcius_to_kelvin
11
+ import rasters as rt
12
+ from GEOS5FP import GEOS5FP
13
+ from solar_apparent_time import solar_day_of_year_for_area, solar_hour_of_day_for_area
14
+
15
+ from .timer import Timer
16
+
17
+ from rasters import Raster, RasterGeometry
18
+
19
+ from .vegetation_conversion.vegetation_conversion import FVC_from_NDVI, LAI_from_NDVI
20
+
21
+ from .constants import *
22
+ from .closure import STIC_closure
23
+ from .soil_moisture_initialization import initialize_soil_moisture
24
+ from .soil_moisture_iteration import iterate_soil_moisture
25
+ from .net_radiation import calculate_net_longwave_radiation
26
+ from .initialize_with_solar import initialize_with_solar
27
+ from .canopy_air_stream import calculate_canopy_air_stream_vapor_pressure
28
+ from .initialize_without_solar import initialize_without_solar
29
+ from .iterate_with_solar import iterate_with_solar
30
+ from .iterate_without_solar import iterate_without_solar
31
+ from .root_zone_initialization import calculate_root_zone_moisture
32
+
33
+ from .soil_heat_flux import calculate_SEBAL_soil_heat_flux
34
+
35
+ __author__ = 'Kaniska Mallick, Madeleine Pascolini-Campbell, Gregory Halverson'
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+ def STIC_JPL(
40
+ ST_C: Union[Raster, np.ndarray],
41
+ emissivity: Union[Raster, np.ndarray],
42
+ NDVI: Union[Raster, np.ndarray],
43
+ albedo: Union[Raster, np.ndarray],
44
+ Rn_Wm2: Union[Raster, np.ndarray],
45
+ geometry: RasterGeometry = None,
46
+ time_UTC: datetime = None,
47
+ hour_of_day: np.ndarray = None,
48
+ day_of_year: np.ndarray = None,
49
+ GEOS5FP_connection: GEOS5FP = None,
50
+ Ta_C: Union[Raster, np.ndarray] = None,
51
+ RH: Union[Raster, np.ndarray] = None,
52
+ G: Union[Raster, np.ndarray] = None,
53
+ G_method: str = DEFAULT_G_METHOD,
54
+ SM: Union[Raster, np.ndarray] = None,
55
+ Rg_Wm2: Union[Raster, np.ndarray] = None,
56
+ FVC: Union[Raster, np.ndarray] = None,
57
+ LAI: Union[Raster, np.ndarray] = None,
58
+ elevation_m: Union[Raster, np.ndarray] = None,
59
+ delta_hPa: Union[Raster, np.ndarray] = None,
60
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA,
61
+ rho_kgm3: Union[Raster, np.ndarray] = RHO_KGM3,
62
+ Cp_Jkg: Union[Raster, np.ndarray] = CP_JKG,
63
+ alpha: float = PT_ALPHA,
64
+ LE_convergence_target: float = LE_CONVERGENCE_TARGET_WM2,
65
+ max_iterations: int = MAX_ITERATIONS,
66
+ diagnostic_directory: str = None,
67
+ show_distributions: bool = SHOW_DISTRIBUTIONS,
68
+ use_variable_alpha: bool = USE_VARIABLE_ALPHA) -> Dict[str, Union[Raster, np.ndarray]]:
69
+ results = {}
70
+
71
+ if geometry is None and isinstance(ST_C, Raster):
72
+ geometry = ST_C.geometry
73
+
74
+ if GEOS5FP_connection is None:
75
+ GEOS5FP_connection = GEOS5FP()
76
+
77
+ if (day_of_year is None or hour_of_day is None) and time_UTC is not None and geometry is not None:
78
+ day_of_year = solar_day_of_year_for_area(time_UTC=time_UTC, geometry=geometry)
79
+ hour_of_day = solar_hour_of_day_for_area(time_UTC=time_UTC, geometry=geometry)
80
+
81
+ if time_UTC is None and day_of_year is None and hour_of_day is None:
82
+ raise ValueError("no time given between time_UTC, day_of_year, and hour_of_day")
83
+
84
+ diag_kwargs = {
85
+ "show_distributions": show_distributions,
86
+ "output_directory": diagnostic_directory
87
+ }
88
+
89
+ seconds_of_day = hour_of_day * 3600.0
90
+
91
+ # load air temperature in Celsius if not provided
92
+ if Ta_C is None:
93
+ Ta_C = GEOS5FP_connection.Ta_C(time_UTC=time_UTC, geometry=geometry, resampling=resampling)
94
+
95
+ # load relative humidity if not provided
96
+ if RH is None:
97
+ RH = GEOS5FP_connection.RH(time_UTC=time_UTC, geometry=geometry, resampling=resampling)
98
+
99
+ # calculate fraction of vegetation cover if it's not given
100
+ if FVC is None:
101
+ FVC = FVC_from_NDVI(NDVI)
102
+
103
+ # calculate leaf area index if it's not given
104
+ if LAI is None:
105
+ LAI = LAI_from_NDVI(NDVI)
106
+
107
+ # saturation air pressure in hPa
108
+ SVP_hPa = 6.13753 * (np.exp((17.27 * Ta_C) / (Ta_C + 237.3)))
109
+
110
+ # calculate delta term if it's not given
111
+ if delta_hPa is None:
112
+ # slope of saturation vapor pressure to air temperature (hpa/K)
113
+ delta_hPa = 4098 * SVP_hPa / (Ta_C + 237.3) ** 2
114
+
115
+ Ta_K = celcius_to_kelvin(Ta_C)
116
+
117
+ # actual vapor pressure at TA (hpa/K)
118
+ Ea_hPa = SVP_hPa * (RH)
119
+ Ea_Pa = Ea_hPa * 100.0
120
+
121
+ # vapor pressure deficit (hPa)
122
+ VPD_hPa = SVP_hPa - Ea_hPa
123
+
124
+ # swapping in the dew-point calculation from PT-JPL
125
+ Td_C = Ta_C - ((100 - RH * 100) / 5.0)
126
+
127
+ # difference between surface and air temperature (Celsius)
128
+ dTS_C = ST_C - Ta_C
129
+
130
+ # saturation vapor pressure at surface temperature (hPa/K)
131
+ Estar_hPa = 6.13753 * np.exp((17.27 * ST_C) / (ST_C + 237.3))
132
+
133
+ if Rg_Wm2 is None:
134
+ # if G is None and SM is None:
135
+ # raise ValueError("soil heat flux or soil moisture prior required if solar radiation is not given")
136
+
137
+ if G is None:
138
+ G = calculate_SEBAL_soil_heat_flux(
139
+ ST_C=ST_C,
140
+ NDVI=NDVI,
141
+ albedo=albedo,
142
+ Rn=Rn_Wm2,
143
+ )
144
+
145
+ phi_Wm2 = Rn_Wm2 - G
146
+
147
+ # initialize without solar radiation
148
+ SM, SMrz, s1, s3, s33, s44, Ms, Tsd_C, Es_hPa, Ds = initialize_without_solar(
149
+ ST_C = ST_C, # Surface temperature in Celsius
150
+ Ta_C = Ta_C, # Air temperature in Celsius
151
+ dTS = dTS_C, # Temperature difference between surface and air in Celsius
152
+ Td_C = Td_C, # Dewpoint temperature in Celsius
153
+ Ea_hPa = Ea_hPa, # Actual vapor pressure in hPa
154
+ Estar_hPa = Estar_hPa, # Saturation vapor pressure at surface temperature (hPa/K)
155
+ SVP_hPa = SVP_hPa, # Saturation vapor pressure at the surface in hPa
156
+ delta_hPa = delta_hPa, # Slope of the saturation vapor pressure-temperature curve in hPa/K
157
+ phi_Wm2 = phi_Wm2, # Available energy in W/m2
158
+ gamma_hPa = gamma_hPa, # Psychrometric constant in hPa/°C
159
+ alpha = alpha # Priestley-Taylor alpha
160
+ )
161
+ else:
162
+ SM, SMrz, Ms, s1, s3, Ep_PT, Rnsoil, LWnet_Wm2, G, Tsd_C, Ds, Es_hPa, phi_Wm2 = initialize_with_solar(
163
+ seconds_of_day = seconds_of_day, # time of day in seconds since midnight
164
+ Rg_Wm2 = Rg_Wm2, # solar radiation (W/m^2)
165
+ Rn_Wm2 = Rn_Wm2, # net radiation (W/m^2)
166
+ ST_C = ST_C, # surface temperature (Celsius)
167
+ emissivity = emissivity, # emissivity of the surface
168
+ Ta_C = Ta_C, # air temperature (Celsius)
169
+ dTS_C = dTS_C, # surface air temperature difference (Celsius)
170
+ Td_C = Td_C, # dew point temperature (Celsius)
171
+ VPD_hPa = VPD_hPa, # vapor pressure deficit (hPa)
172
+ SVP_hPa = SVP_hPa, # saturation vapor pressure at given air temperature (hPa)
173
+ Ea_hPa = Ea_hPa, # actual vapor pressure at air temperature (hPa)
174
+ Estar_hPa = Estar_hPa, # saturation vapor pressure at surface temperature (hPa)
175
+ delta_hPa = delta_hPa, # slope of saturation vapor pressure to air temperature (hpa/K)
176
+ NDVI=NDVI, # normalized difference vegetation index
177
+ FVC = FVC, # fractional vegetation cover
178
+ LAI = LAI, # leaf area index
179
+ albedo = albedo, # albedo of the surface
180
+ gamma_hPa=gamma_hPa, # psychrometric constant (hPa/°C)
181
+ G_method = DEFAULT_G_METHOD, # method for calculating soil heat flux
182
+ )
183
+
184
+ diagnostic(Ms, "Ms", **diag_kwargs)
185
+
186
+ # STIC analytical equations (convergence on LE)
187
+ gB_ms, gS_ms, dT_C, EF = STIC_closure(
188
+ delta_hPa=delta_hPa,
189
+ phi_Wm2=phi_Wm2,
190
+ Es_hPa=Es_hPa,
191
+ Ea_hPa=Ea_hPa,
192
+ Estar_hPa=Estar_hPa,
193
+ SM=SM,
194
+ gamma_hPa=gamma_hPa,
195
+ rho_kgm3=rho_kgm3,
196
+ Cp_Jkg=Cp_Jkg,
197
+ alpha=alpha
198
+ )
199
+
200
+ gBB = gB_ms
201
+ gSS = gS_ms
202
+ gBB_by_gSS = rt.where(gSS == 0, 0, gBB / gSS)
203
+ gB_by_gS = rt.where(gS_ms == 0, 0, gB_ms / gS_ms)
204
+ dT_C = dT_C
205
+ T0_C = dT_C + Ta_C
206
+
207
+ PET_Wm2 = ((delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa)) # Penman potential evaporation
208
+
209
+ gR = (4 * SB_SIGMA * (Ta_C + 273) ** 3 * emissivity) / (rho_kgm3 * Cp_Jkg)
210
+ omega = ((delta_hPa / gamma_hPa) + 1) / ((delta_hPa / gamma_hPa) + 1 + gB_by_gS)
211
+ LE_eq = (phi_Wm2 * (delta_hPa / gamma_hPa)) / ((delta_hPa / gamma_hPa) + 1)
212
+ LE_imp = (Cp_Jkg * 0.0289644 / gamma_hPa) * gS_ms * 40 * VPD_hPa
213
+ LE_init = omega * LE_eq + (1 - omega) * LE_imp
214
+ dry = (Ds > VPD_hPa) & (PET_Wm2 > phi_Wm2) & (dTS_C > 0) & (Td_C <= 0)
215
+ omega = rt.where(dry,
216
+ ((delta_hPa / gamma_hPa) + 1 + gR / gB_ms) / ((delta_hPa / gamma_hPa) + 1 + gB_ms / gS_ms + gR / gS_ms + gR / gB_ms),
217
+ omega)
218
+ LE_eq = rt.where(dry, (phi_Wm2 * (delta_hPa / gamma_hPa)) / ((delta_hPa / gamma_hPa) + 1 + gR / gB_ms), LE_eq)
219
+ LE_init = rt.where(dry, omega * LE_eq + (1 - omega * LE_imp), LE_init)
220
+
221
+ # sensible heat flux
222
+ 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))))
223
+
224
+ LE_Wm2_new = LE_init
225
+ LE_Wm2_change = LE_convergence_target
226
+ LE_Wm2_old = LE_Wm2_new
227
+ LE_transpiration_Wm2 = None
228
+ PT_Wm2 = None
229
+ iteration = 1
230
+ LE_Wm2_max_change = 0
231
+ t = Timer()
232
+
233
+ while (np.nanmax(LE_Wm2_change) >= LE_convergence_target and iteration <= max_iterations):
234
+ logger.info(f"running STIC iteration {cl.val(iteration)} / {cl.val(max_iterations)}")
235
+
236
+ if Rg_Wm2 is None:
237
+ SM, SMrz, Ms, s1, e0, e0star, Tsd_C, D0, alphaN = iterate_without_solar(
238
+ LE = LE_Wm2_new, # Latent heat flux (W/m^2)
239
+ PET = PET_Wm2, # Potential evapotranspiration (W/m^2)
240
+ SM = SM,
241
+ ST_C = ST_C, # Surface temperature (°C)
242
+ Ta_C = Ta_C, # Air temperature (°C)
243
+ dTS = dTS_C, # Surface-air temperature difference (°C)
244
+ T0 = T0_C, # Reference temperature (°C)
245
+ gB = gB_ms, # Boundary layer conductance (m/s)
246
+ gS = gS_ms, # Stomatal conductance (m/s)
247
+ Ea_hPa = Ea_hPa, # Actual vapor pressure (hPa)
248
+ Td_C = Td_C, # Dew point temperature (°C)
249
+ VPD_hPa = VPD_hPa, # Vapor pressure deficit (hPa)
250
+ Estar = Estar_hPa, # Saturation vapor pressure at surface temperature (hPa)
251
+ delta = delta_hPa, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
252
+ phi = phi_Wm2, # available energy (W/m^2)
253
+ Ds = Ds, # Vapor pressure deficit at source (hPa)
254
+ Es = Es_hPa, # Saturation vapor pressure (hPa)
255
+ s3 = s3, # Slope of the saturation vapor pressure and temperature
256
+ s4 = s44, # Slope of the saturation vapor pressure and temperature
257
+ gB_by_gS = gB_by_gS, # Ratio of boundary layer conductance to stomatal conductance
258
+ gamma_hPa = gamma_hPa, # Psychrometric constant (hPa/°C)
259
+ rho_kgm3 = rho_kgm3, # Air density (kg/m^3)
260
+ Cp_Jkg = Cp_Jkg # Specific heat at constant pressure (J/kg/K)
261
+ )
262
+ else:
263
+ SM, G, e0, e0star, D0, alphaN = iterate_with_solar(
264
+ seconds_of_day = seconds_of_day, # Seconds of the day
265
+ ST_C = ST_C, # Soil temperature (°C)
266
+ NDVI = NDVI, # Normalized Difference Vegetation Index
267
+ albedo = albedo, # Albedo
268
+ gB_ms = gB_ms, # boundary layer conductance (m/s)
269
+ gS_ms = gS_ms, # stomatal conductance (m/s)
270
+ LE_Wm2 = LE_Wm2_new, # latent heat flux (W/m^2)
271
+ Rg_Wm2 = Rg_Wm2, # Incoming solar radiation (W/m^2)
272
+ Rn_Wm2 = Rn_Wm2, # Net radiation (W/m^2)
273
+ LWnet_Wm2 = LWnet_Wm2, # Net longwave radiation (W/m^2)
274
+ Ta_C = Ta_C, # Air temperature (°C)
275
+ dTS_C = dTS_C, # Change in soil temperature (°C)
276
+ Td_C = Td_C, # Dew point temperature (°C)
277
+ Tsd_C = Tsd_C, # Soil dew point temperature (°C)
278
+ Ea_hPa = Ea_hPa, # actual vapor pressure (hPa)
279
+ Estar_hPa = Estar_hPa, # saturation vapor pressure at surface temperature (hPa)
280
+ VPD_hPa = VPD_hPa, # Vapor pressure deficit (hPa)
281
+ SVP_hPa = SVP_hPa, # Saturation vapor pressure (hPa)
282
+ delta_hPa = delta_hPa, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
283
+ phi_Wm2 = phi_Wm2, # Net radiation minus soil heat flux (W/m^2)
284
+ Es_hPa = Es_hPa, # Saturation vapor pressure (hPa)
285
+ s1 = s1, # Soil moisture parameter
286
+ s3 = s3, # Soil moisture parameter
287
+ FVC = FVC, # Fractional canopy cover
288
+ T0_C = T0_C, # Reference temperature (°C)
289
+ gB_by_gS = gB_by_gS, # Ratio of boundary layer conductance to stomatal conductance
290
+ gamma_hPa = gamma_hPa, # Psychrometric constant (hPa/°C)
291
+ rho_kgm3 = rho_kgm3, # Air density (kg/m^3)
292
+ Cp_Jkg = Cp_Jkg, # Specific heat at constant pressure (J/kg/K)
293
+ G_method = "santanello" # Method for calculating soil heat flux
294
+ )
295
+
296
+ if use_variable_alpha:
297
+ alpha = alphaN
298
+ logger.info(f"using variable Priestley-Taylor alpha with mean: {cl.val(np.round(np.nanmean(alpha), 3))}")
299
+
300
+ # re-estimated conductances and states
301
+ gB_ms, gS_ms, dT_C, EF = STIC_closure(
302
+ delta_hPa=delta_hPa, # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
303
+ phi_Wm2=phi_Wm2, # available energy (W/m^2)
304
+ Es_hPa=Es_hPa, # Vapor pressure at the reference height (hPa)
305
+ Ea_hPa=Ea_hPa, # Actual vapor pressure (hPa)
306
+ Estar_hPa=Estar_hPa, # Saturation vapor pressure at the reference height (hPa)
307
+ SM=SM, # Soil moisture (m³/m³)
308
+ gamma_hPa=gamma_hPa, # Psychrometric constant (hPa/°C)
309
+ rho_kgm3=rho_kgm3, # Air density (kg/m³)
310
+ Cp_Jkg=Cp_Jkg, # Specific heat capacity of air (J/kg/°C)
311
+ alpha=alpha # Stability correction factor for conductance
312
+ )
313
+
314
+ gB_by_gS = rt.where(gS_ms == 0, 0, gB_ms / gS_ms)
315
+ T0_C = dT_C + Ta_C
316
+ # latent heat flux
317
+ LE_Wm2_new = ((delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa * (1 + gB_by_gS)))
318
+ LE_Wm2_new = rt.where(LE_Wm2_new > phi_Wm2, phi_Wm2, LE_Wm2_new)
319
+ # Sensible Heat Flux
320
+ 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))))
321
+ # potential evaporation (Penman)
322
+ PET_Wm2 = ((delta_hPa * phi_Wm2 + rho_kgm3 * Cp_Jkg * gB_ms * VPD_hPa) / (delta_hPa + gamma_hPa))
323
+ # Potential Transpiration
324
+ 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
325
+ # ET PARTITIONING
326
+ LE_soil_Wm2 = rt.clip(SM * PET_Wm2, 0, None)
327
+ LE_transpiration_Wm2 = rt.clip(LE_Wm2_new - LE_soil_Wm2, 0, None)
328
+ # change in latent heat flux estimate
329
+ LE_Wm2_change = np.abs(LE_Wm2_old - LE_Wm2_new)
330
+ LE_Wm2_new = rt.where(np.isnan(LE_Wm2_new), LE_Wm2_old, LE_Wm2_new)
331
+ LE_Wm2_old = LE_Wm2_new
332
+ LE_Wm2_max_change = np.nanmax(LE_Wm2_change)
333
+ logger.info(
334
+ 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)")
335
+
336
+ diagnostic(SM, f"SM_{iteration}", **diag_kwargs)
337
+ diagnostic(G, f"G_{iteration}", **diag_kwargs)
338
+ diagnostic(LE_Wm2_new, f"LE_{iteration}", **diag_kwargs)
339
+
340
+ if LE_Wm2_max_change <= LE_convergence_target:
341
+ 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 ''}")
342
+
343
+ iteration += 1
344
+
345
+ iteration -= 1
346
+ results["LE_max_change"] = LE_Wm2_max_change
347
+ results["iteration"] = iteration
348
+
349
+ LE = LE_Wm2_new
350
+
351
+ results["LE"] = LE
352
+ results["LE_change"] = LE_Wm2_change
353
+ results["LEt"] = LE_transpiration_Wm2
354
+ results["PT"] = PT_Wm2
355
+ results["PET"] = PET_Wm2
356
+ results["G"] = G
357
+
358
+ if isinstance(geometry, RasterGeometry):
359
+ for name, array in results.items():
360
+ try:
361
+ results[name] = Raster(array.reshape(geometry.shape), geometry=geometry)
362
+ except Exception as e:
363
+ pass
364
+
365
+ results["LE"].cmap = ET_COLORMAP
366
+ results["PET"].cmap = ET_COLORMAP
367
+
368
+ warnings.resetwarnings()
369
+
370
+ return results
STIC_JPL/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from .STIC_JPL import *
2
+
3
+ from os.path import join, abspath, dirname
4
+
5
+ with open(join(abspath(dirname(__file__)), "version.txt")) as f:
6
+ version = f.read()
7
+
8
+ __version__ = version
9
+ __author__ = "Gregory H. Halverson"
@@ -0,0 +1,37 @@
1
+ from typing import Union
2
+ import numpy as np
3
+
4
+ import rasters as rt
5
+
6
+ from rasters import Raster
7
+
8
+ from .constants import *
9
+
10
+ def calculate_canopy_air_stream_vapor_pressure(
11
+ LE: Union[Raster, np.ndarray], # Latent heat flux [W/m^2]
12
+ Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure [hPa]
13
+ Estar: Union[Raster, np.ndarray], # Saturation vapor pressure [hPa]
14
+ gB: Union[Raster, np.ndarray], # Conductance of boundary layer [mol/m^2/s]
15
+ gS: Union[Raster, np.ndarray], # Conductance of stomata [mol/m^2/s]
16
+ rho_kgm3: Union[Raster, np.ndarray, float] = RHO_KGM3, # Air density (kg/m^3)
17
+ Cp_Jkg: Union[Raster, np.ndarray, float] = CP_JKG, # Specific heat at constant pressure (J/kg/K)
18
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA, # Psychrometric constant (hPa/°C)
19
+ ) -> Union[Raster, np.ndarray]:
20
+ """
21
+ Calculate the canopy air stream vapor pressure.
22
+
23
+ Parameters:
24
+ LE (Union[Raster, np.ndarray]): Latent heat flux [W/m^2]
25
+ Ea_hPa (Union[Raster, np.ndarray]): Actual vapor pressure [hPa]
26
+ Estar (Union[Raster, np.ndarray]): Saturation vapor pressure [hPa]
27
+ gB (Union[Raster, np.ndarray]): Conductance of boundary layer [mol/m^2/s]
28
+ gS (Union[Raster, np.ndarray]): Conductance of stomata [mol/m^2/s]
29
+
30
+ Returns:
31
+ Union[Raster, np.ndarray]: The calculated canopy air stream vapor pressure [hPa]
32
+ """
33
+ e0star = Ea_hPa + (gamma_hPa * LE * (gB + gS)) / (rho_kgm3 * Cp_Jkg * gB * gS)
34
+ e0star = rt.where(e0star < 0, Estar, e0star)
35
+ e0star = rt.where(e0star > 250, Estar, e0star)
36
+
37
+ return e0star
STIC_JPL/closure.py ADDED
@@ -0,0 +1,60 @@
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 .constants import *
9
+
10
+ def STIC_closure(
11
+ delta_hPa: Union[Raster, np.ndarray], # Slope of the saturation vapor pressure-temperature curve (hPa/°C)
12
+ phi_Wm2: Union[Raster, np.ndarray], # available energy (W/m^2)
13
+ Es_hPa: Union[Raster, np.ndarray], # Vapor pressure at the reference height (hPa)
14
+ Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure (hPa)
15
+ Estar_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure at the reference height (hPa)
16
+ SM: Union[Raster, np.ndarray], # Soil moisture (m³/m³)
17
+ gamma_hPa: float = GAMMA_HPA, # Psychrometric constant (hPa/°C)
18
+ rho_kgm3: float = RHO_KGM3, # Air density (kg/m³)
19
+ Cp_Jkg: float = CP_JKG, # Specific heat capacity of air (J/kg/°C)
20
+ alpha: float = PT_ALPHA # Priestley-Taylor alpha
21
+ ) -> Tuple[Union[Raster, np.ndarray]]:
22
+ """
23
+ STIC closure equations with modified Priestley Taylor and Penman Monteith
24
+ (Mallick et al., 2015, Water Resources research)
25
+
26
+ Parameters:
27
+ delta (np.ndarray): Slope of the saturation vapor pressure-temperature curve (hPa/°C)
28
+ phi (np.ndarray): available energy (W/m^2)
29
+ Es (np.ndarray): Vapor pressure at the reference height (hPa)
30
+ Ea (np.ndarray): Actual vapor pressure (hPa)
31
+ Estar (np.ndarray): Saturation vapor pressure at the reference height (hPa)
32
+ SM (np.ndarray): Soil moisture (m³/m³)
33
+ rho (float, optional): Air density (kg/m³). Defaults to RHO.
34
+ cp (float, optional): Specific heat capacity of air (J/kg/°C). Defaults to CP.
35
+ gamma (float, optional): Psychrometric constant (hPa/°C). Defaults to GAMMA.
36
+ alpha (float, optional): Stability correction factor for conductance. Defaults to ALPHA.
37
+
38
+ Returns:
39
+ gB (np.ndarray): boundary layer conductance (m s^-1)
40
+ gS (np.ndarray): stomatal conductance (m s^-1)
41
+ dT (np.ndarray): difference between surface and air temperature
42
+ EF (np.ndarray): evaporative fraction
43
+ """
44
+ # boundary layer conductance (m s^-1)
45
+ gB = ((2 * phi_Wm2 * alpha * delta_hPa * gamma_hPa) / (2 * Cp_Jkg * delta_hPa * Es_hPa * rho_kgm3 - 2 * Cp_Jkg * delta_hPa * Ea_hPa * rho_kgm3 - 2 * Cp_Jkg * Ea_hPa * gamma_hPa * rho_kgm3 + Cp_Jkg * Es_hPa * gamma_hPa * rho_kgm3 + Cp_Jkg * Estar_hPa * gamma_hPa * rho_kgm3 - Cp_Jkg * SM * Es_hPa * gamma_hPa * rho_kgm3 + Cp_Jkg * SM * Estar_hPa * gamma_hPa * rho_kgm3))
46
+ gB = rt.clip(gB, 0.0001, 0.2)
47
+
48
+ # stomatal conductance (m s^-1)
49
+ gS = (-(2 * (phi_Wm2 * alpha * delta_hPa * Ea_hPa * gamma_hPa - phi_Wm2 * alpha * delta_hPa * Es_hPa * gamma_hPa)) / (Cp_Jkg * Estar_hPa ** 2 * gamma_hPa * rho_kgm3 - Cp_Jkg * Es_hPa ** 2 * gamma_hPa * rho_kgm3 - 2 * Cp_Jkg * delta_hPa * Es_hPa ** 2 * rho_kgm3 + 2 * Cp_Jkg * delta_hPa * Ea_hPa * Es_hPa * rho_kgm3 - 2 * Cp_Jkg * delta_hPa * Ea_hPa * Estar_hPa * rho_kgm3 + 2 * Cp_Jkg * delta_hPa * Es_hPa * Estar_hPa * rho_kgm3 + 2 * Cp_Jkg * Ea_hPa * Es_hPa * gamma_hPa * rho_kgm3 - 2 * Cp_Jkg * Ea_hPa * Estar_hPa * gamma_hPa * rho_kgm3 + Cp_Jkg * SM * Es_hPa ** 2 * gamma_hPa * rho_kgm3 + Cp_Jkg * SM * Estar_hPa ** 2 * gamma_hPa * rho_kgm3 - 2 * Cp_Jkg * SM * Es_hPa * Estar_hPa * gamma_hPa * rho_kgm3))
50
+ gS = rt.clip(gS, 0.0001, 0.2)
51
+
52
+ # difference between surface and air temperature (Celsius)
53
+ dT = (2 * delta_hPa * Es_hPa - 2 * delta_hPa * Ea_hPa - 2 * Ea_hPa * gamma_hPa + Es_hPa * gamma_hPa + Estar_hPa * gamma_hPa - SM * Es_hPa * gamma_hPa + SM * Estar_hPa * gamma_hPa + 2 * alpha * delta_hPa * Ea_hPa - 2 * alpha * delta_hPa * Es_hPa) / (2 * alpha * delta_hPa * gamma_hPa)
54
+ dT = rt.clip(dT, -10, 50)
55
+
56
+ # evaporative fraction
57
+ EF = -(2 * alpha * delta_hPa * Ea_hPa - 2 * alpha * delta_hPa * Es_hPa) / (2 * delta_hPa * Es_hPa - 2 * delta_hPa * Ea_hPa - 2 * Ea_hPa * gamma_hPa + Es_hPa * gamma_hPa + Estar_hPa * gamma_hPa - SM * Es_hPa * gamma_hPa + SM * Estar_hPa * gamma_hPa)
58
+ EF = rt.clip(EF, 0, 1)
59
+
60
+ return gB, gS, dT, EF
STIC_JPL/constants.py ADDED
@@ -0,0 +1,21 @@
1
+ from matplotlib.colors import LinearSegmentedColormap
2
+
3
+ RHO_KGM3 = 1.2 # Air density (kg m-3)
4
+ CP_JKG = 1013 # Specific heat of air at constant pressure (J/kg/K)
5
+ GAMMA_HPA = 0.67 # Psychrometric constant (hpa/K)
6
+ PT_ALPHA = 1.26 # Priestley-Taylor coefficient
7
+ SB_SIGMA = 5.67e-8 # Stefann Boltzmann constant
8
+ DEFAULT_G_METHOD = "santanello"
9
+ LE_CONVERGENCE_TARGET_WM2 = 2.0
10
+ MAX_ITERATIONS = 30
11
+ USE_VARIABLE_ALPHA = True
12
+ SHOW_DISTRIBUTIONS = True
13
+
14
+ ET_COLORMAP = LinearSegmentedColormap.from_list("ET", [
15
+ "#f6e8c3",
16
+ "#d8b365",
17
+ "#99974a",
18
+ "#53792d",
19
+ "#6bdfd2",
20
+ "#1839c5"
21
+ ])
STIC_JPL/diagnostic.py ADDED
@@ -0,0 +1,70 @@
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")
@@ -0,0 +1,81 @@
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 .vegetation_conversion.vegetation_conversion import FVC_from_NDVI
9
+ from .soil_heat_flux import calculate_SEBAL_soil_heat_flux
10
+
11
+ from .constants import *
12
+ from .soil_moisture_initialization import initialize_soil_moisture
13
+ from .net_radiation import calculate_net_longwave_radiation
14
+
15
+
16
+ def initialize_with_solar(
17
+ seconds_of_day: Union[Raster, np.ndarray], # time of day in seconds since midnight
18
+ Rg_Wm2: Union[Raster, np.ndarray], # solar radiation (W/m^2)
19
+ Rn_Wm2: Union[Raster, np.ndarray], # net radiation (W/m^2)
20
+ ST_C: Union[Raster, np.ndarray], # surface temperature (Celsius)
21
+ emissivity: Union[Raster, np.ndarray], # emissivity of the surface
22
+ Ta_C: Union[Raster, np.ndarray], # air temperature (Celsius)
23
+ dTS_C: Union[Raster, np.ndarray], # surface air temperature difference (Celsius)
24
+ Td_C: Union[Raster, np.ndarray], # dew point temperature (Celsius)
25
+ VPD_hPa: Union[Raster, np.ndarray], # vapor pressure deficit (hPa)
26
+ SVP_hPa: Union[Raster, np.ndarray], # saturation vapor pressure at given air temperature (hPa)
27
+ Ea_hPa: Union[Raster, np.ndarray], # actual vapor pressure at air temperature (hPa)
28
+ Estar_hPa: Union[Raster, np.ndarray], # saturation vapor pressure at surface temperature (hPa)
29
+ delta_hPa: Union[Raster, np.ndarray], # slope of saturation vapor pressure to air temperature (hpa/K)
30
+ NDVI: Union[Raster, np.ndarray], # normalized difference vegetation index
31
+ FVC: Union[Raster, np.ndarray], # fractional vegetation cover
32
+ LAI: Union[Raster, np.ndarray], # leaf area index
33
+ albedo: Union[Raster, np.ndarray], # albedo of the surface
34
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA, # psychrometric constant (hPa/°C)
35
+ G_method: str = DEFAULT_G_METHOD # method for calculating soil heat flux
36
+ ) -> Tuple[Union[Raster, np.ndarray]]:
37
+ # Rn SOIL
38
+ kRN = 0.6
39
+ Rn_soil = Rn_Wm2 * np.exp(-kRN * LAI)
40
+
41
+ LWnet = calculate_net_longwave_radiation(
42
+ Ta_C=Ta_C,
43
+ Ea_hPa=Ea_hPa,
44
+ ST_C=ST_C,
45
+ emissivity=emissivity,
46
+ albedo=albedo
47
+ )
48
+
49
+ # initialize soil moisture
50
+ SM, SMrz, Ms, Ep_PT, Ds, s1, s3, Tsd_C = initialize_soil_moisture(
51
+ delta_hPa=delta_hPa, # slope of saturation vapor pressure to air temperature (hpa/K)
52
+ ST_C=ST_C, # surface temperature (Celsius)
53
+ Ta_C=Ta_C, # air temperature (Celsius)
54
+ Td_C=Td_C, # dew point temperature (Celsius)
55
+ dTS=dTS_C, # surface air temperature difference (Celsius)
56
+ Rn_Wm2=Rn_Wm2, # net radiation (W/m^2)
57
+ LWnet_Wm2=LWnet, # net longwave radiation (W/m^2)
58
+ FVC=FVC, # fractional vegetation cover
59
+ VPD_hPa=VPD_hPa, # vapor pressure deficit (hPa)
60
+ SVP_hPa=SVP_hPa, # saturation vapor pressure at given air temperature (hPa)
61
+ Ea_hPa=Ea_hPa, # actual vapor pressure at air temperature (hPa)
62
+ Estar_hPa=Estar_hPa, # saturation vapor pressure at surface temperature (hPa)
63
+ gamma_hPa=gamma_hPa # psychrometric constant (hPa/°C)
64
+ )
65
+
66
+ # calculate soil heat flux
67
+ G = calculate_SEBAL_soil_heat_flux(
68
+ ST_C=ST_C, # Surface temperature in Celsius
69
+ NDVI=NDVI, # Normalized Difference Vegetation Index
70
+ albedo=albedo, # Albedo of the surface
71
+ Rn=Rn_Wm2 # Net radiation (W/m^2)
72
+ )
73
+
74
+ # get phi with new comp
75
+ phi = Rn_Wm2 - G
76
+
77
+ Es = rt.where((Ep_PT > phi) & (dTS_C > 0) & (Td_C <= 0), Ea_hPa + SMrz * (Estar_hPa - Ea_hPa),
78
+ Ea_hPa + Ms * (Estar_hPa - Ea_hPa))
79
+
80
+ # Return all the created variables
81
+ return SM, SMrz, Ms, s1, s3, Ep_PT, Rn_soil, LWnet, G, Tsd_C, Ds, Es, phi