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,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