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.
- STIC_JPL/ECOv002-cal-val-STIC-JPL-inputs.csv +1066 -0
- STIC_JPL/ECOv002-cal-val-STIC-JPL-outputs.csv +1066 -0
- STIC_JPL/ECOv002_calval_STIC_inputs.py +19 -0
- STIC_JPL/FVC_from_NDVI.py +49 -0
- STIC_JPL/LAI_from_NDVI.py +61 -0
- STIC_JPL/STIC_JPL.py +5 -0
- STIC_JPL/__init__.py +4 -0
- STIC_JPL/canopy_air_stream.py +37 -0
- STIC_JPL/celcius_to_kelvin.py +11 -0
- STIC_JPL/closure.py +68 -0
- STIC_JPL/constants.py +31 -0
- STIC_JPL/exceptions.py +3 -0
- STIC_JPL/generate_STIC_inputs.py +65 -0
- STIC_JPL/initialize_with_solar.py +80 -0
- STIC_JPL/initialize_without_solar.py +85 -0
- STIC_JPL/iterate_with_solar.py +144 -0
- STIC_JPL/iterate_without_solar.py +121 -0
- STIC_JPL/model.py +420 -0
- STIC_JPL/net_radiation.py +38 -0
- STIC_JPL/process_STIC_table.py +152 -0
- STIC_JPL/root_zone_initialization.py +36 -0
- STIC_JPL/root_zone_iteration.py +66 -0
- STIC_JPL/soil_moisture_initialization.py +115 -0
- STIC_JPL/soil_moisture_iteration.py +131 -0
- STIC_JPL/verify.py +71 -0
- STIC_JPL/version.py +3 -0
- stic_jpl-1.6.0.dist-info/METADATA +103 -0
- stic_jpl-1.6.0.dist-info/RECORD +33 -0
- stic_jpl-1.6.0.dist-info/WHEEL +5 -0
- stic_jpl-1.6.0.dist-info/top_level.txt +2 -0
- tests/test_import_STIC.py +2 -0
- tests/test_import_dependencies.py +18 -0
- tests/test_verify.py +5 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
def load_ECOv002_calval_STIC_inputs() -> pd.DataFrame:
|
|
5
|
+
"""
|
|
6
|
+
Load the input data for the STIC model from the ECOSTRESS Collection 2 Cal-Val dataset.
|
|
7
|
+
|
|
8
|
+
Returns:
|
|
9
|
+
pd.DataFrame: A DataFrame containing the input data.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Define the path to the input CSV file relative to this module's directory
|
|
13
|
+
module_dir = os.path.dirname(os.path.abspath(__file__))
|
|
14
|
+
input_file_path = os.path.join(module_dir, "ECOv002-cal-val-STIC-JPL-inputs.csv")
|
|
15
|
+
|
|
16
|
+
# Load the input data into a DataFrame
|
|
17
|
+
inputs_df = pd.read_csv(input_file_path)
|
|
18
|
+
|
|
19
|
+
return inputs_df
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
import numpy as np
|
|
3
|
+
import rasters as rt
|
|
4
|
+
from rasters import Raster
|
|
5
|
+
|
|
6
|
+
KPAR = 0.5
|
|
7
|
+
MIN_FIPAR = 0.0
|
|
8
|
+
MAX_FIPAR = 1.0
|
|
9
|
+
MIN_LAI = 0.0
|
|
10
|
+
MAX_LAI = 10.0
|
|
11
|
+
|
|
12
|
+
def FVC_from_NDVI(NDVI: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
|
|
13
|
+
"""
|
|
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
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
NDVI (Union[Raster, np.ndarray]): Input NDVI data.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Union[Raster, np.ndarray]: Estimated Fractional Vegetation Cover (FVC).
|
|
42
|
+
"""
|
|
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]
|
|
47
|
+
FVC = rt.clip((NDVI - NDVIs) / (NDVIv - NDVIs), 0.0, 1.0)
|
|
48
|
+
|
|
49
|
+
return FVC
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
import numpy as np
|
|
3
|
+
import rasters as rt
|
|
4
|
+
from rasters import Raster
|
|
5
|
+
|
|
6
|
+
# Constants
|
|
7
|
+
KPAR = 0.5 # Extinction coefficient for PAR, assumed average for broadleaf canopies (Weiss & Baret, 2010)
|
|
8
|
+
MIN_FIPAR = 0.0
|
|
9
|
+
MAX_FIPAR = 1.0
|
|
10
|
+
MIN_LAI = 0.0
|
|
11
|
+
MAX_LAI = 10.0
|
|
12
|
+
|
|
13
|
+
def LAI_from_NDVI(
|
|
14
|
+
NDVI: Union[Raster, np.ndarray],
|
|
15
|
+
min_fIPAR: float = MIN_FIPAR,
|
|
16
|
+
max_fIPAR: float = MAX_FIPAR,
|
|
17
|
+
min_LAI: float = MIN_LAI,
|
|
18
|
+
max_LAI: float = MAX_LAI) -> Union[Raster, np.ndarray]:
|
|
19
|
+
"""
|
|
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.
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
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).
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
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
|
+
|
|
51
|
+
"""
|
|
52
|
+
# Empirical conversion from NDVI to fIPAR (adjusted for background noise)
|
|
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
|
|
56
|
+
fIPAR = np.where(fIPAR == 0, np.nan, fIPAR)
|
|
57
|
+
|
|
58
|
+
# Apply Beer–Lambert law to estimate LAI
|
|
59
|
+
LAI = rt.clip(-np.log(1 - fIPAR) * (1 / KPAR), min_LAI, max_LAI)
|
|
60
|
+
|
|
61
|
+
return LAI
|
STIC_JPL/STIC_JPL.py
ADDED
STIC_JPL/__init__.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
import numpy as np
|
|
3
|
+
from rasters import Raster
|
|
4
|
+
|
|
5
|
+
def celcius_to_kelvin(T_C: Union[Raster, np.ndarray]) -> Union[Raster, np.ndarray]:
|
|
6
|
+
"""
|
|
7
|
+
convert temperature in celsius to kelvin.
|
|
8
|
+
:param T_C: temperature in celsius
|
|
9
|
+
:return: temperature in kelvin
|
|
10
|
+
"""
|
|
11
|
+
return T_C + 273.15
|
STIC_JPL/closure.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
|
|
45
|
+
epsilon = 1e-8 # Small value to prevent division by zero
|
|
46
|
+
|
|
47
|
+
# boundary layer conductance (m s^-1)
|
|
48
|
+
gB_den = (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)
|
|
49
|
+
gB = ((2 * phi_Wm2 * alpha * delta_hPa * gamma_hPa) / (gB_den + epsilon))
|
|
50
|
+
gB = rt.clip(gB, 0.0001, 0.2)
|
|
51
|
+
|
|
52
|
+
# stomatal conductance (m s^-1)
|
|
53
|
+
gS_den = (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)
|
|
54
|
+
gS = (-(2 * (phi_Wm2 * alpha * delta_hPa * Ea_hPa * gamma_hPa - phi_Wm2 * alpha * delta_hPa * Es_hPa * gamma_hPa)) / (gS_den + epsilon))
|
|
55
|
+
gS = rt.clip(gS, 0.0001, 0.2)
|
|
56
|
+
|
|
57
|
+
# difference between surface and air temperature (Celsius)
|
|
58
|
+
dT_num = (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)
|
|
59
|
+
dT_den = (2 * alpha * delta_hPa * gamma_hPa)
|
|
60
|
+
dT = dT_num / (dT_den + epsilon)
|
|
61
|
+
dT = rt.clip(dT, -10, 50)
|
|
62
|
+
|
|
63
|
+
# evaporative fraction
|
|
64
|
+
EF_den = (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)
|
|
65
|
+
EF = -(2 * alpha * delta_hPa * Ea_hPa - 2 * alpha * delta_hPa * Es_hPa) / (EF_den + epsilon)
|
|
66
|
+
EF = rt.clip(EF, 0, 1)
|
|
67
|
+
|
|
68
|
+
return gB, gS, dT, EF
|
STIC_JPL/constants.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
2
|
+
|
|
3
|
+
RESAMPLING = "cubic"
|
|
4
|
+
CONSTRAIN_NEGATIVE_LE = False
|
|
5
|
+
SUPPLY_SWIN = False
|
|
6
|
+
UPSCALE_TO_DAYLIGHT = False
|
|
7
|
+
|
|
8
|
+
RHO_KGM3 = 1.2 # Air density (kg m-3)
|
|
9
|
+
CP_JKG = 1013 # Specific heat of air at constant pressure (J/kg/K)
|
|
10
|
+
GAMMA_HPA = 0.67 # Psychrometric constant (hpa/K)
|
|
11
|
+
PT_ALPHA = 1.26 # Priestley-Taylor coefficient
|
|
12
|
+
SB_SIGMA = 5.67e-8 # Stefann Boltzmann constant
|
|
13
|
+
DEFAULT_G_METHOD = "santanello"
|
|
14
|
+
LE_CONVERGENCE_TARGET_WM2 = 2.0
|
|
15
|
+
MAX_ITERATIONS = 30
|
|
16
|
+
USE_VARIABLE_ALPHA = True
|
|
17
|
+
SHOW_DISTRIBUTIONS = True
|
|
18
|
+
|
|
19
|
+
ET_COLORMAP = LinearSegmentedColormap.from_list("ET", [
|
|
20
|
+
"#f6e8c3",
|
|
21
|
+
"#d8b365",
|
|
22
|
+
"#99974a",
|
|
23
|
+
"#53792d",
|
|
24
|
+
"#6bdfd2",
|
|
25
|
+
"#1839c5"
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
GEOS5FP_INPUTS = [
|
|
29
|
+
"Ta_C",
|
|
30
|
+
"RH"
|
|
31
|
+
]
|
STIC_JPL/exceptions.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from dateutil import parser
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
from rasters import Point
|
|
7
|
+
from sentinel_tiles import sentinel_tiles
|
|
8
|
+
from solar_apparent_time import UTC_to_solar
|
|
9
|
+
from SEBAL_soil_heat_flux import calculate_SEBAL_soil_heat_flux
|
|
10
|
+
|
|
11
|
+
from .model import STIC_JPL, MAX_ITERATIONS, USE_VARIABLE_ALPHA
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
def generate_STIC_inputs(STIC_inputs_from_calval_df: DataFrame) -> DataFrame:
|
|
16
|
+
"""
|
|
17
|
+
STIC_inputs_from_claval_df:
|
|
18
|
+
Pandas DataFrame containing the columns: tower, lat, lon, time_UTC, albedo, elevation_km
|
|
19
|
+
return:
|
|
20
|
+
Pandas DataFrame containing the columns: tower, lat, lon, time_UTC, doy, albedo, elevation_km, AOT, COT, vapor_gccm, ozone_cm, SZA, KG
|
|
21
|
+
"""
|
|
22
|
+
# output_rows = []
|
|
23
|
+
STIC_inputs_df = STIC_inputs_from_calval_df.copy()
|
|
24
|
+
|
|
25
|
+
hour_of_day = []
|
|
26
|
+
doy = []
|
|
27
|
+
Topt = []
|
|
28
|
+
fAPARmax = []
|
|
29
|
+
|
|
30
|
+
for i, input_row in STIC_inputs_from_calval_df.iterrows():
|
|
31
|
+
tower = input_row.tower
|
|
32
|
+
lat = input_row.lat
|
|
33
|
+
lon = input_row.lon
|
|
34
|
+
time_UTC = input_row.time_UTC
|
|
35
|
+
albedo = input_row.albedo
|
|
36
|
+
elevation_km = input_row.elevation_km
|
|
37
|
+
logger.info(f"collecting STIC inputs for tower {tower} lat {lat} lon {lon} time {time_UTC} UTC")
|
|
38
|
+
time_UTC = parser.parse(str(time_UTC))
|
|
39
|
+
time_solar = UTC_to_solar(time_UTC, lon)
|
|
40
|
+
hour_of_day.append(time_solar.hour)
|
|
41
|
+
doy.append(time_UTC.timetuple().tm_yday)
|
|
42
|
+
date_UTC = time_UTC.date()
|
|
43
|
+
tile = sentinel_tiles.toMGRS(lat, lon)[:5]
|
|
44
|
+
tile_grid = sentinel_tiles.grid(tile=tile, cell_size=70)
|
|
45
|
+
rows, cols = tile_grid.shape
|
|
46
|
+
row, col = tile_grid.index_point(Point(lon, lat))
|
|
47
|
+
geometry = tile_grid[max(0, row - 1):min(row + 2, rows - 1),
|
|
48
|
+
max(0, col - 1):min(col + 2, cols - 1)]
|
|
49
|
+
|
|
50
|
+
if not "hour_of_day" in STIC_inputs_df.columns:
|
|
51
|
+
STIC_inputs_df["hour_of_day"] = hour_of_day
|
|
52
|
+
|
|
53
|
+
if not "doy" in STIC_inputs_df.columns:
|
|
54
|
+
STIC_inputs_df["doy"] = doy
|
|
55
|
+
|
|
56
|
+
if not "Topt" in STIC_inputs_df.columns:
|
|
57
|
+
STIC_inputs_df["Topt"] = Topt
|
|
58
|
+
|
|
59
|
+
if not "fAPARmax" in STIC_inputs_df.columns:
|
|
60
|
+
STIC_inputs_df["fAPARmax"] = fAPARmax
|
|
61
|
+
|
|
62
|
+
if "Ta" in STIC_inputs_df and "Ta_C" not in STIC_inputs_df:
|
|
63
|
+
STIC_inputs_df.rename({"Ta": "Ta_C"}, inplace=True)
|
|
64
|
+
|
|
65
|
+
return STIC_inputs_df
|
|
@@ -0,0 +1,80 @@
|
|
|
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 .soil_moisture_initialization import initialize_soil_moisture
|
|
12
|
+
from .net_radiation import calculate_net_longwave_radiation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def initialize_with_solar(
|
|
16
|
+
seconds_of_day: Union[Raster, np.ndarray], # time of day in seconds since midnight
|
|
17
|
+
Rg_Wm2: Union[Raster, np.ndarray], # solar radiation (W/m^2)
|
|
18
|
+
Rn_Wm2: Union[Raster, np.ndarray], # net radiation (W/m^2)
|
|
19
|
+
ST_C: Union[Raster, np.ndarray], # surface temperature (Celsius)
|
|
20
|
+
emissivity: Union[Raster, np.ndarray], # emissivity of the surface
|
|
21
|
+
Ta_C: Union[Raster, np.ndarray], # air temperature (Celsius)
|
|
22
|
+
dTS_C: Union[Raster, np.ndarray], # surface air temperature difference (Celsius)
|
|
23
|
+
Td_C: Union[Raster, np.ndarray], # dew point temperature (Celsius)
|
|
24
|
+
VPD_hPa: Union[Raster, np.ndarray], # vapor pressure deficit (hPa)
|
|
25
|
+
SVP_hPa: Union[Raster, np.ndarray], # saturation vapor pressure at given air temperature (hPa)
|
|
26
|
+
Ea_hPa: Union[Raster, np.ndarray], # actual vapor pressure at air temperature (hPa)
|
|
27
|
+
Estar_hPa: Union[Raster, np.ndarray], # saturation vapor pressure at surface temperature (hPa)
|
|
28
|
+
delta_hPa: Union[Raster, np.ndarray], # slope of saturation vapor pressure to air temperature (hpa/K)
|
|
29
|
+
NDVI: Union[Raster, np.ndarray], # normalized difference vegetation index
|
|
30
|
+
FVC: Union[Raster, np.ndarray], # fractional vegetation cover
|
|
31
|
+
LAI: Union[Raster, np.ndarray], # leaf area index
|
|
32
|
+
albedo: Union[Raster, np.ndarray], # albedo of the surface
|
|
33
|
+
gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA, # psychrometric constant (hPa/°C)
|
|
34
|
+
G_method: str = DEFAULT_G_METHOD # method for calculating soil heat flux
|
|
35
|
+
) -> Tuple[Union[Raster, np.ndarray]]:
|
|
36
|
+
# Rn SOIL
|
|
37
|
+
kRN = 0.6
|
|
38
|
+
Rn_soil = Rn_Wm2 * np.exp(-kRN * LAI)
|
|
39
|
+
|
|
40
|
+
LWnet = calculate_net_longwave_radiation(
|
|
41
|
+
Ta_C=Ta_C,
|
|
42
|
+
Ea_hPa=Ea_hPa,
|
|
43
|
+
ST_C=ST_C,
|
|
44
|
+
emissivity=emissivity,
|
|
45
|
+
albedo=albedo
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# initialize soil moisture
|
|
49
|
+
SM, SMrz, Ms, Ep_PT, Ds, s1, s3, Tsd_C = initialize_soil_moisture(
|
|
50
|
+
delta_hPa=delta_hPa, # slope of saturation vapor pressure to air temperature (hpa/K)
|
|
51
|
+
ST_C=ST_C, # surface temperature (Celsius)
|
|
52
|
+
Ta_C=Ta_C, # air temperature (Celsius)
|
|
53
|
+
Td_C=Td_C, # dew point temperature (Celsius)
|
|
54
|
+
dTS=dTS_C, # surface air temperature difference (Celsius)
|
|
55
|
+
Rn_Wm2=Rn_Wm2, # net radiation (W/m^2)
|
|
56
|
+
LWnet_Wm2=LWnet, # net longwave radiation (W/m^2)
|
|
57
|
+
FVC=FVC, # fractional vegetation cover
|
|
58
|
+
VPD_hPa=VPD_hPa, # vapor pressure deficit (hPa)
|
|
59
|
+
SVP_hPa=SVP_hPa, # saturation vapor pressure at given air temperature (hPa)
|
|
60
|
+
Ea_hPa=Ea_hPa, # actual vapor pressure at air temperature (hPa)
|
|
61
|
+
Estar_hPa=Estar_hPa, # saturation vapor pressure at surface temperature (hPa)
|
|
62
|
+
gamma_hPa=gamma_hPa # psychrometric constant (hPa/°C)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# calculate soil heat flux
|
|
66
|
+
G = calculate_SEBAL_soil_heat_flux(
|
|
67
|
+
ST_C=ST_C, # Surface temperature in Celsius
|
|
68
|
+
NDVI=NDVI, # Normalized Difference Vegetation Index
|
|
69
|
+
albedo=albedo, # Albedo of the surface
|
|
70
|
+
Rn=Rn_Wm2 # Net radiation (W/m^2)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# get phi with new comp
|
|
74
|
+
phi = Rn_Wm2 - G
|
|
75
|
+
|
|
76
|
+
Es = rt.where((Ep_PT > phi) & (dTS_C > 0) & (Td_C <= 0), Ea_hPa + SMrz * (Estar_hPa - Ea_hPa),
|
|
77
|
+
Ea_hPa + Ms * (Estar_hPa - Ea_hPa))
|
|
78
|
+
|
|
79
|
+
# Return all the created variables
|
|
80
|
+
return SM, SMrz, Ms, s1, s3, Ep_PT, Rn_soil, LWnet, G, Tsd_C, Ds, Es, phi
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
|
|
9
|
+
def initialize_without_solar(
|
|
10
|
+
ST_C: Union[Raster, np.ndarray], # Surface temperature in Celsius
|
|
11
|
+
Ta_C: Union[Raster, np.ndarray], # Air temperature in Celsius
|
|
12
|
+
dTS: Union[Raster, np.ndarray], # Temperature difference between surface and air in Celsius
|
|
13
|
+
Td_C: Union[Raster, np.ndarray], # Dewpoint temperature in Celsius
|
|
14
|
+
Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure in hPa
|
|
15
|
+
Estar_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure at surface temperature in hPa
|
|
16
|
+
SVP_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure at the surface in hPa
|
|
17
|
+
delta_hPa: Union[Raster, np.ndarray], # Slope of the saturation vapor pressure-temperature curve in hPa
|
|
18
|
+
phi_Wm2: Union[Raster, np.ndarray], # Available energy in W/m2
|
|
19
|
+
gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA, # Psychrometric constant in hPa/°C
|
|
20
|
+
alpha: Union[Raster, np.ndarray, float] = PT_ALPHA # Priestley-Taylor alpha
|
|
21
|
+
) -> Tuple[Union[Raster, np.ndarray]]:
|
|
22
|
+
"""
|
|
23
|
+
Initializes the variables related to moisture and vapor pressure without considering solar radiation.
|
|
24
|
+
|
|
25
|
+
Parameters:
|
|
26
|
+
ST_C (np.ndarray): Surface temperature in Celsius.
|
|
27
|
+
Ta_C (np.ndarray): Air temperature in Celsius.
|
|
28
|
+
dTS (np.ndarray): Temperature difference between surface and air in Celsius.
|
|
29
|
+
Td_C (np.ndarray): Dewpoint temperature in Celsius.
|
|
30
|
+
Ea_hPa (np.ndarray): Actual vapor pressure in hPa.
|
|
31
|
+
Estar (np.ndarray): saturation vapor pressure at surface temperature (hPa/K)
|
|
32
|
+
SVP_hPa (np.ndarray): Saturation vapor pressure at the surface in hPa.
|
|
33
|
+
delta (np.ndarray): Slope of the saturation vapor pressure-temperature curve in hPa/K.
|
|
34
|
+
phi (np.ndarray): available energy in W/m2.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
|
38
|
+
- SM (np.ndarray): Soil moisture.
|
|
39
|
+
- SMrz (np.ndarray): Rootzone moisture.
|
|
40
|
+
- s1 (np.ndarray): Slope of saturation vapor pressure and dewpoint temperature.
|
|
41
|
+
- s3 (np.ndarray): Slope of saturation vapor pressure and temperature.
|
|
42
|
+
- s33 (np.ndarray): Slope of saturation vapor pressure and surface temperature.
|
|
43
|
+
- s44 (np.ndarray): s44
|
|
44
|
+
- Ms (np.ndarray): Surface moisture.
|
|
45
|
+
- Tsd_C (np.ndarray): Surface dewpoint temperature in Celsius.
|
|
46
|
+
- Es (np.ndarray): Surface vapor pressure in hPa.
|
|
47
|
+
- Ds (np.ndarray): Vapor pressure deficit at the surface.
|
|
48
|
+
"""
|
|
49
|
+
s33 = (45.03 + 3.014 * ST_C + 0.05345 * ST_C ** 2 + 0.00224 * ST_C ** 3) * 1e-2 # hpa/K
|
|
50
|
+
s1 = (45.03 + 3.014 * Td_C + 0.05345 * Td_C ** 2 + 0.00224 * Td_C ** 3) * 1e-2 # hpa/K
|
|
51
|
+
|
|
52
|
+
# Surface dewpoint (Celsius)
|
|
53
|
+
Tsd_C = ((Estar_hPa - Ea_hPa) - (s33 * ST_C) + (s1 * Td_C)) / (s1 - s33)
|
|
54
|
+
|
|
55
|
+
# slope of saturation vapor pressure and temperature
|
|
56
|
+
s3 = rt.where((dTS > -20) & (dTS < 5), (Estar_hPa - Ea_hPa) / (ST_C - Td_C),
|
|
57
|
+
(45.03 + 3.014 * ST_C + 0.05345 * ST_C ** 2 + 0.00224 * ST_C ** 3) * 1e-2) # hpa/K
|
|
58
|
+
|
|
59
|
+
# Surface Moisture (Ms)
|
|
60
|
+
# Surface wetness
|
|
61
|
+
Ms = (s1 / s3) * ((Tsd_C - Td_C) / (ST_C - Td_C))
|
|
62
|
+
Ms = rt.clip(rt.where((dTS < 0) & (Ms < 0) & (phi_Wm2 < 0), np.abs(Ms), Ms), 0, 1)
|
|
63
|
+
|
|
64
|
+
# Rootzone Moisture (Mrz)
|
|
65
|
+
s44 = (SVP_hPa - Ea_hPa) / (Ta_C - Td_C)
|
|
66
|
+
SMrz = (gamma_hPa * s1 * (Tsd_C - Td_C)) / (
|
|
67
|
+
delta_hPa * s3 * (ST_C - Td_C) + gamma_hPa * s44 * (Ta_C - Td_C) - delta_hPa * s1 * (
|
|
68
|
+
Tsd_C - Td_C)) # rootzone wetness
|
|
69
|
+
SMrz = rt.clip(rt.where((dTS < 0) & (SMrz < 0) & (phi_Wm2 < 0), np.abs(SMrz), SMrz), 0, 1)
|
|
70
|
+
|
|
71
|
+
# now the limits of both Ms and Mrz are consistent
|
|
72
|
+
# combine M to account for Hysteresis and initial estimation of surface vapor pressure
|
|
73
|
+
# Potential evaporation (Priestley-Taylor eqn.)
|
|
74
|
+
Ep_PT = (alpha * delta_hPa * phi_Wm2) / (delta_hPa + gamma_hPa)
|
|
75
|
+
Es = rt.where((Ep_PT > phi_Wm2) & (dTS > 0) & (Td_C <= 0), Ea_hPa + SMrz * (Estar_hPa - Ea_hPa),
|
|
76
|
+
Ea_hPa + Ms * (Estar_hPa - Ea_hPa))
|
|
77
|
+
|
|
78
|
+
# soil moisture
|
|
79
|
+
SM = rt.where((Ep_PT > phi_Wm2) & (dTS > 0) & (Td_C <= 0), SMrz, Ms)
|
|
80
|
+
|
|
81
|
+
# hysteresis logic
|
|
82
|
+
# vapor pressure deficit at surface (Ds is later replaced by D0)
|
|
83
|
+
Ds = (Estar_hPa - Es)
|
|
84
|
+
|
|
85
|
+
return SM, SMrz, s1, s3, s33, s44, Ms, Tsd_C, Es, Ds
|