PM-JPL 1.2.1__py3-none-any.whl → 1.7.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.
- PMJPL/ECOv002-cal-val-PM-JPL-inputs.csv +1048 -0
- PMJPL/ECOv002-cal-val-PM-JPL-outputs.csv +1048 -0
- PMJPL/ECOv002-static-tower-PM-JPL-inputs.csv +122 -0
- PMJPL/ECOv002_calval_PMJPL_inputs.py +19 -0
- PMJPL/ECOv002_static_tower_PMJPL_inputs.py +19 -0
- PMJPL/PMJPL.py +45 -378
- PMJPL/{parameters.py → PMJPL_parameter_from_IGBP.py} +22 -7
- PMJPL/VPD_factor.py +1 -1
- PMJPL/__init__.py +1 -6
- PMJPL/calculate_gamma.py +128 -0
- PMJPL/canopy_aerodynamic_resistance.py +151 -18
- PMJPL/canopy_conductance.py +71 -15
- PMJPL/closed_minimum_temperature.py +11 -0
- PMJPL/closed_vapor_pressure_deficit.py +11 -0
- PMJPL/constants.py +6 -1
- PMJPL/correctance_factor.py +56 -7
- PMJPL/generate_PMJPL_inputs.py +263 -0
- PMJPL/interception.py +1 -3
- PMJPL/leaf_conductance_to_evaporated_water.py +11 -0
- PMJPL/leaf_conductance_to_sensible_heat.py +58 -0
- PMJPL/maximum_boundary_layer_resistance.py +11 -0
- PMJPL/minimum_boundary_layer_resistance.py +11 -0
- PMJPL/{tmin_factor.py → minimum_temperature_factor.py} +2 -2
- PMJPL/mod16.csv +19 -0
- PMJPL/model.py +690 -0
- PMJPL/open_minimum_temperature.py +11 -0
- PMJPL/open_vapor_pressure_deficit.py +11 -0
- PMJPL/potential_soil_evaporation.py +2 -2
- PMJPL/potential_stomatal_conductance.py +11 -0
- PMJPL/process_PMJPL_table.py +208 -0
- PMJPL/process_daily_ET_table.py +40 -0
- PMJPL/transpiration.py +4 -4
- PMJPL/verify.py +77 -0
- PMJPL/version.py +4 -0
- PMJPL/wet_canopy_resistance.py +1 -0
- PMJPL/wet_soil_evaporation.py +4 -4
- {pm_jpl-1.2.1.dist-info → pm_jpl-1.7.0.dist-info}/METADATA +18 -21
- pm_jpl-1.7.0.dist-info/RECORD +42 -0
- {pm_jpl-1.2.1.dist-info → pm_jpl-1.7.0.dist-info}/WHEEL +1 -1
- PMJPL/MCD12C1/MCD12C1.py +0 -10
- PMJPL/MCD12C1/__init__.py +0 -1
- PMJPL/SEBAL/SEBAL.py +0 -45
- PMJPL/SEBAL/__init__.py +0 -1
- PMJPL/downscaling/__init__.py +0 -1
- PMJPL/downscaling/downscaling.py +0 -271
- PMJPL/downscaling/linear_downscale.py +0 -71
- PMJPL/evapotranspiration_conversion/__init__.py +0 -1
- PMJPL/evapotranspiration_conversion/evapotranspiration_conversion.py +0 -80
- PMJPL/fwet.py +0 -21
- PMJPL/meteorology_conversion/__init__.py +0 -1
- PMJPL/meteorology_conversion/meteorology_conversion.py +0 -123
- PMJPL/penman_monteith/__init__.py +0 -1
- PMJPL/penman_monteith/penman_monteith.py +0 -20
- PMJPL/priestley_taylor/__init__.py +0 -1
- PMJPL/priestley_taylor/priestley_taylor.py +0 -27
- PMJPL/santanello/__init__.py +0 -1
- PMJPL/santanello/santanello.py +0 -46
- PMJPL/soil_heat_flux/__init__.py +0 -1
- PMJPL/soil_heat_flux/soil_heat_flux.py +0 -62
- PMJPL/vegetation_conversion/__init__.py +0 -1
- PMJPL/vegetation_conversion/vegetation_conversion.py +0 -47
- PMJPL/verma_net_radiation/__init__.py +0 -1
- PMJPL/verma_net_radiation/verma_net_radiation.py +0 -108
- pm_jpl-1.2.1.dist-info/RECORD +0 -44
- {pm_jpl-1.2.1.dist-info → pm_jpl-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {pm_jpl-1.2.1.dist-info → pm_jpl-1.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .PMJPL_parameter_from_IGBP import PMJPL_parameter_from_IGBP
|
|
2
|
+
from rasters import Raster, RasterGeometry, VectorGeometry
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
def open_vapor_pressure_deficit(IGBP: Union[Raster, int], geometry=None, IGBP_upsampling_resolution_meters=None):
|
|
6
|
+
return PMJPL_parameter_from_IGBP(
|
|
7
|
+
variable="VPD_open",
|
|
8
|
+
IGBP=IGBP,
|
|
9
|
+
geometry=geometry,
|
|
10
|
+
IGBP_upsampling_resolution_meters=IGBP_upsampling_resolution_meters
|
|
11
|
+
)
|
|
@@ -11,7 +11,7 @@ def calculate_potential_soil_evaporation(
|
|
|
11
11
|
rho: float,
|
|
12
12
|
Cp_Jkg: float,
|
|
13
13
|
FVC: float,
|
|
14
|
-
|
|
14
|
+
VPD_Pa: float,
|
|
15
15
|
ras: float,
|
|
16
16
|
fwet: float,
|
|
17
17
|
rtot: float,
|
|
@@ -39,7 +39,7 @@ def calculate_potential_soil_evaporation(
|
|
|
39
39
|
- The Penman-Monteith equation takes into account various factors such as radiation, air density, heat capacity, vegetation cover, vapor pressure deficit, aerodynamic resistance, soil wetness, and total resistance.
|
|
40
40
|
- The function returns the potential soil evaporation as either a Raster object or a NumPy array, depending on the input data type.
|
|
41
41
|
"""
|
|
42
|
-
numerator = (delta_Pa * Asoil + rho * Cp_Jkg * (1.0 - FVC) *
|
|
42
|
+
numerator = (delta_Pa * Asoil + rho * Cp_Jkg * (1.0 - FVC) * VPD_Pa / ras) * (1.0 - fwet)
|
|
43
43
|
denominator = delta_Pa + gamma_Pa * rtot / ras
|
|
44
44
|
LE_soil_pot = numerator / denominator
|
|
45
45
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .PMJPL_parameter_from_IGBP import PMJPL_parameter_from_IGBP
|
|
2
|
+
from rasters import Raster, RasterGeometry, VectorGeometry
|
|
3
|
+
from typing import Union
|
|
4
|
+
|
|
5
|
+
def potential_stomatal_conductance(IGBP: Union[Raster, int], geometry=None, IGBP_upsampling_resolution_meters=None):
|
|
6
|
+
return PMJPL_parameter_from_IGBP(
|
|
7
|
+
variable="CL",
|
|
8
|
+
IGBP=IGBP,
|
|
9
|
+
geometry=geometry,
|
|
10
|
+
IGBP_upsampling_resolution_meters=IGBP_upsampling_resolution_meters
|
|
11
|
+
)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module: process_PMJPL_table.py
|
|
3
|
+
|
|
4
|
+
This module provides a function to process input data for the PM-JPL (Penman-Monteith Jet Propulsion Laboratory) model.
|
|
5
|
+
It prepares the required variables from a pandas DataFrame, handles missing or alternative column names, computes derived variables as needed, and runs the PM-JPL model to generate output variables, which are appended to the input DataFrame.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import rasters as rt
|
|
12
|
+
from rasters import MultiPoint, WGS84
|
|
13
|
+
|
|
14
|
+
from dateutil import parser
|
|
15
|
+
from pandas import DataFrame
|
|
16
|
+
|
|
17
|
+
from .constants import *
|
|
18
|
+
from .PMJPL import PMJPL
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# FIXME include additional inputs required by PM-JPL that were not required by PT-JPL
|
|
23
|
+
|
|
24
|
+
def process_PMJPL_table(
|
|
25
|
+
input_df: DataFrame,
|
|
26
|
+
upscale_to_daylight: bool = False,
|
|
27
|
+
regenerate_net_radiation: bool = False
|
|
28
|
+
) -> DataFrame:
|
|
29
|
+
"""
|
|
30
|
+
Processes an input DataFrame to prepare all required variables for the PM-JPL model,
|
|
31
|
+
runs the model, and returns a DataFrame with the model outputs appended as new columns.
|
|
32
|
+
|
|
33
|
+
This function is designed to work with tabular (pandas DataFrame) data, such as point or site-level measurements or extracted pixel values from gridded products. It is compatible with DataFrames produced by ECOSTRESS Cal-Val or similar sources, and is suitable for both single-site and batch sensitivity analysis workflows.
|
|
34
|
+
|
|
35
|
+
The function is commonly used as a forward process in sensitivity or perturbation analysis, and can be chained with net radiation calculations prior to running the PM-JPL model.
|
|
36
|
+
|
|
37
|
+
Expected Input DataFrame Columns:
|
|
38
|
+
- 'NDVI': Normalized Difference Vegetation Index (required)
|
|
39
|
+
- 'ST_C': Surface temperature in Celsius (required)
|
|
40
|
+
- 'albedo': Surface albedo (required)
|
|
41
|
+
- 'Ta_C' or 'Ta': Air temperature in Celsius (required)
|
|
42
|
+
- 'RH': Relative humidity (0-1, required)
|
|
43
|
+
- 'Rn_Wm2': Net radiation (W/m^2, required)
|
|
44
|
+
- 'time_UTC': Time in UTC (required)
|
|
45
|
+
- 'geometry': Geometry object (optional, will be constructed from 'lat' and 'lon' if missing)
|
|
46
|
+
- 'lat', 'lon': Latitude and longitude (optional, used to construct geometry if needed)
|
|
47
|
+
|
|
48
|
+
The function will attempt to load or compute any missing optional variables using spatial context if possible.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
DataFrame: The input DataFrame with PM-JPL model outputs added as columns. Output columns include:
|
|
52
|
+
- 'LE': Total instantaneous evapotranspiration
|
|
53
|
+
- 'LE_canopy': Canopy transpiration
|
|
54
|
+
- 'LE_soil': Soil evaporation
|
|
55
|
+
- 'LE_interception': Interception evaporation
|
|
56
|
+
- 'PET': Potential evapotranspiration
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
Suppose you have a CSV file with columns: NDVI, ST_C, albedo, Ta_C, RH, Rn, time_UTC, lat, lon
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import pandas as pd
|
|
63
|
+
from PMJPL.process_PMJPL_table import process_PMJPL_table
|
|
64
|
+
|
|
65
|
+
# Load your data
|
|
66
|
+
df = pd.read_csv('my_input_data.csv')
|
|
67
|
+
|
|
68
|
+
# Process the table and run the PM-JPL model
|
|
69
|
+
output_df = process_PMJPL_table(df)
|
|
70
|
+
|
|
71
|
+
# The output DataFrame will have new columns: 'LE', 'LE_canopy', 'LE_soil', 'LE_interception', 'PET'
|
|
72
|
+
print(output_df.head())
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Notes:
|
|
76
|
+
- If any required columns are missing, a KeyError will be raised.
|
|
77
|
+
- If geometry is not provided, latitude and longitude columns are required to construct spatial context.
|
|
78
|
+
- All input columns should be numeric and of compatible shape.
|
|
79
|
+
- This function is suitable for batch-processing site-level or point data tables for ET partitioning and for use in sensitivity analysis workflows.
|
|
80
|
+
"""
|
|
81
|
+
logger.info("starting PM-JPL table processing")
|
|
82
|
+
|
|
83
|
+
# Extract and typecast surface temperature (ST_C) and NDVI
|
|
84
|
+
ST_C = np.array(input_df.ST_C).astype(np.float64)
|
|
85
|
+
emissivity = np.array(input_df.emissivity).astype(np.float64)
|
|
86
|
+
NDVI = np.array(input_df.NDVI).astype(np.float64)
|
|
87
|
+
|
|
88
|
+
# Mask NDVI values below threshold (0.06) as NaN
|
|
89
|
+
NDVI = np.where(NDVI > 0.06, NDVI, np.nan).astype(np.float64)
|
|
90
|
+
|
|
91
|
+
# Extract and typecast albedo
|
|
92
|
+
albedo = np.array(input_df.albedo).astype(np.float64)
|
|
93
|
+
|
|
94
|
+
# Handle air temperature column name differences (Ta_C or Ta)
|
|
95
|
+
if "Ta_C" in input_df:
|
|
96
|
+
Ta_C = np.array(input_df.Ta_C).astype(np.float64)
|
|
97
|
+
elif "Ta" in input_df:
|
|
98
|
+
Ta_C = np.array(input_df.Ta).astype(np.float64)
|
|
99
|
+
else:
|
|
100
|
+
raise KeyError("Input DataFrame must contain either 'Ta_C' or 'Ta' column.")
|
|
101
|
+
|
|
102
|
+
# Extract and typecast relative humidity and net radiation
|
|
103
|
+
RH = np.array(input_df.RH).astype(np.float64)
|
|
104
|
+
|
|
105
|
+
if "SWin_Wm2" in input_df:
|
|
106
|
+
SWin_Wm2 = np.array(input_df.SWin_Wm2).astype(np.float64)
|
|
107
|
+
else:
|
|
108
|
+
SWin_Wm2 = None
|
|
109
|
+
|
|
110
|
+
if "Rn_Wm2" in input_df:
|
|
111
|
+
Rn_Wm2 = np.array(input_df.Rn_Wm2).astype(np.float64)
|
|
112
|
+
else:
|
|
113
|
+
Rn_Wm2 = None
|
|
114
|
+
|
|
115
|
+
if "Rn_daily_Wm2" in input_df:
|
|
116
|
+
Rn_daylight_Wm2 = np.array(input_df.Rn_daily_Wm2).astype(np.float64)
|
|
117
|
+
else:
|
|
118
|
+
Rn_daylight_Wm2 = None
|
|
119
|
+
|
|
120
|
+
if "Tmin_C" in input_df:
|
|
121
|
+
Tmin_C = np.array(input_df.Tmin_C).astype(np.float64)
|
|
122
|
+
else:
|
|
123
|
+
Tmin_C = None
|
|
124
|
+
|
|
125
|
+
if "elevation_km" in input_df:
|
|
126
|
+
elevation_km = np.array(input_df.elevation_km).astype(np.float64)
|
|
127
|
+
else:
|
|
128
|
+
elevation_km = None
|
|
129
|
+
|
|
130
|
+
if "IGBP" in input_df:
|
|
131
|
+
IGBP = np.array(input_df.IGBP).astype(np.int8)
|
|
132
|
+
else:
|
|
133
|
+
IGBP = None
|
|
134
|
+
|
|
135
|
+
# --- Handle geometry and time columns ---
|
|
136
|
+
import pandas as pd
|
|
137
|
+
from rasters import MultiPoint, WGS84
|
|
138
|
+
from shapely.geometry import Point
|
|
139
|
+
|
|
140
|
+
def ensure_geometry(df):
|
|
141
|
+
if "geometry" in df:
|
|
142
|
+
if isinstance(df.geometry.iloc[0], str):
|
|
143
|
+
def parse_geom(s):
|
|
144
|
+
s = s.strip()
|
|
145
|
+
if s.startswith("POINT"):
|
|
146
|
+
coords = s.replace("POINT", "").replace("(", "").replace(")", "").strip().split()
|
|
147
|
+
return Point(float(coords[0]), float(coords[1]))
|
|
148
|
+
elif "," in s:
|
|
149
|
+
coords = [float(c) for c in s.split(",")]
|
|
150
|
+
return Point(coords[0], coords[1])
|
|
151
|
+
else:
|
|
152
|
+
coords = [float(c) for c in s.split()]
|
|
153
|
+
return Point(coords[0], coords[1])
|
|
154
|
+
df = df.copy()
|
|
155
|
+
df['geometry'] = df['geometry'].apply(parse_geom)
|
|
156
|
+
return df
|
|
157
|
+
|
|
158
|
+
input_df = ensure_geometry(input_df)
|
|
159
|
+
|
|
160
|
+
logger.info("started extracting geometry from PM-JPL input table")
|
|
161
|
+
|
|
162
|
+
if "geometry" in input_df:
|
|
163
|
+
# Convert Point objects to coordinate tuples for MultiPoint
|
|
164
|
+
if hasattr(input_df.geometry.iloc[0], "x") and hasattr(input_df.geometry.iloc[0], "y"):
|
|
165
|
+
coords = [(pt.x, pt.y) for pt in input_df.geometry]
|
|
166
|
+
geometry = MultiPoint(coords, crs=WGS84)
|
|
167
|
+
else:
|
|
168
|
+
geometry = MultiPoint(input_df.geometry, crs=WGS84)
|
|
169
|
+
elif "lat" in input_df and "lon" in input_df:
|
|
170
|
+
lat = np.array(input_df.lat).astype(np.float64)
|
|
171
|
+
lon = np.array(input_df.lon).astype(np.float64)
|
|
172
|
+
geometry = MultiPoint(x=lon, y=lat, crs=WGS84)
|
|
173
|
+
else:
|
|
174
|
+
raise KeyError("Input DataFrame must contain either 'geometry' or both 'lat' and 'lon' columns.")
|
|
175
|
+
|
|
176
|
+
logger.info("completed extracting geometry from PM-JPL input table")
|
|
177
|
+
|
|
178
|
+
logger.info("started extracting time from PM-JPL input table")
|
|
179
|
+
time_UTC = pd.to_datetime(input_df.time_UTC).tolist()
|
|
180
|
+
logger.info("completed extracting time from PM-JPL input table")
|
|
181
|
+
|
|
182
|
+
# --- Pass time and geometry to the model ---
|
|
183
|
+
results = PMJPL(
|
|
184
|
+
geometry=geometry,
|
|
185
|
+
NDVI=NDVI,
|
|
186
|
+
Ta_C=Ta_C,
|
|
187
|
+
ST_C=ST_C,
|
|
188
|
+
emissivity=emissivity,
|
|
189
|
+
RH=RH,
|
|
190
|
+
Rn_Wm2=Rn_Wm2,
|
|
191
|
+
Rn_daylight_Wm2=Rn_daylight_Wm2,
|
|
192
|
+
SWin_Wm2=SWin_Wm2,
|
|
193
|
+
albedo=albedo,
|
|
194
|
+
Tmin_C=Tmin_C,
|
|
195
|
+
IGBP=IGBP,
|
|
196
|
+
elevation_km=elevation_km,
|
|
197
|
+
time_UTC=time_UTC,
|
|
198
|
+
upscale_to_daylight=upscale_to_daylight,
|
|
199
|
+
regenerate_net_radiation=regenerate_net_radiation
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
output_df = input_df.copy()
|
|
203
|
+
for key, value in results.items():
|
|
204
|
+
output_df[key] = value
|
|
205
|
+
|
|
206
|
+
logger.info("PM-JPL table processing complete")
|
|
207
|
+
|
|
208
|
+
return output_df
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
from sun_angles import SHA_deg_from_DOY_lat, daylight_from_SHA, sunrise_from_SHA
|
|
4
|
+
from verma_net_radiation import daylight_Rn_integration_verma
|
|
5
|
+
from daylight_evapotranspiration import daylight_ET_from_daily_LE
|
|
6
|
+
|
|
7
|
+
from meteorology_conversion import celcius_to_kelvin
|
|
8
|
+
|
|
9
|
+
def process_daily_ET_table(input_df: pd.DataFrame) -> pd.DataFrame:
|
|
10
|
+
hour_of_day = input_df.hour_of_day
|
|
11
|
+
DOY = input_df.doy
|
|
12
|
+
lat = input_df.lat
|
|
13
|
+
LE = input_df.LE
|
|
14
|
+
Rn = input_df.Rn
|
|
15
|
+
EF = LE / Rn
|
|
16
|
+
|
|
17
|
+
SHA_deg = SHA_deg_from_DOY_lat(DOY=DOY, latitude=lat)
|
|
18
|
+
sunrise_hour = sunrise_from_SHA(SHA_deg)
|
|
19
|
+
daylight_hours = daylight_from_SHA(SHA_deg)
|
|
20
|
+
|
|
21
|
+
Rn_daylight = daylight_Rn_integration_verma(
|
|
22
|
+
Rn=Rn,
|
|
23
|
+
hour_of_day=hour_of_day,
|
|
24
|
+
DOY=DOY,
|
|
25
|
+
lat=lat,
|
|
26
|
+
sunrise_hour=sunrise_hour,
|
|
27
|
+
daylight_hours=daylight_hours
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
LE_daylight = EF * Rn_daylight
|
|
31
|
+
ET = daylight_ET_from_daily_LE(LE_daylight, daylight_hours)
|
|
32
|
+
|
|
33
|
+
output_df = input_df.copy()
|
|
34
|
+
output_df["EF"] = EF
|
|
35
|
+
output_df["sunrise_hour"] = sunrise_hour
|
|
36
|
+
output_df["daylight_hours"] = daylight_hours
|
|
37
|
+
output_df["Rn_daylight"] = Rn_daylight
|
|
38
|
+
output_df["ET"] = ET
|
|
39
|
+
|
|
40
|
+
return output_df
|
PMJPL/transpiration.py
CHANGED
|
@@ -8,8 +8,8 @@ from .constants import GAMMA_PA
|
|
|
8
8
|
def calculate_transpiration(
|
|
9
9
|
delta_Pa: Union[Raster, np.ndarray],
|
|
10
10
|
Ac: Union[Raster, np.ndarray],
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
rho_kgm3: Union[Raster, np.ndarray],
|
|
12
|
+
Cp_Jkg: Union[Raster, np.ndarray],
|
|
13
13
|
VPD_Pa: Union[Raster, np.ndarray],
|
|
14
14
|
FVC: Union[Raster, np.ndarray],
|
|
15
15
|
ra: Union[Raster, np.ndarray],
|
|
@@ -34,11 +34,11 @@ def calculate_transpiration(
|
|
|
34
34
|
Returns:
|
|
35
35
|
Union[Raster, np.ndarray]: transpiration (LEc) in mm/day.
|
|
36
36
|
"""
|
|
37
|
-
numerator = (delta_Pa * Ac + (
|
|
37
|
+
numerator = (delta_Pa * Ac + (rho_kgm3 * Cp_Jkg * FVC * VPD_Pa / ra)) * (1.0 - fwet)
|
|
38
38
|
denominator = delta_Pa + gamma_Pa * (1.0 + (rs / ra))
|
|
39
39
|
LEc = numerator / denominator
|
|
40
40
|
|
|
41
41
|
# fill transpiration with zero
|
|
42
|
-
LEc = rt.where(
|
|
42
|
+
LEc = rt.where(np.isnan(LEc), 0.0, LEc)
|
|
43
43
|
|
|
44
44
|
return LEc
|
PMJPL/verify.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
def verify() -> bool:
|
|
2
|
+
"""
|
|
3
|
+
Verifies the correctness of the PT-JPL 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_PMJPL_inputs import load_ECOv002_calval_PMJPL_inputs
|
|
18
|
+
from .process_PMJPL_table import process_PMJPL_table
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
# Load input and output tables
|
|
22
|
+
input_df = load_ECOv002_calval_PMJPL_inputs()
|
|
23
|
+
module_dir = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
output_file_path = os.path.join(module_dir, "ECOv002-cal-val-PM-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_PMJPL_table(input_df)
|
|
29
|
+
|
|
30
|
+
# Columns to compare (model outputs)
|
|
31
|
+
output_columns = [
|
|
32
|
+
"G_Wm2",
|
|
33
|
+
# "Rn_soil_Wm2",
|
|
34
|
+
# "LE_soil_Wm2",
|
|
35
|
+
# "Rn_canopy_Wm2",
|
|
36
|
+
# "PET_Wm2",
|
|
37
|
+
# "LE_canopy_Wm2",
|
|
38
|
+
# "LE_interception_Wm2",
|
|
39
|
+
"LE_Wm2"
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
# Compare each output column and collect mismatches
|
|
43
|
+
mismatches = []
|
|
44
|
+
for col in output_columns:
|
|
45
|
+
if col not in model_df or col not in output_df:
|
|
46
|
+
mismatches.append((col, 'missing_column', None))
|
|
47
|
+
continue
|
|
48
|
+
model_vals = model_df[col].values
|
|
49
|
+
ref_vals = output_df[col].values
|
|
50
|
+
# Use numpy allclose for floating point comparison
|
|
51
|
+
if not np.allclose(model_vals, ref_vals, rtol=1e-5, atol=1e-8, equal_nan=True):
|
|
52
|
+
# Find indices where values differ
|
|
53
|
+
diffs = np.abs(model_vals - ref_vals)
|
|
54
|
+
max_diff = np.nanmax(diffs)
|
|
55
|
+
idxs = np.where(~np.isclose(model_vals, ref_vals, rtol=1e-5, atol=1e-8, equal_nan=True))[0]
|
|
56
|
+
mismatch_info = {
|
|
57
|
+
'indices': idxs.tolist(),
|
|
58
|
+
'model_values': model_vals[idxs].tolist(),
|
|
59
|
+
'ref_values': ref_vals[idxs].tolist(),
|
|
60
|
+
'diffs': diffs[idxs].tolist(),
|
|
61
|
+
'max_diff': float(max_diff)
|
|
62
|
+
}
|
|
63
|
+
mismatches.append((col, 'value_mismatch', mismatch_info))
|
|
64
|
+
if mismatches:
|
|
65
|
+
print("Verification failed. Details:")
|
|
66
|
+
for col, reason, info in mismatches:
|
|
67
|
+
if reason == 'missing_column':
|
|
68
|
+
print(f" Missing column: {col}")
|
|
69
|
+
elif reason == 'value_mismatch':
|
|
70
|
+
print(f" Mismatch in column: {col}")
|
|
71
|
+
print(f" Max difference: {info['max_diff']}")
|
|
72
|
+
print(f" Indices off: {info['indices']}")
|
|
73
|
+
print(f" Model values: {info['model_values']}")
|
|
74
|
+
print(f" Reference values: {info['ref_values']}")
|
|
75
|
+
print(f" Differences: {info['diffs']}")
|
|
76
|
+
return False
|
|
77
|
+
return True
|
PMJPL/version.py
ADDED
PMJPL/wet_canopy_resistance.py
CHANGED
|
@@ -18,4 +18,5 @@ def calculate_wet_canopy_resistance(
|
|
|
18
18
|
:param fwet: relative surface wetness
|
|
19
19
|
:return: wet canopy resistance
|
|
20
20
|
"""
|
|
21
|
+
# print(f"conductance: {conductance.shape}, LAI: {LAI.shape}, fwet: {fwet.shape}")
|
|
21
22
|
return rt.clip(1.0 / rt.clip(conductance * LAI * fwet, 1.0 / max_resistance, None), min_resistance, max_resistance)
|
PMJPL/wet_soil_evaporation.py
CHANGED
|
@@ -7,10 +7,10 @@ from .constants import GAMMA_PA
|
|
|
7
7
|
def calculate_wet_soil_evaporation(
|
|
8
8
|
delta_Pa: Union[Raster, np.ndarray],
|
|
9
9
|
Asoil: Union[Raster, np.ndarray],
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
rho_kgm3: Union[Raster, np.ndarray],
|
|
11
|
+
Cp_Jkg: Union[Raster, np.ndarray],
|
|
12
12
|
FVC: Union[Raster, np.ndarray],
|
|
13
|
-
|
|
13
|
+
VPD_Pa: Union[Raster, np.ndarray],
|
|
14
14
|
ras: Union[Raster, np.ndarray],
|
|
15
15
|
fwet: Union[Raster, np.ndarray],
|
|
16
16
|
rtot: Union[Raster, np.ndarray],
|
|
@@ -29,7 +29,7 @@ def calculate_wet_soil_evaporation(
|
|
|
29
29
|
:param gamme: gamma constant (default: GAMMA)
|
|
30
30
|
:return: wet soil evaporation in watts per square meter
|
|
31
31
|
"""
|
|
32
|
-
numerator = (delta_Pa * Asoil +
|
|
32
|
+
numerator = (delta_Pa * Asoil + rho_kgm3 * Cp_Jkg * (1.0 - FVC) * VPD_Pa / ras) * fwet
|
|
33
33
|
denominator = delta_Pa + gamma_Pa * rtot / ras
|
|
34
34
|
LE_soil_wet = numerator / denominator
|
|
35
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PM-JPL
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: JPL implementation of the MOD16 evapotranspiration algorithm for high resolution instantaneous remote sensing imagery
|
|
5
5
|
Author-email: Gregory Halverson <gregory.h.halverson@jpl.nasa.gov>, Qiaozhen Mu <qiaozhen@ntsg.umt.edu>, Maosheng Zhao <zhao@ntsg.umt.edu>, "Steven W. Running" <swr@ntsg.umt.edu>, "Claire S. Villanueva-Weeks" <claire.s.villanueva-weeks@jpl.gov>
|
|
6
6
|
Project-URL: Homepage, https://github.com/JPL-Evapotranspiration-Algorithms/PM-JPL
|
|
@@ -9,28 +9,25 @@ Classifier: Operating System :: OS Independent
|
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
12
|
+
Requires-Dist: carlson-fractional-vegetation-cover
|
|
13
|
+
Requires-Dist: carlson-leaf-area-index
|
|
14
|
+
Requires-Dist: check-distribution
|
|
15
|
+
Requires-Dist: daylight-evapotranspiration>=1.2.1
|
|
16
|
+
Requires-Dist: ECOv002-CMR>=1.0.5
|
|
17
|
+
Requires-Dist: ECOv002-granules>=1.0.3
|
|
18
|
+
Requires-Dist: ECOv003-granules
|
|
19
|
+
Requires-Dist: GEOS5FP>=1.1.1
|
|
20
|
+
Requires-Dist: MCD12C1_2019_v006
|
|
21
|
+
Requires-Dist: meteorology-conversion
|
|
22
|
+
Requires-Dist: NASADEM
|
|
18
23
|
Requires-Dist: numpy
|
|
19
24
|
Requires-Dist: pandas
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
Requires-Dist: requests
|
|
27
|
-
Requires-Dist: scikit-image
|
|
28
|
-
Requires-Dist: scipy
|
|
29
|
-
Requires-Dist: shapely
|
|
30
|
-
Requires-Dist: six
|
|
31
|
-
Requires-Dist: sun-angles
|
|
32
|
-
Requires-Dist: tensorflow
|
|
33
|
-
Requires-Dist: urllib3
|
|
25
|
+
Requires-Dist: priestley-taylor
|
|
26
|
+
Requires-Dist: PTJPL>=1.5.1
|
|
27
|
+
Requires-Dist: rasters>=1.11.0
|
|
28
|
+
Requires-Dist: SEBAL-soil-heat-flux
|
|
29
|
+
Requires-Dist: sun-angles>=1.3.0
|
|
30
|
+
Requires-Dist: verma-net-radiation>=1.8.0
|
|
34
31
|
Provides-Extra: dev
|
|
35
32
|
Requires-Dist: build; extra == "dev"
|
|
36
33
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
PMJPL/ECOv002-cal-val-PM-JPL-inputs.csv,sha256=8U5x7QZI6qklpCNdEWuAt19RS2BtYjWDDgG1D49CREY,1060696
|
|
2
|
+
PMJPL/ECOv002-cal-val-PM-JPL-outputs.csv,sha256=i2nEvXaZqRX7IlXyd2BkDK1clrVcTcDGqV1QoMG30qI,1623539
|
|
3
|
+
PMJPL/ECOv002-static-tower-PM-JPL-inputs.csv,sha256=2ckj9kpAS8QSLets0jegf1G8Z8mSYjYT80k12kDQZlc,14915
|
|
4
|
+
PMJPL/ECOv002_calval_PMJPL_inputs.py,sha256=c0tqGN-oJWoQUGZq5s3EdllrgkOQxMOBwdit_5a8s3U,608
|
|
5
|
+
PMJPL/ECOv002_static_tower_PMJPL_inputs.py,sha256=nvMOIqZtJPKs--YHH4IyxAjfOGVv4BH8EriKMJolFek,618
|
|
6
|
+
PMJPL/PMJPL.py,sha256=8YXZpOT181YYWnSal0-yQliKkJev1OZKR2Qdx_vK2t0,3554
|
|
7
|
+
PMJPL/PMJPL_parameter_from_IGBP.py,sha256=3mzk957afEp0jBKWxpSDhzm33ZGkG_5RCyijlpQTlwk,2537
|
|
8
|
+
PMJPL/VPD_factor.py,sha256=ezHMnnMPGNBt6ZTyiwsTEyMr8fwre6JDsHQea68P7T0,970
|
|
9
|
+
PMJPL/__init__.py,sha256=jke4us8XYaXqU4fL97WYb7Ot1kdD4hV_2NyqFchUh6Q,91
|
|
10
|
+
PMJPL/calculate_gamma.py,sha256=1rrIe9gz3VvkzaLBURSQDke3drIHnqoyBOdNfyvxhaM,5900
|
|
11
|
+
PMJPL/canopy_aerodynamic_resistance.py,sha256=eU5x8riEROyynsMiWDHR_sDCeDexsWDlhuU69tUvC2s,8490
|
|
12
|
+
PMJPL/canopy_conductance.py,sha256=ow7Egm_2wu2Zu5WxkESaUABf7UQA0ldmyPe5Wf4FQ2Y,4814
|
|
13
|
+
PMJPL/closed_minimum_temperature.py,sha256=kMnu9vXK0qDDfyWk5-gdbEkLWzcn28Hz_NplMkyeiUE,461
|
|
14
|
+
PMJPL/closed_vapor_pressure_deficit.py,sha256=v-bihgWfvaklR6f1jkb7XT4hIDk04O2zC08b8QmVJAQ,463
|
|
15
|
+
PMJPL/constants.py,sha256=kFu_YZDpDHvKTOuxICaUV-VCf-B8S_2ZQ79gx2rjzjg,890
|
|
16
|
+
PMJPL/correctance_factor.py,sha256=bVsu23oeTJoeMe-SIsH6cdFyATsLhgwvQUz79yf6kcQ,3181
|
|
17
|
+
PMJPL/generate_PMJPL_inputs.py,sha256=YxConMMvqC1HQdg04fOZIqGLp_-CXzR4SRhiTSm4S8o,13523
|
|
18
|
+
PMJPL/interception.py,sha256=2kDqMefH_kzMO9mLVY9BPFSoJE4K0pkQCXwrSi2CR9c,1605
|
|
19
|
+
PMJPL/leaf_conductance_to_evaporated_water.py,sha256=04_w2Davv3LXqaeSOd_fwRp0wuG4rrjOXBGY4bwvw9U,467
|
|
20
|
+
PMJPL/leaf_conductance_to_sensible_heat.py,sha256=nNQliqi0bMRFqXXva4s05nJnhdhgv1PGLVHlwPK3IuA,3602
|
|
21
|
+
PMJPL/maximum_boundary_layer_resistance.py,sha256=fy2r8B3Ucs9mqI8JYLjS6BYL03iSAIPHQc-ZCd4UUlA,464
|
|
22
|
+
PMJPL/minimum_boundary_layer_resistance.py,sha256=sYbLK_nPrnSD8LMMr_phULaAoBJk-5Nx1AT_D5q70ww,464
|
|
23
|
+
PMJPL/minimum_temperature_factor.py,sha256=bHET3MLD9MlGwd8d5l0DR-4AY1emwJ1Eoqv7FxmZCWk,1585
|
|
24
|
+
PMJPL/mod16.csv,sha256=oq-4b7dCqIrxTX2RcHjZMIZuqE8qThkW3bykqNpw18c,849
|
|
25
|
+
PMJPL/model.py,sha256=RwRd8DHIEM4H33fopO9T0XLVVv06ADwoH_TZjlb2894,26866
|
|
26
|
+
PMJPL/open_minimum_temperature.py,sha256=m03AU4REmcPEcWDyixSkNPYGgG_YAfGeYAg6LwPXjZY,457
|
|
27
|
+
PMJPL/open_vapor_pressure_deficit.py,sha256=d5Q1EqOCOZE9QKDU0k1trSujHytz_QeRehHHjwhUI5I,459
|
|
28
|
+
PMJPL/potential_soil_evaporation.py,sha256=O8bHGM4ARqSXJDHu6IWneSEy5qScenotGR3uawijGWg,1829
|
|
29
|
+
PMJPL/potential_stomatal_conductance.py,sha256=MlK9_GATJ_yMr2YEqp_8wz6y4chWufrUEO9xrRaQqGs,456
|
|
30
|
+
PMJPL/process_PMJPL_table.py,sha256=sGM3Q45p94DwLgo9RIKpt2XzZ6ElLoluibGtXzpfCE0,8266
|
|
31
|
+
PMJPL/process_daily_ET_table.py,sha256=xwyWvsSNSswNDX-CYljJaHsDz5ktWPsDCvjuxQ3tJ2w,1202
|
|
32
|
+
PMJPL/soil_moisture_constraint.py,sha256=URUGsE1_p2P1dVQ8qKRcKb93hahYWtAmYqoassRu-PI,667
|
|
33
|
+
PMJPL/transpiration.py,sha256=lhRm2A0yxrlJxDw2he-k3PhE7G-sBsVvAMKo5fmh7wU,1998
|
|
34
|
+
PMJPL/verify.py,sha256=NJ4YFWnil-1WnVt7OmNoDDaqVyn6ghzWGGFyIqN_bss,3216
|
|
35
|
+
PMJPL/version.py,sha256=7e7k9a2pQYZR4Fyhl6UDY8NQ1lFi5ChR0Zk9Mpl45BU,115
|
|
36
|
+
PMJPL/wet_canopy_resistance.py,sha256=Eape5EBJuoP-IRvD0SWHJKCJWhzm03vcG72OZVYqt_Q,879
|
|
37
|
+
PMJPL/wet_soil_evaporation.py,sha256=mw9JG1oXfVdSWnVRLP2SPd53mSRK96b8k3E1zf0ci7A,1440
|
|
38
|
+
pm_jpl-1.7.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
39
|
+
pm_jpl-1.7.0.dist-info/METADATA,sha256=EZvJWQw-dmMYhm36Vkk8qkt8OEmspTrzC7O3ZXFxiHo,4399
|
|
40
|
+
pm_jpl-1.7.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
+
pm_jpl-1.7.0.dist-info/top_level.txt,sha256=YaAFwdYHUxfUW08hiuFquUEdDGrsYn1duuMMjJKh0Ws,6
|
|
42
|
+
pm_jpl-1.7.0.dist-info/RECORD,,
|
PMJPL/MCD12C1/MCD12C1.py
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from os.path import join, abspath, dirname
|
|
2
|
-
import numpy as np
|
|
3
|
-
import rasters as rt
|
|
4
|
-
from rasters import Raster, RasterGeometry
|
|
5
|
-
|
|
6
|
-
def load_MCD12C1_IGBP(geometry: RasterGeometry = None) -> Raster:
|
|
7
|
-
filename = join(abspath(dirname(__file__)), "MCD12C1.A2019001.006.2020220162300.tif")
|
|
8
|
-
image = rt.Raster.open(filename, geometry=geometry, resampling="nearest")
|
|
9
|
-
|
|
10
|
-
return image
|
PMJPL/MCD12C1/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .MCD12C1 import *
|
PMJPL/SEBAL/SEBAL.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pandas as pd
|
|
3
|
-
import rasters as rt
|
|
4
|
-
|
|
5
|
-
def calculate_soil_heat_flux(Rn: np.ndarray, ST_C: np.ndarray, NDVI: np.ndarray, albedo: np.ndarray) -> np.ndarray:
|
|
6
|
-
"""
|
|
7
|
-
This function calculates the soil heat flux (G) in the Surface Energy Balance Algorithm for Land (SEBAL) model.
|
|
8
|
-
The formula used in the function is a simplification of the more complex relationship between these variables in the energy balance at the surface.
|
|
9
|
-
|
|
10
|
-
Parameters:
|
|
11
|
-
Rn (np.ndarray): Net radiation at the surface.
|
|
12
|
-
ST_C (np.ndarray): Surface temperature in Celsius.
|
|
13
|
-
NDVI (np.ndarray): Normalized Difference Vegetation Index, indicating the presence and condition of vegetation.
|
|
14
|
-
albedo (np.ndarray): Measure of the diffuse reflection of solar radiation.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
np.ndarray: The soil heat flux (G), a key component in the energy balance.
|
|
18
|
-
|
|
19
|
-
Reference:
|
|
20
|
-
"Evapotranspiration Estimation Based on Remote Sensing and the SEBAL Model in the Bosten Lake Basin of China" [^1^][1]
|
|
21
|
-
"""
|
|
22
|
-
# Empirical coefficients used in the calculation
|
|
23
|
-
coeff1 = 0.0038
|
|
24
|
-
coeff2 = 0.0074
|
|
25
|
-
|
|
26
|
-
# Vegetation cover correction factor
|
|
27
|
-
NDVI_correction = 1 - 0.98 * NDVI ** 4
|
|
28
|
-
|
|
29
|
-
# Calculation of the soil heat flux (G)
|
|
30
|
-
G = Rn * ST_C * (coeff1 + coeff2 * albedo) * NDVI_correction
|
|
31
|
-
|
|
32
|
-
G = rt.clip(G, 0, None)
|
|
33
|
-
|
|
34
|
-
return G
|
|
35
|
-
|
|
36
|
-
def process_SEBAL_G_table(input_df: pd.DataFrame) -> pd.DataFrame:
|
|
37
|
-
Rn = input_df.Rn
|
|
38
|
-
ST_C = input_df.ST_C
|
|
39
|
-
NDVI = input_df.NDVI
|
|
40
|
-
albedo = input_df.albedo
|
|
41
|
-
G = calculate_soil_heat_flux(Rn=Rn, ST_C=ST_C, NDVI=NDVI, albedo=albedo)
|
|
42
|
-
output_df = input_df.copy()
|
|
43
|
-
output_df["G"] = G
|
|
44
|
-
|
|
45
|
-
return output_df
|
PMJPL/SEBAL/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .SEBAL import *
|
PMJPL/downscaling/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .downscaling import *
|