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,38 @@
1
+ from typing import Union
2
+ import numpy as np
3
+
4
+ from rasters import Raster
5
+
6
+ from .constants import SB_SIGMA
7
+
8
+ def calculate_net_longwave_radiation(
9
+ Ta_C: Union[Raster, np.ndarray],
10
+ Ea_hPa: Union[Raster, np.ndarray],
11
+ ST_C: Union[Raster, np.ndarray],
12
+ emissivity: Union[Raster, np.ndarray],
13
+ albedo: Union[Raster, np.ndarray],
14
+ sigma: float = SB_SIGMA) -> Union[Raster, np.ndarray]:
15
+ """
16
+ Calculate the net radiation.
17
+
18
+ Parameters:
19
+ Ta_C (np.ndarray): Air temperature in Celsius
20
+ Ea_hPa (np.ndarray): Actual vapor pressure at air temperature in hPa
21
+ ST_C (np.ndarray): Surface temperature in Celsius
22
+ emissivity (np.ndarray): Emissivity of the surface
23
+ RG (np.ndarray): Global radiation
24
+ albedo (np.ndarray): Albedo of the surface
25
+
26
+ Returns:
27
+ Lnet (np.ndarray): Net longwave radiation
28
+ """
29
+ etaa = 1.24 * (Ea_hPa / (Ta_C + 273.15)) ** (1.0 / 7.0) # air emissivity
30
+ LWin = sigma * etaa * (Ta_C + 273.15) ** 4
31
+ LWout = sigma * emissivity * (ST_C + 273.15) ** 4
32
+
33
+ # emissivity was being applied twice here
34
+ # LWnet = emissivity * LWin - LWout
35
+
36
+ LWnet = LWin - LWout
37
+
38
+ return LWnet
@@ -0,0 +1,152 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from dateutil import parser
6
+ from pandas import DataFrame
7
+ from rasters import Point, MultiPoint, WGS84
8
+ from sentinel_tiles import sentinel_tiles
9
+ from solar_apparent_time import UTC_to_solar
10
+ from SEBAL_soil_heat_flux import calculate_SEBAL_soil_heat_flux
11
+ from shapely.geometry import Point
12
+
13
+ from .constants import *
14
+ from .model import STIC_JPL, MAX_ITERATIONS, USE_VARIABLE_ALPHA
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ def process_STIC_table(
19
+ input_df: DataFrame,
20
+ max_iterations = MAX_ITERATIONS,
21
+ use_variable_alpha = USE_VARIABLE_ALPHA,
22
+ constrain_negative_LE = CONSTRAIN_NEGATIVE_LE,
23
+ supply_SWin = SUPPLY_SWIN,
24
+ upscale_to_daylight = UPSCALE_TO_DAYLIGHT,
25
+ offline_mode: bool = False
26
+ ) -> DataFrame:
27
+ """
28
+ Process STIC table with batch processing.
29
+
30
+ Note: daylight_evapotranspiration now supports arrays of datetime objects,
31
+ so batch processing works efficiently even with upscale_to_daylight=True.
32
+ """
33
+ return process_STIC_table_single(
34
+ input_df,
35
+ max_iterations=max_iterations,
36
+ use_variable_alpha=use_variable_alpha,
37
+ constrain_negative_LE=constrain_negative_LE,
38
+ supply_SWin=supply_SWin,
39
+ upscale_to_daylight=upscale_to_daylight,
40
+ offline_mode=offline_mode
41
+ )
42
+
43
+
44
+ def process_STIC_table_single(
45
+ input_df: DataFrame,
46
+ max_iterations = MAX_ITERATIONS,
47
+ use_variable_alpha = USE_VARIABLE_ALPHA,
48
+ constrain_negative_LE = CONSTRAIN_NEGATIVE_LE,
49
+ supply_SWin = SUPPLY_SWIN,
50
+ upscale_to_daylight = UPSCALE_TO_DAYLIGHT,
51
+ offline_mode: bool = False
52
+ ) -> DataFrame:
53
+ """Process a single row or batch of data through STIC-JPL model."""
54
+
55
+ ST_C = np.float64(np.array(input_df.ST_C))
56
+ emissivity = np.float64(np.array(input_df.EmisWB))
57
+ NDVI = np.float64(np.array(input_df.NDVI))
58
+ albedo = np.float64(np.array(input_df.albedo))
59
+ Ta_C = np.float64(np.array(input_df.Ta_C))
60
+ RH = np.float64(np.array(input_df.RH))
61
+ Rn_Wm2 = np.float64(np.array(input_df.Rn_Wm2))
62
+ Rg = np.float64(np.array(input_df.Rg))
63
+
64
+ if "G" in input_df:
65
+ G_Wm2 = np.array(input_df.G)
66
+ else:
67
+ G_Wm2 = calculate_SEBAL_soil_heat_flux(
68
+ Rn=Rn_Wm2,
69
+ ST_C=ST_C,
70
+ NDVI=NDVI,
71
+ albedo=albedo
72
+ )
73
+
74
+ if "SWin_Wm2" in input_df and supply_SWin:
75
+ SWin_Wm2 = np.float64(np.array(input_df.SWin_Wm2))
76
+ else:
77
+ SWin_Wm2 = None
78
+
79
+ # --- Handle geometry and time columns ---
80
+ def ensure_geometry(df):
81
+ if "geometry" in df:
82
+ if isinstance(df.geometry.iloc[0], str):
83
+ def parse_geom(s):
84
+ s = s.strip()
85
+ if s.startswith("POINT"):
86
+ coords = s.replace("POINT", "").replace("(", "").replace(")", "").strip().split()
87
+ return Point(float(coords[0]), float(coords[1]))
88
+ elif "," in s:
89
+ coords = [float(c) for c in s.split(",")]
90
+ return Point(coords[0], coords[1])
91
+ else:
92
+ coords = [float(c) for c in s.split()]
93
+ return Point(coords[0], coords[1])
94
+ df = df.copy()
95
+ df['geometry'] = df['geometry'].apply(parse_geom)
96
+ return df
97
+
98
+ input_df = ensure_geometry(input_df)
99
+
100
+ logger.info("started extracting geometry from PT-JPL-SM input table")
101
+
102
+ if "geometry" in input_df:
103
+ # Convert Point objects to coordinate tuples for MultiPoint
104
+ if hasattr(input_df.geometry.iloc[0], "x") and hasattr(input_df.geometry.iloc[0], "y"):
105
+ coords = [(pt.x, pt.y) for pt in input_df.geometry]
106
+ geometry = MultiPoint(coords, crs=WGS84)
107
+ else:
108
+ geometry = MultiPoint(input_df.geometry, crs=WGS84)
109
+ elif "lat" in input_df and "lon" in input_df:
110
+ lat = np.array(input_df.lat).astype(np.float64)
111
+ lon = np.array(input_df.lon).astype(np.float64)
112
+ geometry = MultiPoint(x=lon, y=lat, crs=WGS84)
113
+ else:
114
+ raise KeyError("Input DataFrame must contain either 'geometry' or both 'lat' and 'lon' columns.")
115
+
116
+ logger.info("completed extracting geometry from PT-JPL-SM input table")
117
+
118
+ logger.info("started extracting time from PT-JPL-SM input table")
119
+
120
+ # Handle time conversion - for single row, extract single datetime; for multiple rows, use list
121
+ if len(input_df) == 1:
122
+ time_UTC = pd.to_datetime(input_df.time_UTC.iloc[0])
123
+ else:
124
+ time_UTC = pd.to_datetime(input_df.time_UTC).tolist()
125
+
126
+ logger.info("completed extracting time from PT-JPL-SM input table")
127
+
128
+ results = STIC_JPL(
129
+ geometry=geometry,
130
+ ST_C = ST_C,
131
+ emissivity=emissivity,
132
+ NDVI=NDVI,
133
+ albedo=albedo,
134
+ Ta_C=Ta_C,
135
+ RH=RH,
136
+ Rn_Wm2=Rn_Wm2,
137
+ G_Wm2=G_Wm2,
138
+ SWin_Wm2=SWin_Wm2,
139
+ time_UTC=time_UTC,
140
+ max_iterations=max_iterations,
141
+ use_variable_alpha=use_variable_alpha,
142
+ constrain_negative_LE=constrain_negative_LE,
143
+ upscale_to_daylight=upscale_to_daylight,
144
+ offline_mode=offline_mode
145
+ )
146
+
147
+ output_df = input_df.copy()
148
+
149
+ for key, value in results.items():
150
+ output_df[key] = value
151
+
152
+ return output_df
@@ -0,0 +1,36 @@
1
+ from typing import Union
2
+ import numpy as np
3
+
4
+ import rasters as rt
5
+ from rasters import Raster
6
+
7
+ from .constants import GAMMA_HPA
8
+
9
+ def calculate_root_zone_moisture(
10
+ delta_hPa: Union[Raster, np.ndarray],
11
+ ST_C: Union[Raster, np.ndarray],
12
+ Ta_C: Union[Raster, np.ndarray],
13
+ Td_C: Union[Raster, np.ndarray],
14
+ s11: Union[Raster, np.ndarray],
15
+ s33: Union[Raster, np.ndarray],
16
+ s44: Union[Raster, np.ndarray],
17
+ Tsd_C: Union[Raster, np.ndarray],
18
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA) -> Union[Raster, np.ndarray]:
19
+ """
20
+ This function calculates the rootzone moisture (Mrz) using various parameters.
21
+
22
+ Parameters:
23
+ delta (np.ndarray): Rate of change of saturation vapor pressure with temperature (hPa/°C)
24
+ ST_C (np.ndarray): Surface temperature (°C)
25
+ Ta_C (np.ndarray): Air temperature (°C)
26
+ Td_C (np.ndarray): Dewpoint temperature (°C)
27
+ s11 (np.ndarray): The slope of SVP at dewpoint temperature (hPa/K)
28
+ s33 (np.ndarray): The slope of SVP at surface temperature (hPa/K)
29
+ s44 (np.ndarray): The difference between saturation vapor pressure and actual vapor pressure divided by the difference between air temperature and dewpoint temperature (hPa/K)
30
+ Tsd_C (np.ndarray): The surface dewpoint temperature (°C)
31
+
32
+ Returns:
33
+ SMrz (np.ndarray): The rootzone moisture (m³/m³)
34
+ """
35
+ return rt.clip((gamma_hPa * s11 * (Tsd_C - Td_C)) / (delta_hPa * s33 * (ST_C - Td_C) + gamma_hPa * s44 * (Ta_C - Td_C) - delta_hPa * s11 * (Tsd_C - Td_C)), 0.0001, 0.999)
36
+
@@ -0,0 +1,66 @@
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 GAMMA_HPA
9
+
10
+ def calculate_rootzone_moisture(
11
+ delta_hPa: Union[Raster, np.ndarray], # Rate of change of saturation vapor pressure with temperature (hPa/°C)
12
+ s1_hPa: Union[Raster, np.ndarray], # The slope of SVP at surface temperature (hPa/K)
13
+ s3_hPa: Union[Raster, np.ndarray], # The slope of SVP at dewpoint temperature (hPa/K)
14
+ ST_C: Union[Raster, np.ndarray], # surface temperature (°C)
15
+ Ta_C: Union[Raster, np.ndarray], # air temperature (°C)
16
+ dTS_C: Union[Raster, np.ndarray], # difference between surface and air temperature (°C)
17
+ Td_C: Union[Raster, np.ndarray], # dewpoint temperature (°C)
18
+ Tsd_C: Union[Raster, np.ndarray], # surface dewpoint temperature (°C)
19
+ Rg_Wm2: Union[Raster, np.ndarray], # Incoming solar radiation (W/m^2)
20
+ Rn_Wm2: Union[Raster, np.ndarray], # Net radiation (W/m^2)
21
+ LWnet_Wm2: Union[Raster, np.ndarray], # Net longwave radiation (W/m^2)
22
+ FVC: Union[Raster, np.ndarray], # Fractional vegetation cover (unitless)
23
+ VPD_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit (hPa)
24
+ D0_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit at source (hPa)
25
+ SVP_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure (hPa)
26
+ Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure (hPa)
27
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA # Psychrometric constant (hPa/°C)
28
+ ) -> Union[Raster, np.ndarray]:
29
+ """
30
+ Calculates the root zone moisture (Mrz) based on thermal IR and meteorological information.
31
+
32
+ Args:
33
+ delta (np.ndarray): Rate of change of saturation vapor pressure with temperature (kPa/°C).
34
+ s1 (np.ndarray): The slope of saturation vapor pressure at surface temperature (hPa/K).
35
+ s3 (np.ndarray): The slope of saturation vapor pressure at dewpoint temperature (hPa/K).
36
+ ST_C (np.ndarray): Surface temperature in degrees Celsius.
37
+ Ta_C (np.ndarray): Air temperature in degrees Celsius.
38
+ dTS (np.ndarray): Difference between surface and air temperature in degrees Celsius.
39
+ Td_C (np.ndarray): Dewpoint temperature in degrees Celsius.
40
+ Tsd_C (np.ndarray): Surface dewpoint temperature in degrees Celsius.
41
+ Rg (np.ndarray): Incoming solar radiation in W/m^2.
42
+ Rn (np.ndarray): Net radiation in W/m^2.
43
+ Lnet (np.ndarray): Net longwave radiation in W/m^2.
44
+ fc (np.ndarray): Fractional vegetation cover (unitless).
45
+ VPD_hPa (np.ndarray): Vapor pressure deficit in hPa.
46
+ D0 (np.ndarray): Vapor pressure deficit at source in hPa.
47
+ SVP_hPa (np.ndarray): Saturation vapor pressure in hPa.
48
+ Ea_hPa (np.ndarray): Actual vapor pressure in hPa.
49
+
50
+ Returns:
51
+ np.ndarray: Root zone moisture (Mrz) (value 0 to 1).
52
+ """
53
+ s44 = (SVP_hPa - Ea_hPa) / (Ta_C - Td_C)
54
+ kTSTD = (ST_C - Tsd_C) / (Ta_C - Td_C)
55
+ Mrz = (gamma_hPa * s1_hPa * (Tsd_C - Td_C)) / (delta_hPa * s3_hPa * kTSTD * (ST_C - Td_C) + gamma_hPa * s44 * (Ta_C - Td_C) - delta_hPa * s1_hPa * (Tsd_C - Td_C))
56
+ Mrz = rt.where((Rn_Wm2 < 0) & (dTS_C < 0) & (Mrz < 0), np.abs(Mrz), Mrz)
57
+ Mrz = rt.where((Rn_Wm2 < 0) & (dTS_C > 0) & (Mrz < 0), np.abs(Mrz), Mrz)
58
+ Mrz = rt.where((Rn_Wm2 > 0) & (dTS_C < 0) & (Mrz < 0), np.abs(Mrz), Mrz)
59
+ Mrz = rt.where((Rn_Wm2 > 0) & (dTS_C > 0) & (Mrz < 0), np.abs(Mrz), Mrz)
60
+ Mrz = rt.where((Rg_Wm2 > 0) & (Mrz < 0), np.abs(Mrz), Mrz)
61
+ Mrz = rt.where((Rg_Wm2 < 0) & (Mrz < 0), np.abs(Mrz), Mrz)
62
+ Mrz = rt.where((Td_C < 0) & (Mrz < 0), np.abs(Mrz), Mrz)
63
+ Mrz = rt.where(Mrz > 1, 1, Mrz)
64
+ Mrz = rt.where(Mrz < 0, 0.0001, Mrz)
65
+
66
+ return Mrz
@@ -0,0 +1,115 @@
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 .FVC_from_NDVI import FVC_from_NDVI
8
+ from .LAI_from_NDVI import LAI_from_NDVI
9
+
10
+ from .constants import GAMMA_HPA
11
+ from .root_zone_initialization import calculate_root_zone_moisture
12
+
13
+ def initialize_soil_moisture(
14
+ delta_hPa: Union[Raster, np.ndarray], # Rate of change of saturation vapor pressure with temperature (hPa/°C)
15
+ ST_C: Union[Raster, np.ndarray], # Surface temperature (°C)
16
+ Ta_C: Union[Raster, np.ndarray], # Air temperature (°C)
17
+ Td_C: Union[Raster, np.ndarray], # Dewpoint temperature (°C)
18
+ dTS: Union[Raster, np.ndarray], # Difference between surface and air temperature (°C)
19
+ Rn_Wm2: Union[Raster, np.ndarray], # Net radiation (W/m²)
20
+ LWnet_Wm2: Union[Raster, np.ndarray], # Net longwave radiation (W/m²)
21
+ FVC: Union[Raster, np.ndarray], # Fractional vegetation cover (unitless)
22
+ VPD_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit (hPa)
23
+ SVP_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure (hPa)
24
+ Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure (hPa)
25
+ Estar_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure at surface temperature (hPa)
26
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA # Psychrometric constant (hPa/°C)
27
+ ) -> Tuple[Union[Raster, np.ndarray]]:
28
+ """
29
+ This function estimates the soil moisture availability (SM) based on thermal IR and meteorological information.
30
+ The estimated SM is treated as initial SM, which will be later estimated through iteration in the actual ET estimation loop to establish feedback between SM and biophysical states.
31
+
32
+ Parameters:
33
+ delta (np.ndarray): Rate of change of saturation vapor pressure with temperature (kPa/°C)
34
+ ST_C (np.ndarray): Surface temperature (°C)
35
+ Ta_C (np.ndarray): Air temperature (°C)
36
+ Td_C (np.ndarray): Dewpoint temperature (°C)
37
+ dTS (np.ndarray): Difference between surface and air temperature (°C)
38
+ Rn (np.ndarray): Net radiation (W/m²)
39
+ LWnet (np.ndarray): Net longwave radiation (W/m²)
40
+ NDVI (np.ndarray): Normalized Difference Vegetation Index (unitless)
41
+ VPD_hPa (np.ndarray): Vapor pressure deficit (hPa)
42
+ SVP_hPa (np.ndarray): Saturation vapor pressure (hPa)
43
+ Ea_hPa (np.ndarray): Actual vapor pressure (hPa)
44
+ Estar (np.ndarray): Saturation vapor pressure at surface temperature (hPa)
45
+
46
+ Returns:
47
+ SM (np.ndarray): soil moisture (m³/m³)
48
+ Mrz (np.ndarray): The rootzone moisture (m³/m³)
49
+ Ms (np.ndarray): The surface moisture (m³/m³)
50
+ Ep_PT (np.ndarray): The potential evaporation (Priestley-Taylor eqn.) (mm/day)
51
+ Ds (np.ndarray): The vapor pressure deficit at surface (hPa)
52
+ s1 (np.ndarray): The slope of SVP at dewpoint temperature (hPa/K)
53
+ s3 (np.ndarray): The slope of SVP at surface temperature (hPa/K)
54
+ Tsd_C (np.ndarray): The surface dewpoint temperature (°C)
55
+ """
56
+ # Compute the surface dewpoint temperature
57
+ s11 = (45.03 + 3.014 * Td_C + 0.05345 * Td_C ** 2 + 0.00224 * Td_C ** 3) * 1e-2 # slope of SVP at TD (hpa/K)
58
+ s22 = (Estar_hPa - Ea_hPa) / (ST_C - Td_C)
59
+ s33 = (45.03 + 3.014 * ST_C + 0.05345 * ST_C ** 2 + 0.00224 * ST_C ** 3) * 1e-2 # slope of SVP at TS (hpa/K)
60
+ s44 = (SVP_hPa - Ea_hPa) / (Ta_C - Td_C)
61
+ Tsd_C = (Estar_hPa - Ea_hPa - s33 * ST_C + s11 * Td_C) / (s11 - s33) # Surface dewpoint temperature (degC)
62
+
63
+ # Calculate the surface moisture or wetness
64
+ Msurf = (s11 / s22) * ((Tsd_C - Td_C) / (ST_C - Td_C)) # Surface wetness
65
+ Msurf = rt.clip(Msurf, 0.0001, 0.9999)
66
+
67
+ # Calculate the surface vapor pressure and deficit
68
+ esurf = Ea_hPa + Msurf * (Estar_hPa - Ea_hPa)
69
+ Dsurf = esurf - Ea_hPa
70
+
71
+ # Separate the soil and canopy wetness to form a composite surface moisture
72
+ Ms = Msurf
73
+ Mcan = FVC * Msurf
74
+ Msoil = (1 - FVC) * Msurf
75
+
76
+ TdewIndex = (ST_C - Tsd_C) / (Ta_C - Td_C) # % TdewIndex > 1 signifies super dry condition
77
+ Ep_PT = (1.26 * delta_hPa * Rn_Wm2) / (delta_hPa + gamma_hPa) # Potential evaporation (Priestley-Taylor eqn.)
78
+
79
+ # Adjust surface wetness based on certain conditions
80
+ Ms = rt.where((FVC <= 0.25) & (TdewIndex < 1), Msoil, Ms)
81
+ Mcan = rt.where((FVC <= 0.25) & (TdewIndex < 1), 0, Mcan)
82
+ Ms = rt.where((FVC <= 0.25) & (Ta_C > 10) & (Td_C < 0) & (LWnet_Wm2 < -125), Msoil, Ms)
83
+ Mcan = rt.where((FVC <= 0.25) & (Ta_C > 10) & (Td_C < 0) & (LWnet_Wm2 < -125), 0, Mcan)
84
+
85
+ # Calculate the root-zone moisture
86
+ SMrz = calculate_root_zone_moisture(
87
+ delta_hPa=delta_hPa,
88
+ ST_C=ST_C,
89
+ Ta_C=Ta_C,
90
+ Td_C=Td_C,
91
+ s11=s11,
92
+ s33=s33,
93
+ s44=s44,
94
+ Tsd_C=Tsd_C,
95
+ gamma_hPa=gamma_hPa
96
+ )
97
+
98
+ # Combine soil moisture to account for hysteresis and initial estimation of surface vapor pressure
99
+ SM = Ms
100
+ SM = rt.where((Ep_PT > Rn_Wm2) & (dTS > 0), SMrz, SM)
101
+ SM = rt.where((Ep_PT > Rn_Wm2) & (FVC <= 0.25), SMrz, SM)
102
+ SM = rt.where((Ep_PT > Rn_Wm2) & (Dsurf > VPD_hPa), SMrz, SM)
103
+ SM = rt.where((FVC <= 0.25) & (dTS > 0) & (Ta_C > 10) & (Td_C < 0) & (LWnet_Wm2 < -125), SMrz, SM)
104
+ SM = rt.where((FVC <= 0.25) & (dTS > 0) & (Ta_C > 10) & (Td_C < 0) & (Dsurf > VPD_hPa), SMrz, SM)
105
+ SM = rt.where((Ep_PT < Rn_Wm2) & (FVC <= 0.25) & (Dsurf > VPD_hPa), SMrz, SM)
106
+
107
+ es = Ea_hPa + SM * (Estar_hPa - Ea_hPa)
108
+
109
+ # vapor pressure deficit at surface
110
+ Ds = (Estar_hPa - es)
111
+
112
+ s1 = s11
113
+ s3 = s33
114
+
115
+ return SM, SMrz, Ms, Ep_PT, Ds, s1, s3, Tsd_C
@@ -0,0 +1,131 @@
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 GAMMA_HPA
9
+ from .root_zone_iteration import calculate_rootzone_moisture
10
+
11
+ def iterate_soil_moisture(
12
+ delta_hPa: Union[Raster, np.ndarray], # Rate of change of saturation vapor pressure with temperature (hPa/°C)
13
+ s1: Union[Raster, np.ndarray], # The slope of SVP at surface temperature (hPa/K)
14
+ s3: Union[Raster, np.ndarray], # The slope of SVP at dewpoint temperature (hPa/K)
15
+ ST_C: Union[Raster, np.ndarray], # surface temperature (°C)
16
+ Ta_C: Union[Raster, np.ndarray], # air temperature (°C)
17
+ dTS_C: Union[Raster, np.ndarray], # difference between surface and air temperature (°C)
18
+ Td_C: Union[Raster, np.ndarray], # dewpoint temperature (°C)
19
+ Tsd_C: Union[Raster, np.ndarray], # surface dewpoint temperature (°C)
20
+ Rg_Wm2: Union[Raster, np.ndarray], # Incoming solar radiation (W/m^2)
21
+ Rn_Wm2: Union[Raster, np.ndarray], # Net radiation (W/m^2)
22
+ LWnet_Wm2: Union[Raster, np.ndarray], # Net longwave radiation (W/m^2)
23
+ FVC: Union[Raster, np.ndarray], # Fractional vegetation cover (unitless)
24
+ VPD_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit (hPa)
25
+ D0_hPa: Union[Raster, np.ndarray], # Vapor pressure deficit at source (hPa)
26
+ SVP_hPa: Union[Raster, np.ndarray], # Saturation vapor pressure (hPa)
27
+ Ea_hPa: Union[Raster, np.ndarray], # Actual vapor pressure (hPa)
28
+ T0: Union[Raster, np.ndarray], # Temperature at source (°C)
29
+ gamma_hPa: Union[Raster, np.ndarray, float] = GAMMA_HPA # Psychrometric constant (hPa/°C)
30
+ ) -> Union[Raster, np.ndarray]:
31
+ """
32
+ Estimates the soil moisture availability (SM) (or wetness) (value 0 to 1) based on thermal IR and meteorological
33
+ information. However, this M will be treated as initial M, which will be later on estimated through iteration in the
34
+ actual ET estimation loop to establish feedback between M and biophysical states.
35
+
36
+ Args:
37
+ delta (np.ndarray): Rate of change of saturation vapor pressure with temperature (kPa/°C).
38
+ s1 (np.ndarray): The slope of saturation vapor pressure at surface temperature (hPa/K).
39
+ s3 (np.ndarray): The slope of saturation vapor pressure at dewpoint temperature (hPa/K).
40
+ ST_C (np.ndarray): Surface temperature in degrees Celsius.
41
+ Ta_C (np.ndarray): Air temperature in degrees Celsius.
42
+ dTS (np.ndarray): Difference between surface and air temperature in degrees Celsius.
43
+ Td_C (np.ndarray): Dewpoint temperature in degrees Celsius.
44
+ Tsd_C (np.ndarray): Surface dewpoint temperature in degrees Celsius.
45
+ Rg (np.ndarray): Incoming solar radiation in W/m^2.
46
+ Rn (np.ndarray): Net radiation in W/m^2.
47
+ LWnet (np.ndarray): Net longwave radiation in W/m^2.
48
+ FVC (np.ndarray): Fractional vegetation cover (unitless).
49
+ VPD_hPa (np.ndarray): Vapor pressure deficit in hPa.
50
+ D0 (np.ndarray): Vapor pressure deficit at source in hPa.
51
+ SVP_hPa (np.ndarray): Saturation vapor pressure in hPa.
52
+ Ea_hPa (np.ndarray): Actual vapor pressure in hPa.
53
+ T0 (np.ndarray): Temperature at source in degrees Celsius.
54
+
55
+ Returns:
56
+ np.ndarray: Soil moisture availability (SM) (or wetness) (value 0 to 1).
57
+ """
58
+ # calculate surface wetness (Msurf)
59
+ kTSTD = (T0 - Td_C) / (ST_C - Td_C)
60
+ Msurf = (s1 / s3) * ((Tsd_C - Td_C) / (kTSTD * (ST_C - Td_C))) # Surface wetness
61
+ Msurf = rt.where((Rn_Wm2 < 0) & (dTS_C < 0) & (Msurf < 0), np.abs(Msurf), Msurf)
62
+ Msurf = rt.where((Rn_Wm2 < 0) & (dTS_C > 0) & (Msurf < 0), np.abs(Msurf), Msurf)
63
+ Msurf = rt.where((Rn_Wm2 > 0) & (dTS_C < 0) & (Msurf < 0), np.abs(Msurf), Msurf)
64
+ Msurf = rt.where((Rn_Wm2 > 0) & (dTS_C > 0) & (Msurf < 0), np.abs(Msurf), Msurf)
65
+
66
+ # solar radiation is only used to correct negative values of surface wetness
67
+ Msurf = rt.where((Rg_Wm2 > 0) & (Msurf < 0), np.abs(Msurf), Msurf)
68
+ Msurf = rt.where((Rg_Wm2 < 0) & (Msurf < 0), np.abs(Msurf), Msurf)
69
+
70
+ Msurf = rt.where((Td_C < 0) & (Msurf < 0), np.abs(Msurf), Msurf)
71
+ Msurf = rt.clip(Msurf, 0.0001, 1)
72
+
73
+ # Separating soil and canopy wetness to form a composite surface moisture
74
+ Ms = Msurf
75
+ Mcan = FVC * Msurf
76
+ Msoil = (1 - FVC) * Msurf
77
+
78
+ TdewIndex = (ST_C - Tsd_C) / (Ta_C - Td_C)
79
+
80
+ # Potential evaporation (Priestley-Taylor eqn.)
81
+ Ep_PT = (1.26 * delta_hPa * Rn_Wm2) / (delta_hPa + gamma_hPa)
82
+
83
+ # calculate surface wetness (Ms)
84
+ Ms = rt.where((Ep_PT > Rn_Wm2) & (FVC <= 0.25) & (TdewIndex < 1), Msoil, Ms)
85
+ Ms = rt.where((FVC <= 0.25) & (Ta_C > 10) & (Td_C < 0) & (LWnet_Wm2 < -125), Msoil, Ms)
86
+ Ms = rt.where((Rn_Wm2 > Ep_PT) & (FVC <= 0.25) & (TdewIndex < 1) & (Td_C <= 0), Msoil, Ms)
87
+ Ms = rt.where((Rn_Wm2 > Ep_PT) & (FVC <= 0.25) & (TdewIndex < 1), Msoil, Ms)
88
+ Ms = rt.where((D0_hPa > VPD_hPa) & (FVC <= 0.25) & (TdewIndex < 1), Msoil, Ms)
89
+
90
+ # calculate canopy wetness (Mcan)
91
+ Mcan = rt.where((Ep_PT > Rn_Wm2) & (FVC <= 0.25) & (TdewIndex < 1), 0, Mcan)
92
+ Mcan = rt.where((FVC <= 0.25) & (Ta_C > 10) & (Td_C < 0) & (LWnet_Wm2 < -125), 0, Mcan)
93
+ Mcan = rt.where((Rn_Wm2 > Ep_PT) & (FVC <= 0.25) & (TdewIndex < 1) & (Td_C <= 0), 0, Mcan)
94
+ Mcan = rt.where((Rn_Wm2 > Ep_PT) & (FVC <= 0.25) & (TdewIndex < 1), 0, Mcan)
95
+ Mcan = rt.where((D0_hPa > VPD_hPa) & (FVC <= 0.25) & (TdewIndex < 1), 0, Mcan)
96
+
97
+ # calculate rootzone moisture (Mrz)
98
+ Mrz = calculate_rootzone_moisture(
99
+ delta_hPa=delta_hPa,
100
+ s1_hPa=s1,
101
+ s3_hPa=s3,
102
+ ST_C=ST_C,
103
+ Ta_C=Ta_C,
104
+ dTS_C=dTS_C,
105
+ Td_C=Td_C,
106
+ Tsd_C=Tsd_C,
107
+ Rg_Wm2=Rg_Wm2,
108
+ Rn_Wm2=Rn_Wm2,
109
+ LWnet_Wm2=LWnet_Wm2,
110
+ FVC=FVC,
111
+ VPD_hPa=VPD_hPa,
112
+ D0_hPa=D0_hPa,
113
+ SVP_hPa=SVP_hPa,
114
+ Ea_hPa=Ea_hPa,
115
+ gamma_hPa=gamma_hPa
116
+ )
117
+
118
+ TdewIndex = (ST_C - Tsd_C) / (Ta_C - Td_C)
119
+
120
+ # Potential evaporation (Priestley-Taylor eqn.)
121
+ Ep_PT = (1.26 * delta_hPa * Rn_Wm2) / (delta_hPa + gamma_hPa)
122
+
123
+ # COMBINE M to account for Hysteresis and initial estimation of surface vapor pressure
124
+ SM = Msurf
125
+ SM = rt.where((Ep_PT > Rn_Wm2) & (dTS_C > 0) & (FVC <= 0.25) & (D0_hPa > VPD_hPa) & (TdewIndex < 1), Mrz, SM)
126
+ SM = rt.where((FVC <= 0.25) & (dTS_C > 0) & (Ta_C > 10) & (Td_C < 0) & (LWnet_Wm2 < -125) & (D0_hPa > VPD_hPa), Mrz, SM)
127
+
128
+ return SM
129
+
130
+
131
+
STIC_JPL/verify.py ADDED
@@ -0,0 +1,71 @@
1
+ def verify() -> bool:
2
+ """
3
+ Verifies the correctness of the PT-JPL-SM model implementation by comparing
4
+ its outputs to a reference dataset.
5
+
6
+ This function loads a known input table and the corresponding expected output table.
7
+ It runs the model on the input data, then compares the resulting outputs to the
8
+ reference outputs for key variables using strict numerical tolerances. If all
9
+ outputs match within tolerance, the function returns True. Otherwise, it prints
10
+ which column failed and returns False.
11
+
12
+ Returns:
13
+ bool: True if all model outputs match the reference outputs within tolerance, False otherwise.
14
+ """
15
+ import pandas as pd
16
+ import numpy as np
17
+ from .ECOv002_calval_STIC_inputs import load_ECOv002_calval_STIC_inputs
18
+ from .process_STIC_table import process_STIC_table
19
+ import os
20
+
21
+ # Load input and output tables
22
+ input_df = load_ECOv002_calval_STIC_inputs()
23
+ module_dir = os.path.dirname(os.path.abspath(__file__))
24
+ output_file_path = os.path.join(module_dir, "ECOv002-cal-val-STIC-JPL-outputs.csv")
25
+ output_df = pd.read_csv(output_file_path)
26
+
27
+ # Run the model on the input table
28
+ model_df = process_STIC_table(input_df)
29
+
30
+ # Columns to compare (model outputs)
31
+ output_columns = [
32
+ "G_Wm2",
33
+ "LE_Wm2"
34
+ ]
35
+
36
+ # Compare each output column and collect mismatches
37
+ mismatches = []
38
+ for col in output_columns:
39
+ if col not in model_df or col not in output_df:
40
+ mismatches.append((col, 'missing_column', None))
41
+ continue
42
+ model_vals = model_df[col].values
43
+ ref_vals = output_df[col].values
44
+ # Use numpy allclose for floating point comparison
45
+ if not np.allclose(model_vals, ref_vals, rtol=1e-5, atol=1e-8, equal_nan=True):
46
+ # Find indices where values differ
47
+ diffs = np.abs(model_vals - ref_vals)
48
+ max_diff = np.nanmax(diffs)
49
+ idxs = np.where(~np.isclose(model_vals, ref_vals, rtol=1e-5, atol=1e-8, equal_nan=True))[0]
50
+ mismatch_info = {
51
+ 'indices': idxs.tolist(),
52
+ 'model_values': model_vals[idxs].tolist(),
53
+ 'ref_values': ref_vals[idxs].tolist(),
54
+ 'diffs': diffs[idxs].tolist(),
55
+ 'max_diff': float(max_diff)
56
+ }
57
+ mismatches.append((col, 'value_mismatch', mismatch_info))
58
+ if mismatches:
59
+ print("Verification failed. Details:")
60
+ for col, reason, info in mismatches:
61
+ if reason == 'missing_column':
62
+ print(f" Missing column: {col}")
63
+ elif reason == 'value_mismatch':
64
+ print(f" Mismatch in column: {col}")
65
+ print(f" Max difference: {info['max_diff']}")
66
+ print(f" Indices off: {info['indices']}")
67
+ print(f" Model values: {info['model_values']}")
68
+ print(f" Reference values: {info['ref_values']}")
69
+ print(f" Differences: {info['diffs']}")
70
+ return False
71
+ return True
STIC_JPL/version.py ADDED
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("STIC-JPL")