BESS-JPL 1.26.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.
- BESS_JPL/BESS_JPL.py +54 -0
- BESS_JPL/C3_photosynthesis.py +165 -0
- BESS_JPL/C4_fraction.jpeg +0 -0
- BESS_JPL/C4_fraction.tif +0 -0
- BESS_JPL/C4_fraction.tif.aux.xml +11 -0
- BESS_JPL/C4_photosynthesis.py +133 -0
- BESS_JPL/ECOv002-cal-val-BESS-JPL-GEOS5FP-inputs.csv +1066 -0
- BESS_JPL/ECOv002-cal-val-BESS-JPL-inputs.csv +1066 -0
- BESS_JPL/ECOv002-cal-val-BESS-JPL-outputs.csv +1066 -0
- BESS_JPL/ECOv002-cal-val-FLiESANN-inputs.csv +1066 -0
- BESS_JPL/ECOv002-static-tower-BESS-JPL-inputs.csv +122 -0
- BESS_JPL/ECOv002_calval_BESS_inputs.py +30 -0
- BESS_JPL/ECOv002_static_tower_BESS_inputs.py +19 -0
- BESS_JPL/FVC_from_NDVI.py +22 -0
- BESS_JPL/LAI_from_NDVI.py +28 -0
- BESS_JPL/NDVI_maximum.jpeg +0 -0
- BESS_JPL/NDVI_maximum.tif +0 -0
- BESS_JPL/NDVI_minimum.jpeg +0 -0
- BESS_JPL/NDVI_minimum.tif +0 -0
- BESS_JPL/__init__.py +5 -0
- BESS_JPL/ball_berry_intercept_C3.jpeg +0 -0
- BESS_JPL/ball_berry_intercept_C3.tif +0 -0
- BESS_JPL/ball_berry_slope_C3.jpeg +0 -0
- BESS_JPL/ball_berry_slope_C3.tif +0 -0
- BESS_JPL/ball_berry_slope_C4.jpeg +0 -0
- BESS_JPL/ball_berry_slope_C4.tif +0 -0
- BESS_JPL/calculate_VCmax.py +90 -0
- BESS_JPL/calculate_bulk_aerodynamic_resistance.py +119 -0
- BESS_JPL/calculate_friction_velocity.py +111 -0
- BESS_JPL/canopy_energy_balance.py +110 -0
- BESS_JPL/canopy_longwave_radiation.py +117 -0
- BESS_JPL/canopy_shortwave_radiation.py +276 -0
- BESS_JPL/carbon_uptake_efficiency.jpeg +0 -0
- BESS_JPL/carbon_uptake_efficiency.tif +0 -0
- BESS_JPL/carbon_water_fluxes.py +313 -0
- BESS_JPL/colors.py +33 -0
- BESS_JPL/constants.py +25 -0
- BESS_JPL/exceptions.py +3 -0
- BESS_JPL/generate_BESS_GEOS5FP_inputs.py +58 -0
- BESS_JPL/generate_BESS_inputs_table.py +186 -0
- BESS_JPL/generate_input_dataset.py +243 -0
- BESS_JPL/generate_output_dataset.py +26 -0
- BESS_JPL/interpolate_C3_C4.py +12 -0
- BESS_JPL/kn.jpeg +0 -0
- BESS_JPL/kn.tif +0 -0
- BESS_JPL/load_C4_fraction.py +20 -0
- BESS_JPL/load_NDVI_maximum.py +17 -0
- BESS_JPL/load_NDVI_minimum.py +17 -0
- BESS_JPL/load_ball_berry_intercept_C3.py +10 -0
- BESS_JPL/load_ball_berry_slope_C3.py +10 -0
- BESS_JPL/load_ball_berry_slope_C4.py +10 -0
- BESS_JPL/load_carbon_uptake_efficiency.py +10 -0
- BESS_JPL/load_kn.py +10 -0
- BESS_JPL/load_peakVCmax_C3.py +12 -0
- BESS_JPL/load_peakVCmax_C4.py +12 -0
- BESS_JPL/meteorology.py +429 -0
- BESS_JPL/model.py +594 -0
- BESS_JPL/peakVCmax_C3.jpeg +0 -0
- BESS_JPL/peakVCmax_C3.tif +0 -0
- BESS_JPL/peakVCmax_C4.jpeg +0 -0
- BESS_JPL/peakVCmax_C4.tif +0 -0
- BESS_JPL/process_BESS_table.py +365 -0
- BESS_JPL/process_paw_and_gao_LE.py +50 -0
- BESS_JPL/retrieve_BESS_JPL_GEOS5FP_inputs.py +257 -0
- BESS_JPL/retrieve_BESS_inputs.py +279 -0
- BESS_JPL/soil_energy_balance.py +35 -0
- BESS_JPL/verify.py +127 -0
- BESS_JPL/version.py +3 -0
- bess_jpl-1.26.0.dist-info/METADATA +102 -0
- bess_jpl-1.26.0.dist-info/RECORD +73 -0
- bess_jpl-1.26.0.dist-info/WHEEL +5 -0
- bess_jpl-1.26.0.dist-info/licenses/LICENSE +201 -0
- bess_jpl-1.26.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import rasters as rt
|
|
6
|
+
from dateutil import parser
|
|
7
|
+
from pandas import DataFrame
|
|
8
|
+
|
|
9
|
+
# Import functions for calculating solar time
|
|
10
|
+
from solar_apparent_time import calculate_solar_day_of_year, calculate_solar_hour_of_day
|
|
11
|
+
from geopandas import GeoSeries
|
|
12
|
+
from shapely.geometry import Point as ShapelyPoint
|
|
13
|
+
|
|
14
|
+
from GEOS5FP import GEOS5FP
|
|
15
|
+
|
|
16
|
+
from .constants import *
|
|
17
|
+
from .model import BESS_JPL
|
|
18
|
+
from .retrieve_BESS_JPL_GEOS5FP_inputs import retrieve_BESS_JPL_GEOS5FP_inputs
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
def _is_notebook() -> bool:
|
|
23
|
+
"""Check if code is running in a Jupyter notebook environment."""
|
|
24
|
+
try:
|
|
25
|
+
from IPython import get_ipython
|
|
26
|
+
shell = get_ipython().__class__.__name__
|
|
27
|
+
if shell == 'ZMQInteractiveShell':
|
|
28
|
+
return True # Jupyter notebook or qtconsole
|
|
29
|
+
elif shell == 'TerminalInteractiveShell':
|
|
30
|
+
return False # Terminal running IPython
|
|
31
|
+
else:
|
|
32
|
+
return False # Other type (?)
|
|
33
|
+
except (ImportError, NameError):
|
|
34
|
+
return False # Probably standard Python interpreter
|
|
35
|
+
|
|
36
|
+
def process_BESS_table(
|
|
37
|
+
input_df: DataFrame,
|
|
38
|
+
GEOS5FP_connection: GEOS5FP = None,
|
|
39
|
+
C4_fraction_scale_factor: float = C4_FRACTION_SCALE_FACTOR,
|
|
40
|
+
verbose: bool = None,
|
|
41
|
+
offline_mode: bool = False) -> DataFrame:
|
|
42
|
+
# Set verbose default based on environment if not explicitly provided
|
|
43
|
+
if verbose is None:
|
|
44
|
+
verbose = not _is_notebook()
|
|
45
|
+
|
|
46
|
+
ST_C = np.array(input_df.ST_C).astype(np.float64)
|
|
47
|
+
NDVI = np.array(input_df.NDVI).astype(np.float64)
|
|
48
|
+
|
|
49
|
+
NDVI = np.where(NDVI > 0.06, NDVI, np.nan).astype(np.float64)
|
|
50
|
+
|
|
51
|
+
albedo = np.array(input_df.albedo).astype(np.float64)
|
|
52
|
+
|
|
53
|
+
if "PAR_albedo" in input_df:
|
|
54
|
+
PAR_albedo = np.array(input_df.PAR_albedo).astype(np.float64)
|
|
55
|
+
else:
|
|
56
|
+
PAR_albedo = None
|
|
57
|
+
|
|
58
|
+
if "NIR_albedo" in input_df:
|
|
59
|
+
NIR_albedo = np.array(input_df.NIR_albedo).astype(np.float64)
|
|
60
|
+
else:
|
|
61
|
+
NIR_albedo = None
|
|
62
|
+
|
|
63
|
+
if "Ta_C" in input_df:
|
|
64
|
+
Ta_C = np.array(input_df.Ta_C).astype(np.float64)
|
|
65
|
+
elif "Ta" in input_df:
|
|
66
|
+
Ta_C = np.array(input_df.Ta).astype(np.float64)
|
|
67
|
+
|
|
68
|
+
RH = np.array(input_df.RH).astype(np.float64)
|
|
69
|
+
|
|
70
|
+
if "elevation_m" in input_df:
|
|
71
|
+
elevation_m = np.array(input_df.elevation_m).astype(np.float64)
|
|
72
|
+
elevation_km = elevation_m / 1000
|
|
73
|
+
elif "elevation_km" in input_df:
|
|
74
|
+
elevation_km = np.array(input_df.elevation_km).astype(np.float64)
|
|
75
|
+
elevation_m = elevation_km * 1000
|
|
76
|
+
else:
|
|
77
|
+
elevation_km = None
|
|
78
|
+
elevation_m = None
|
|
79
|
+
|
|
80
|
+
if "NDVI_minimum" in input_df:
|
|
81
|
+
NDVI_minimum = np.array(input_df.NDVI_minimum).astype(np.float64)
|
|
82
|
+
else:
|
|
83
|
+
NDVI_minimum = None
|
|
84
|
+
|
|
85
|
+
if "NDVI_maximum" in input_df:
|
|
86
|
+
NDVI_maximum = np.array(input_df.NDVI_maximum).astype(np.float64).astype(np.float64)
|
|
87
|
+
else:
|
|
88
|
+
NDVI_maximum = None
|
|
89
|
+
|
|
90
|
+
if "C4_fraction" in input_df:
|
|
91
|
+
C4_fraction = np.array(input_df.C4_fraction).astype(np.float64)
|
|
92
|
+
else:
|
|
93
|
+
C4_fraction = None
|
|
94
|
+
|
|
95
|
+
if "carbon_uptake_efficiency" in input_df:
|
|
96
|
+
carbon_uptake_efficiency = np.array(input_df.carbon_uptake_efficiency).astype(np.float64)
|
|
97
|
+
else:
|
|
98
|
+
carbon_uptake_efficiency = None
|
|
99
|
+
|
|
100
|
+
if "kn" in input_df:
|
|
101
|
+
kn = np.array(input_df.kn).astype(np.float64)
|
|
102
|
+
else:
|
|
103
|
+
kn = None
|
|
104
|
+
|
|
105
|
+
if "peakVCmax_C3" in input_df:
|
|
106
|
+
peakVCmax_C3 = np.array(input_df.peakVCmax_C3).astype(np.float64)
|
|
107
|
+
else:
|
|
108
|
+
peakVCmax_C3 = None
|
|
109
|
+
|
|
110
|
+
if "peakVCmax_C4" in input_df:
|
|
111
|
+
peakVCmax_C4 = np.array(input_df.peakVCmax_C4).astype(np.float64)
|
|
112
|
+
else:
|
|
113
|
+
peakVCmax_C4 = None
|
|
114
|
+
|
|
115
|
+
if "ball_berry_slope_C3" in input_df:
|
|
116
|
+
ball_berry_slope_C3 = np.array(input_df.ball_berry_slope_C3).astype(np.float64)
|
|
117
|
+
else:
|
|
118
|
+
ball_berry_slope_C3 = None
|
|
119
|
+
|
|
120
|
+
if "ball_berry_slope_C4" in input_df:
|
|
121
|
+
ball_berry_slope_C4 = np.array(input_df.ball_berry_slope_C4).astype(np.float64)
|
|
122
|
+
else:
|
|
123
|
+
ball_berry_slope_C4 = None
|
|
124
|
+
|
|
125
|
+
if "ball_berry_intercept_C3" in input_df:
|
|
126
|
+
ball_berry_intercept_C3 = np.array(input_df.ball_berry_intercept_C3).astype(np.float64)
|
|
127
|
+
else:
|
|
128
|
+
ball_berry_intercept_C3 = None
|
|
129
|
+
|
|
130
|
+
if "KG_climate" in input_df:
|
|
131
|
+
KG_climate = np.array(input_df.KG_climate)
|
|
132
|
+
else:
|
|
133
|
+
KG_climate = None
|
|
134
|
+
|
|
135
|
+
if "CI" in input_df:
|
|
136
|
+
CI = np.array(input_df.CI).astype(np.float64)
|
|
137
|
+
else:
|
|
138
|
+
CI = None
|
|
139
|
+
|
|
140
|
+
if "canopy_height_meters" in input_df:
|
|
141
|
+
canopy_height_meters = np.array(input_df.canopy_height_meters).astype(np.float64)
|
|
142
|
+
else:
|
|
143
|
+
canopy_height_meters = None
|
|
144
|
+
|
|
145
|
+
if "COT" in input_df:
|
|
146
|
+
COT = np.array(input_df.COT).astype(np.float64)
|
|
147
|
+
else:
|
|
148
|
+
COT = None
|
|
149
|
+
|
|
150
|
+
if "AOT" in input_df:
|
|
151
|
+
AOT = np.array(input_df.AOT).astype(np.float64)
|
|
152
|
+
else:
|
|
153
|
+
AOT = None
|
|
154
|
+
|
|
155
|
+
if "Ca" in input_df:
|
|
156
|
+
Ca = np.array(input_df.Ca).astype(np.float64)
|
|
157
|
+
else:
|
|
158
|
+
# Default to 400 ppm when Ca is not provided
|
|
159
|
+
Ca = np.full_like(ST_C, 400.0, dtype=np.float64)
|
|
160
|
+
|
|
161
|
+
if "wind_speed_mps" in input_df:
|
|
162
|
+
wind_speed_mps = np.array(input_df.wind_speed_mps).astype(np.float64)
|
|
163
|
+
# Apply default wind speed of 7.4 m/s when wind speed is 0 or very low
|
|
164
|
+
# to avoid numerical instability in aerodynamic resistance calculations
|
|
165
|
+
# wind_speed_mps = np.where(wind_speed_mps < 0.1, 7.4, wind_speed_mps)
|
|
166
|
+
else:
|
|
167
|
+
wind_speed_mps = None
|
|
168
|
+
|
|
169
|
+
if "vapor_gccm" in input_df:
|
|
170
|
+
vapor_gccm = np.array(input_df.vapor_gccm).astype(np.float64)
|
|
171
|
+
else:
|
|
172
|
+
vapor_gccm = None
|
|
173
|
+
|
|
174
|
+
if "ozone_cm" in input_df:
|
|
175
|
+
ozone_cm = np.array(input_df.ozone_cm).astype(np.float64)
|
|
176
|
+
else:
|
|
177
|
+
ozone_cm = None
|
|
178
|
+
|
|
179
|
+
# Handle temperature defaults
|
|
180
|
+
if "canopy_temperature_C" in input_df:
|
|
181
|
+
canopy_temperature_C = np.array(input_df.canopy_temperature_C).astype(np.float64)
|
|
182
|
+
else:
|
|
183
|
+
# Default to surface temperature when canopy temperature is not provided
|
|
184
|
+
canopy_temperature_C = ST_C.copy()
|
|
185
|
+
|
|
186
|
+
if "soil_temperature_C" in input_df:
|
|
187
|
+
soil_temperature_C = np.array(input_df.soil_temperature_C).astype(np.float64)
|
|
188
|
+
else:
|
|
189
|
+
# Default to surface temperature when soil temperature is not provided
|
|
190
|
+
soil_temperature_C = ST_C.copy()
|
|
191
|
+
|
|
192
|
+
# --- Handle geometry and time columns ---
|
|
193
|
+
import pandas as pd
|
|
194
|
+
from rasters import MultiPoint, WGS84
|
|
195
|
+
# from shapely.geometry import Point
|
|
196
|
+
from rasters import Point
|
|
197
|
+
|
|
198
|
+
def ensure_geometry(df):
|
|
199
|
+
if "geometry" in df:
|
|
200
|
+
if isinstance(df.geometry.iloc[0], str):
|
|
201
|
+
def parse_geom(s):
|
|
202
|
+
s = s.strip()
|
|
203
|
+
if s.startswith("POINT"):
|
|
204
|
+
coords = s.replace("POINT", "").replace("(", "").replace(")", "").strip().split()
|
|
205
|
+
return Point(float(coords[0]), float(coords[1]))
|
|
206
|
+
elif "," in s:
|
|
207
|
+
coords = [float(c) for c in s.split(",")]
|
|
208
|
+
return Point(coords[0], coords[1])
|
|
209
|
+
else:
|
|
210
|
+
coords = [float(c) for c in s.split()]
|
|
211
|
+
return Point(coords[0], coords[1])
|
|
212
|
+
df = df.copy()
|
|
213
|
+
df['geometry'] = df['geometry'].apply(parse_geom)
|
|
214
|
+
return df
|
|
215
|
+
|
|
216
|
+
input_df = ensure_geometry(input_df)
|
|
217
|
+
|
|
218
|
+
logger.info("started extracting geometry from BESS input table")
|
|
219
|
+
|
|
220
|
+
if "geometry" in input_df:
|
|
221
|
+
# Convert Point objects to a list of Points
|
|
222
|
+
if hasattr(input_df.geometry.iloc[0], "x") and hasattr(input_df.geometry.iloc[0], "y"):
|
|
223
|
+
geometry = [Point(pt.x, pt.y) for pt in input_df.geometry]
|
|
224
|
+
else:
|
|
225
|
+
geometry = [Point(pt) for pt in input_df.geometry]
|
|
226
|
+
elif "lat" in input_df and "lon" in input_df:
|
|
227
|
+
lat = np.array(input_df.lat).astype(np.float64)
|
|
228
|
+
lon = np.array(input_df.lon).astype(np.float64)
|
|
229
|
+
geometry = [Point(lon[i], lat[i]) for i in range(len(lat))]
|
|
230
|
+
else:
|
|
231
|
+
raise KeyError("Input DataFrame must contain either 'geometry' or both 'lat' and 'lon' columns.")
|
|
232
|
+
|
|
233
|
+
logger.info("completed extracting geometry from BESS input table")
|
|
234
|
+
|
|
235
|
+
logger.info("started extracting time from BESS input table")
|
|
236
|
+
time_UTC_list = pd.to_datetime(input_df.time_UTC).tolist()
|
|
237
|
+
|
|
238
|
+
# Calculate day_of_year and hour_of_day for each point
|
|
239
|
+
day_of_year_list = []
|
|
240
|
+
hour_of_day_list = []
|
|
241
|
+
|
|
242
|
+
for i, (time_utc, geom) in enumerate(zip(time_UTC_list, geometry)):
|
|
243
|
+
# Create a GeoSeries with a Shapely Point (lon, lat order)
|
|
244
|
+
shapely_point = ShapelyPoint(geom.x, geom.y)
|
|
245
|
+
geoseries = GeoSeries([shapely_point])
|
|
246
|
+
doy = calculate_solar_day_of_year(time_UTC=time_utc, geometry=geoseries)
|
|
247
|
+
hod = calculate_solar_hour_of_day(time_UTC=time_utc, geometry=geoseries)
|
|
248
|
+
# Extract scalar values if returned as arrays
|
|
249
|
+
doy_scalar = doy[0] if hasattr(doy, '__getitem__') else doy
|
|
250
|
+
hod_scalar = hod[0] if hasattr(hod, '__getitem__') else hod
|
|
251
|
+
day_of_year_list.append(doy_scalar)
|
|
252
|
+
hour_of_day_list.append(hod_scalar)
|
|
253
|
+
|
|
254
|
+
# Convert to numpy arrays (1D)
|
|
255
|
+
day_of_year = np.array(day_of_year_list)
|
|
256
|
+
hour_of_day = np.array(hour_of_day_list)
|
|
257
|
+
|
|
258
|
+
# Convert list of rasters.Point to MultiPoint for compatibility with FLiESANN and other functions
|
|
259
|
+
from rasters import MultiPoint
|
|
260
|
+
# Extract (x, y) tuples from rasters.Point objects
|
|
261
|
+
point_tuples = [(pt.x, pt.y) for pt in geometry]
|
|
262
|
+
geometry_multipoint = MultiPoint(point_tuples)
|
|
263
|
+
|
|
264
|
+
# Check if all times are the same
|
|
265
|
+
if len(set(time_UTC_list)) == 1:
|
|
266
|
+
# All timestamps are identical, use single datetime
|
|
267
|
+
time_UTC = time_UTC_list[0]
|
|
268
|
+
else:
|
|
269
|
+
# Different timestamps per point, keep as list
|
|
270
|
+
time_UTC = time_UTC_list
|
|
271
|
+
|
|
272
|
+
BESS_GEOS5FP_inputs = retrieve_BESS_JPL_GEOS5FP_inputs(
|
|
273
|
+
time_UTC=time_UTC,
|
|
274
|
+
geometry=geometry_multipoint,
|
|
275
|
+
albedo=albedo,
|
|
276
|
+
GEOS5FP_connection=GEOS5FP_connection,
|
|
277
|
+
Ta_C=Ta_C,
|
|
278
|
+
RH=RH,
|
|
279
|
+
COT=COT,
|
|
280
|
+
AOT=AOT,
|
|
281
|
+
vapor_gccm=vapor_gccm,
|
|
282
|
+
ozone_cm=ozone_cm,
|
|
283
|
+
PAR_albedo=PAR_albedo,
|
|
284
|
+
NIR_albedo=albedo,
|
|
285
|
+
Ca=Ca,
|
|
286
|
+
wind_speed_mps=wind_speed_mps,
|
|
287
|
+
verbose=verbose,
|
|
288
|
+
offline_mode=offline_mode
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
albedo = BESS_GEOS5FP_inputs['albedo']
|
|
292
|
+
Ta_C = BESS_GEOS5FP_inputs['Ta_C']
|
|
293
|
+
RH = BESS_GEOS5FP_inputs['RH']
|
|
294
|
+
COT = BESS_GEOS5FP_inputs['COT']
|
|
295
|
+
AOT = BESS_GEOS5FP_inputs['AOT']
|
|
296
|
+
vapor_gccm = BESS_GEOS5FP_inputs['vapor_gccm']
|
|
297
|
+
ozone_cm = BESS_GEOS5FP_inputs['ozone_cm']
|
|
298
|
+
PAR_albedo = BESS_GEOS5FP_inputs['PAR_albedo']
|
|
299
|
+
NIR_albedo = BESS_GEOS5FP_inputs['NIR_albedo']
|
|
300
|
+
Ca = BESS_GEOS5FP_inputs['Ca']
|
|
301
|
+
wind_speed_mps = BESS_GEOS5FP_inputs['wind_speed_mps']
|
|
302
|
+
|
|
303
|
+
logger.info("completed extracting time from BESS input table")
|
|
304
|
+
|
|
305
|
+
results = BESS_JPL(
|
|
306
|
+
geometry=geometry_multipoint,
|
|
307
|
+
time_UTC=time_UTC,
|
|
308
|
+
day_of_year=day_of_year,
|
|
309
|
+
hour_of_day=hour_of_day,
|
|
310
|
+
ST_C=ST_C,
|
|
311
|
+
albedo=albedo,
|
|
312
|
+
NDVI=NDVI,
|
|
313
|
+
Ta_C=Ta_C,
|
|
314
|
+
RH=RH,
|
|
315
|
+
elevation_m=elevation_m,
|
|
316
|
+
NDVI_minimum=NDVI_minimum,
|
|
317
|
+
NDVI_maximum=NDVI_maximum,
|
|
318
|
+
C4_fraction=C4_fraction,
|
|
319
|
+
carbon_uptake_efficiency=carbon_uptake_efficiency,
|
|
320
|
+
kn=kn,
|
|
321
|
+
peakVCmax_C3_μmolm2s1=peakVCmax_C3,
|
|
322
|
+
peakVCmax_C4_μmolm2s1=peakVCmax_C4,
|
|
323
|
+
ball_berry_slope_C3=ball_berry_slope_C3,
|
|
324
|
+
ball_berry_slope_C4=ball_berry_slope_C4,
|
|
325
|
+
ball_berry_intercept_C3=ball_berry_intercept_C3,
|
|
326
|
+
KG_climate=KG_climate,
|
|
327
|
+
CI=CI,
|
|
328
|
+
canopy_height_meters=canopy_height_meters,
|
|
329
|
+
COT=COT,
|
|
330
|
+
AOT=AOT,
|
|
331
|
+
Ca=Ca,
|
|
332
|
+
wind_speed_mps=wind_speed_mps,
|
|
333
|
+
vapor_gccm=vapor_gccm,
|
|
334
|
+
ozone_cm=ozone_cm,
|
|
335
|
+
PAR_albedo=albedo,
|
|
336
|
+
NIR_albedo=albedo,
|
|
337
|
+
canopy_temperature_C=canopy_temperature_C,
|
|
338
|
+
soil_temperature_C=soil_temperature_C,
|
|
339
|
+
C4_fraction_scale_factor=C4_fraction_scale_factor,
|
|
340
|
+
GEOS5FP_connection=GEOS5FP_connection,
|
|
341
|
+
offline_mode=offline_mode
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
output_df = input_df.copy()
|
|
345
|
+
|
|
346
|
+
# Collect new columns to avoid DataFrame fragmentation
|
|
347
|
+
new_columns = {}
|
|
348
|
+
for key, value in results.items():
|
|
349
|
+
# Skip non-array-like objects (e.g., MultiPoint geometry)
|
|
350
|
+
if hasattr(value, '__len__') and not isinstance(value, (str, MultiPoint)):
|
|
351
|
+
try:
|
|
352
|
+
new_columns[key] = value
|
|
353
|
+
except (ValueError, TypeError):
|
|
354
|
+
# Skip values that can't be assigned to DataFrame
|
|
355
|
+
logger.warning(f"Skipping assignment of key '{key}' to output DataFrame")
|
|
356
|
+
continue
|
|
357
|
+
elif isinstance(value, (int, float, np.number)):
|
|
358
|
+
# Handle scalar values
|
|
359
|
+
new_columns[key] = value
|
|
360
|
+
|
|
361
|
+
# Add all new columns at once using concat to avoid fragmentation
|
|
362
|
+
if new_columns:
|
|
363
|
+
output_df = pd.concat([output_df, pd.DataFrame(new_columns, index=output_df.index)], axis=1)
|
|
364
|
+
|
|
365
|
+
return output_df
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def process_paw_and_gao_LE(
|
|
4
|
+
Rn: np.ndarray, # net radiation (W m-2)
|
|
5
|
+
Ta_C: np.ndarray, # air temperature (C)
|
|
6
|
+
VPD_Pa: np.ndarray, # vapor pressure (Pa)
|
|
7
|
+
Cp: np.ndarray, # specific heat of air (J kg-1 K-1)
|
|
8
|
+
rhoa: np.ndarray, # air density (kg m-3)
|
|
9
|
+
gamma: np.ndarray, # psychrometric constant (Pa K-1)
|
|
10
|
+
Rc: np.ndarray,
|
|
11
|
+
rs: np.ndarray,
|
|
12
|
+
desTa: np.ndarray,
|
|
13
|
+
ddesTa: np.ndarray) -> np.ndarray:
|
|
14
|
+
"""
|
|
15
|
+
:param Rn: net radiation (W m-2)
|
|
16
|
+
:param Ta_C: air temperature (C)
|
|
17
|
+
:param VPD_Pa: vapor pressure (Pa)
|
|
18
|
+
:param Cp: specific heat of air (J kg-1 K-1)
|
|
19
|
+
:param rhoa: air density (kg m-3)
|
|
20
|
+
:param gamma: psychrometric constant (Pa K-1)
|
|
21
|
+
:param Rc:
|
|
22
|
+
:param rs:
|
|
23
|
+
:param desTa:
|
|
24
|
+
:param ddesTa:
|
|
25
|
+
:return: latent heat flux (W m-2)
|
|
26
|
+
"""
|
|
27
|
+
# To reduce redundant computation
|
|
28
|
+
rc = rs
|
|
29
|
+
ddesTa_Rc2 = ddesTa * Rc * Rc
|
|
30
|
+
gamma_Rc_rc = gamma * (Rc + rc)
|
|
31
|
+
rhoa_Cp_gamma_Rc_rc = rhoa * Cp * gamma_Rc_rc
|
|
32
|
+
|
|
33
|
+
# Solution (Paw and Gao 1988)
|
|
34
|
+
a = 1.0 / 2.0 * ddesTa_Rc2 / rhoa_Cp_gamma_Rc_rc # Eq. (10b)
|
|
35
|
+
b = -1.0 - Rc * desTa / gamma_Rc_rc - ddesTa_Rc2 * Rn / rhoa_Cp_gamma_Rc_rc # Eq. (10c)
|
|
36
|
+
c = rhoa * Cp / gamma_Rc_rc * VPD_Pa + desTa * Rc / gamma_Rc_rc * Rn + 1.0 / 2.0 * ddesTa_Rc2 / rhoa_Cp_gamma_Rc_rc * Rn * Rn # Eq. (10d) in Paw and Gao (1988)
|
|
37
|
+
|
|
38
|
+
# calculate latent heat flux
|
|
39
|
+
LE = (-b + np.sign(b) * np.sqrt(b * b - 4.0 * a * c)) / (2.0 * a) # Eq. (10a)
|
|
40
|
+
LE = np.real(LE)
|
|
41
|
+
|
|
42
|
+
# Constraints
|
|
43
|
+
# LE[LE > Rn] = Rn[LE > Rn]
|
|
44
|
+
LE = np.clip(LE, 0, Rn)
|
|
45
|
+
# LE[Rn < 0.0] = 0.0
|
|
46
|
+
# LE[LE < 0.0] = 0.0
|
|
47
|
+
# LE[Ta < 0.0] = 0.0 # Now using Celsius
|
|
48
|
+
LE = np.where(Ta_C < 0.0, 0, LE)
|
|
49
|
+
|
|
50
|
+
return LE
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from typing import Union, List
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from rasters import Raster, RasterGeometry
|
|
6
|
+
import rasters as rt
|
|
7
|
+
|
|
8
|
+
from check_distribution import check_distribution
|
|
9
|
+
|
|
10
|
+
from GEOS5FP import GEOS5FP
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
class MissingOfflineParameter(Exception):
|
|
16
|
+
"""Custom exception for missing offline parameters."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def retrieve_BESS_JPL_GEOS5FP_inputs(
|
|
20
|
+
time_UTC: Union[datetime, List[datetime]],
|
|
21
|
+
geometry: RasterGeometry,
|
|
22
|
+
albedo: Union[Raster, np.ndarray],
|
|
23
|
+
GEOS5FP_connection: GEOS5FP = None,
|
|
24
|
+
Ta_C: Union[Raster, np.ndarray] = None,
|
|
25
|
+
RH: Union[Raster, np.ndarray] = None,
|
|
26
|
+
COT: Union[Raster, np.ndarray] = None,
|
|
27
|
+
AOT: Union[Raster, np.ndarray] = None,
|
|
28
|
+
vapor_gccm: Union[Raster, np.ndarray] = None,
|
|
29
|
+
ozone_cm: Union[Raster, np.ndarray] = None,
|
|
30
|
+
PAR_albedo: Union[Raster, np.ndarray] = None,
|
|
31
|
+
NIR_albedo: Union[Raster, np.ndarray] = None,
|
|
32
|
+
Ca: Union[Raster, np.ndarray] = None,
|
|
33
|
+
wind_speed_mps: Union[Raster, np.ndarray] = None,
|
|
34
|
+
resampling: str = "cubic",
|
|
35
|
+
verbose: bool = False,
|
|
36
|
+
offline_mode: bool = False) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Retrieve GEOS-5 FP meteorological inputs for BESS-JPL model.
|
|
39
|
+
|
|
40
|
+
This function retrieves meteorological variables from GEOS-5 FP data products
|
|
41
|
+
when they are not provided as inputs. All missing variables are retrieved in
|
|
42
|
+
a single efficient `.query()` call to minimize network requests and improve
|
|
43
|
+
performance.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
time_UTC : Union[datetime, List[datetime]]
|
|
48
|
+
UTC time for data retrieval. Can be a single datetime or list of datetimes
|
|
49
|
+
for point-by-point queries.
|
|
50
|
+
geometry : RasterGeometry
|
|
51
|
+
Raster geometry for spatial operations
|
|
52
|
+
albedo : Union[Raster, np.ndarray]
|
|
53
|
+
Surface albedo [-], used for albedo calculations
|
|
54
|
+
GEOS5FP_connection : GEOS5FP, optional
|
|
55
|
+
Connection to GEOS-5 FP meteorological data. If None, creates new connection.
|
|
56
|
+
Ta_C : Union[Raster, np.ndarray], optional
|
|
57
|
+
Air temperature [°C]. Retrieved from GEOS-5 FP if None.
|
|
58
|
+
RH : Union[Raster, np.ndarray], optional
|
|
59
|
+
Relative humidity [fraction, 0-1]. Retrieved from GEOS-5 FP if None.
|
|
60
|
+
COT : Union[Raster, np.ndarray], optional
|
|
61
|
+
Cloud optical thickness [-]. Retrieved from GEOS-5 FP if None.
|
|
62
|
+
AOT : Union[Raster, np.ndarray], optional
|
|
63
|
+
Aerosol optical thickness [-]. Retrieved from GEOS-5 FP if None.
|
|
64
|
+
vapor_gccm : Union[Raster, np.ndarray], optional
|
|
65
|
+
Water vapor [g cm⁻²]. Retrieved from GEOS-5 FP if None.
|
|
66
|
+
ozone_cm : Union[Raster, np.ndarray], optional
|
|
67
|
+
Ozone column [cm]. Retrieved from GEOS-5 FP if None.
|
|
68
|
+
albedo_visible : Union[Raster, np.ndarray], optional
|
|
69
|
+
Surface albedo in visible wavelengths (400-700 nm) [-].
|
|
70
|
+
Calculated from GEOS-5 FP albedo products if None.
|
|
71
|
+
albedo_NIR : Union[Raster, np.ndarray], optional
|
|
72
|
+
Surface albedo in near-infrared wavelengths [-].
|
|
73
|
+
Calculated from GEOS-5 FP albedo products if None.
|
|
74
|
+
Ca : Union[Raster, np.ndarray], optional
|
|
75
|
+
Atmospheric CO₂ concentration [ppm]. Retrieved from GEOS-5 FP if None.
|
|
76
|
+
wind_speed_mps : Union[Raster, np.ndarray], optional
|
|
77
|
+
Wind speed [m s⁻¹]. Retrieved from GEOS-5 FP if None.
|
|
78
|
+
resampling : str, optional
|
|
79
|
+
Resampling method for data processing. Default is "cubic".
|
|
80
|
+
|
|
81
|
+
Returns
|
|
82
|
+
-------
|
|
83
|
+
dict
|
|
84
|
+
Dictionary containing all meteorological inputs:
|
|
85
|
+
- albedo : Surface albedo [-]
|
|
86
|
+
- Ta_C : Air temperature [°C]
|
|
87
|
+
- RH : Relative humidity [fraction, 0-1]
|
|
88
|
+
- COT : Cloud optical thickness [-]
|
|
89
|
+
- AOT : Aerosol optical thickness [-]
|
|
90
|
+
- vapor_gccm : Water vapor [g cm⁻²]
|
|
91
|
+
- ozone_cm : Ozone column [cm]
|
|
92
|
+
- PAR_albedo : Surface albedo in PAR wavelengths [-]
|
|
93
|
+
- NIR_albedo : Surface albedo in near-infrared wavelengths [-]
|
|
94
|
+
- Ca : Atmospheric CO₂ concentration [ppm]
|
|
95
|
+
- wind_speed_mps : Wind speed [m s⁻¹]
|
|
96
|
+
|
|
97
|
+
Notes
|
|
98
|
+
-----
|
|
99
|
+
The visible and NIR albedo are calculated by scaling the input albedo with
|
|
100
|
+
the ratio of GEOS-5 FP directional albedo products to total albedo.
|
|
101
|
+
|
|
102
|
+
All missing GEOS-5 FP variables are retrieved in a single `.query()` call
|
|
103
|
+
for optimal performance, reducing network overhead and improving efficiency.
|
|
104
|
+
|
|
105
|
+
When time_UTC is a list, it handles point-by-point queries where each point
|
|
106
|
+
may have a different datetime.
|
|
107
|
+
"""
|
|
108
|
+
# Create GEOS-5 FP connection if not provided
|
|
109
|
+
if GEOS5FP_connection is None:
|
|
110
|
+
GEOS5FP_connection = GEOS5FP()
|
|
111
|
+
|
|
112
|
+
# Initialize results dictionary
|
|
113
|
+
results = {}
|
|
114
|
+
|
|
115
|
+
# Add albedo (always required)
|
|
116
|
+
results["albedo"] = albedo
|
|
117
|
+
|
|
118
|
+
# Add provided inputs to results
|
|
119
|
+
if Ta_C is not None:
|
|
120
|
+
results["Ta_C"] = Ta_C
|
|
121
|
+
if RH is not None:
|
|
122
|
+
results["RH"] = RH
|
|
123
|
+
if COT is not None:
|
|
124
|
+
results["COT"] = COT
|
|
125
|
+
if AOT is not None:
|
|
126
|
+
results["AOT"] = AOT
|
|
127
|
+
if vapor_gccm is not None:
|
|
128
|
+
results["vapor_gccm"] = vapor_gccm
|
|
129
|
+
if ozone_cm is not None:
|
|
130
|
+
results["ozone_cm"] = ozone_cm
|
|
131
|
+
if PAR_albedo is not None:
|
|
132
|
+
results["PAR_albedo"] = PAR_albedo
|
|
133
|
+
if NIR_albedo is not None:
|
|
134
|
+
results["NIR_albedo"] = NIR_albedo
|
|
135
|
+
if Ca is not None:
|
|
136
|
+
results["Ca"] = Ca
|
|
137
|
+
if wind_speed_mps is not None:
|
|
138
|
+
results["wind_speed_mps"] = wind_speed_mps
|
|
139
|
+
|
|
140
|
+
# Determine which variables need to be retrieved from GEOS-5 FP
|
|
141
|
+
variables_to_retrieve = []
|
|
142
|
+
|
|
143
|
+
# Atmospheric parameters (from FLiESANN)
|
|
144
|
+
if COT is None:
|
|
145
|
+
variables_to_retrieve.append("COT")
|
|
146
|
+
if AOT is None:
|
|
147
|
+
variables_to_retrieve.append("AOT")
|
|
148
|
+
if vapor_gccm is None:
|
|
149
|
+
variables_to_retrieve.append("vapor_gccm")
|
|
150
|
+
if ozone_cm is None:
|
|
151
|
+
variables_to_retrieve.append("ozone_cm")
|
|
152
|
+
|
|
153
|
+
# Meteorological parameters
|
|
154
|
+
if Ta_C is None:
|
|
155
|
+
variables_to_retrieve.append("Ta_C")
|
|
156
|
+
if RH is None:
|
|
157
|
+
variables_to_retrieve.append("RH")
|
|
158
|
+
if Ca is None:
|
|
159
|
+
variables_to_retrieve.append("CO2SC")
|
|
160
|
+
if wind_speed_mps is None:
|
|
161
|
+
variables_to_retrieve.append("wind_speed_mps")
|
|
162
|
+
|
|
163
|
+
# Albedo products needed for visible/NIR calculations
|
|
164
|
+
if PAR_albedo is None or NIR_albedo is None:
|
|
165
|
+
variables_to_retrieve.append("ALBEDO")
|
|
166
|
+
if PAR_albedo is None:
|
|
167
|
+
variables_to_retrieve.append("ALBVISDR")
|
|
168
|
+
if NIR_albedo is None:
|
|
169
|
+
variables_to_retrieve.append("ALBNIRDR")
|
|
170
|
+
|
|
171
|
+
if len(variables_to_retrieve) == 0:
|
|
172
|
+
logger.info("All GEOS-5 FP inputs provided, no retrieval needed.")
|
|
173
|
+
else:
|
|
174
|
+
logger.info(f"Retrieving GEOS-5 FP variables: {', '.join(variables_to_retrieve)}")
|
|
175
|
+
|
|
176
|
+
if offline_mode and variables_to_retrieve:
|
|
177
|
+
raise MissingOfflineParameter(f"missing offline parameters for BESS: {', '.join(variables_to_retrieve)}")
|
|
178
|
+
|
|
179
|
+
# Retrieve all missing variables in a single query
|
|
180
|
+
if variables_to_retrieve:
|
|
181
|
+
logger.info(f"Retrieving GEOS-5 FP variables: {', '.join(variables_to_retrieve)}")
|
|
182
|
+
logger.info(f"Time UTC type: {type(time_UTC)}")
|
|
183
|
+
logger.info(f"Geometry type: {type(geometry)}")
|
|
184
|
+
if hasattr(time_UTC, '__len__'):
|
|
185
|
+
logger.info(f"Time UTC length: {len(time_UTC)}")
|
|
186
|
+
if hasattr(geometry, '__len__'):
|
|
187
|
+
logger.info(f"Geometry length: {len(geometry)}")
|
|
188
|
+
|
|
189
|
+
retrieved = GEOS5FP_connection.query(
|
|
190
|
+
target_variables=variables_to_retrieve,
|
|
191
|
+
time_UTC=time_UTC,
|
|
192
|
+
geometry=geometry,
|
|
193
|
+
resampling=resampling,
|
|
194
|
+
verbose=True # Enable verbose logging to see progress
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
logger.info(f"Retrieved keys: {list(retrieved.keys())}")
|
|
198
|
+
if "CO2SC" in retrieved:
|
|
199
|
+
logger.info(f"CO2SC in retrieved: {retrieved['CO2SC']}")
|
|
200
|
+
else:
|
|
201
|
+
logger.warning("CO2SC not in retrieved dictionary!")
|
|
202
|
+
|
|
203
|
+
# Extract retrieved values and add to results
|
|
204
|
+
if "COT" in retrieved:
|
|
205
|
+
results["COT"] = retrieved["COT"]
|
|
206
|
+
if "AOT" in retrieved:
|
|
207
|
+
results["AOT"] = retrieved["AOT"]
|
|
208
|
+
if "vapor_gccm" in retrieved:
|
|
209
|
+
results["vapor_gccm"] = retrieved["vapor_gccm"]
|
|
210
|
+
if "ozone_cm" in retrieved:
|
|
211
|
+
results["ozone_cm"] = retrieved["ozone_cm"]
|
|
212
|
+
if "Ta_C" in retrieved:
|
|
213
|
+
results["Ta_C"] = retrieved["Ta_C"]
|
|
214
|
+
check_distribution(results["Ta_C"], "Ta_C")
|
|
215
|
+
if "RH" in retrieved:
|
|
216
|
+
results["RH"] = retrieved["RH"]
|
|
217
|
+
check_distribution(results["RH"], "RH")
|
|
218
|
+
if "CO2SC" in retrieved:
|
|
219
|
+
results["Ca"] = retrieved["CO2SC"]
|
|
220
|
+
logger.info(f"Retrieved Ca from GEOS-5 FP: {results['Ca']}")
|
|
221
|
+
logger.info(f"Ca type: {type(results['Ca'])}")
|
|
222
|
+
if isinstance(results["Ca"], np.ndarray):
|
|
223
|
+
logger.info(f"Ca shape: {results['Ca'].shape}, dtype: {results['Ca'].dtype}")
|
|
224
|
+
logger.info(f"Ca has NaN: {np.any(np.isnan(results['Ca']))}")
|
|
225
|
+
check_distribution(results["Ca"], "Ca")
|
|
226
|
+
if "wind_speed_mps" in retrieved:
|
|
227
|
+
results["wind_speed_mps"] = rt.clip(retrieved["wind_speed_mps"], 0.1, None)
|
|
228
|
+
check_distribution(results["wind_speed_mps"], "wind_speed_mps")
|
|
229
|
+
|
|
230
|
+
# Calculate visible and NIR albedo from retrieved products and add to results
|
|
231
|
+
if "PAR_albedo" not in results:
|
|
232
|
+
albedo_NWP = retrieved["ALBEDO"]
|
|
233
|
+
RVIS_NWP = retrieved["ALBVISDR"]
|
|
234
|
+
results["PAR_albedo"] = rt.clip(albedo * (RVIS_NWP / albedo_NWP), 0, 1)
|
|
235
|
+
|
|
236
|
+
if "NIR_albedo" not in results:
|
|
237
|
+
albedo_NWP = retrieved["ALBEDO"]
|
|
238
|
+
RNIR_NWP = retrieved["ALBNIRDR"]
|
|
239
|
+
results["NIR_albedo"] = rt.clip(albedo * (RNIR_NWP / albedo_NWP), 0, 1)
|
|
240
|
+
|
|
241
|
+
# Apply default for Ca if not provided and not retrieved
|
|
242
|
+
if 'Ca' not in results:
|
|
243
|
+
logger.info("Ca not provided, using default value of 400 ppm")
|
|
244
|
+
# Create an array of 400.0 with the same shape as albedo
|
|
245
|
+
if isinstance(albedo, np.ndarray):
|
|
246
|
+
results['Ca'] = np.full_like(albedo, 400.0, dtype=np.float64)
|
|
247
|
+
else:
|
|
248
|
+
results['Ca'] = 400.0
|
|
249
|
+
|
|
250
|
+
# Verify all required keys are present
|
|
251
|
+
required_keys = ['albedo', 'Ta_C', 'RH', 'COT', 'AOT', 'vapor_gccm', 'ozone_cm',
|
|
252
|
+
'PAR_albedo', 'NIR_albedo', 'Ca', 'wind_speed_mps']
|
|
253
|
+
missing_keys = [key for key in required_keys if key not in results]
|
|
254
|
+
if missing_keys:
|
|
255
|
+
raise ValueError(f"Missing required keys in results: {missing_keys}")
|
|
256
|
+
|
|
257
|
+
return results
|