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.
Files changed (73) hide show
  1. BESS_JPL/BESS_JPL.py +54 -0
  2. BESS_JPL/C3_photosynthesis.py +165 -0
  3. BESS_JPL/C4_fraction.jpeg +0 -0
  4. BESS_JPL/C4_fraction.tif +0 -0
  5. BESS_JPL/C4_fraction.tif.aux.xml +11 -0
  6. BESS_JPL/C4_photosynthesis.py +133 -0
  7. BESS_JPL/ECOv002-cal-val-BESS-JPL-GEOS5FP-inputs.csv +1066 -0
  8. BESS_JPL/ECOv002-cal-val-BESS-JPL-inputs.csv +1066 -0
  9. BESS_JPL/ECOv002-cal-val-BESS-JPL-outputs.csv +1066 -0
  10. BESS_JPL/ECOv002-cal-val-FLiESANN-inputs.csv +1066 -0
  11. BESS_JPL/ECOv002-static-tower-BESS-JPL-inputs.csv +122 -0
  12. BESS_JPL/ECOv002_calval_BESS_inputs.py +30 -0
  13. BESS_JPL/ECOv002_static_tower_BESS_inputs.py +19 -0
  14. BESS_JPL/FVC_from_NDVI.py +22 -0
  15. BESS_JPL/LAI_from_NDVI.py +28 -0
  16. BESS_JPL/NDVI_maximum.jpeg +0 -0
  17. BESS_JPL/NDVI_maximum.tif +0 -0
  18. BESS_JPL/NDVI_minimum.jpeg +0 -0
  19. BESS_JPL/NDVI_minimum.tif +0 -0
  20. BESS_JPL/__init__.py +5 -0
  21. BESS_JPL/ball_berry_intercept_C3.jpeg +0 -0
  22. BESS_JPL/ball_berry_intercept_C3.tif +0 -0
  23. BESS_JPL/ball_berry_slope_C3.jpeg +0 -0
  24. BESS_JPL/ball_berry_slope_C3.tif +0 -0
  25. BESS_JPL/ball_berry_slope_C4.jpeg +0 -0
  26. BESS_JPL/ball_berry_slope_C4.tif +0 -0
  27. BESS_JPL/calculate_VCmax.py +90 -0
  28. BESS_JPL/calculate_bulk_aerodynamic_resistance.py +119 -0
  29. BESS_JPL/calculate_friction_velocity.py +111 -0
  30. BESS_JPL/canopy_energy_balance.py +110 -0
  31. BESS_JPL/canopy_longwave_radiation.py +117 -0
  32. BESS_JPL/canopy_shortwave_radiation.py +276 -0
  33. BESS_JPL/carbon_uptake_efficiency.jpeg +0 -0
  34. BESS_JPL/carbon_uptake_efficiency.tif +0 -0
  35. BESS_JPL/carbon_water_fluxes.py +313 -0
  36. BESS_JPL/colors.py +33 -0
  37. BESS_JPL/constants.py +25 -0
  38. BESS_JPL/exceptions.py +3 -0
  39. BESS_JPL/generate_BESS_GEOS5FP_inputs.py +58 -0
  40. BESS_JPL/generate_BESS_inputs_table.py +186 -0
  41. BESS_JPL/generate_input_dataset.py +243 -0
  42. BESS_JPL/generate_output_dataset.py +26 -0
  43. BESS_JPL/interpolate_C3_C4.py +12 -0
  44. BESS_JPL/kn.jpeg +0 -0
  45. BESS_JPL/kn.tif +0 -0
  46. BESS_JPL/load_C4_fraction.py +20 -0
  47. BESS_JPL/load_NDVI_maximum.py +17 -0
  48. BESS_JPL/load_NDVI_minimum.py +17 -0
  49. BESS_JPL/load_ball_berry_intercept_C3.py +10 -0
  50. BESS_JPL/load_ball_berry_slope_C3.py +10 -0
  51. BESS_JPL/load_ball_berry_slope_C4.py +10 -0
  52. BESS_JPL/load_carbon_uptake_efficiency.py +10 -0
  53. BESS_JPL/load_kn.py +10 -0
  54. BESS_JPL/load_peakVCmax_C3.py +12 -0
  55. BESS_JPL/load_peakVCmax_C4.py +12 -0
  56. BESS_JPL/meteorology.py +429 -0
  57. BESS_JPL/model.py +594 -0
  58. BESS_JPL/peakVCmax_C3.jpeg +0 -0
  59. BESS_JPL/peakVCmax_C3.tif +0 -0
  60. BESS_JPL/peakVCmax_C4.jpeg +0 -0
  61. BESS_JPL/peakVCmax_C4.tif +0 -0
  62. BESS_JPL/process_BESS_table.py +365 -0
  63. BESS_JPL/process_paw_and_gao_LE.py +50 -0
  64. BESS_JPL/retrieve_BESS_JPL_GEOS5FP_inputs.py +257 -0
  65. BESS_JPL/retrieve_BESS_inputs.py +279 -0
  66. BESS_JPL/soil_energy_balance.py +35 -0
  67. BESS_JPL/verify.py +127 -0
  68. BESS_JPL/version.py +3 -0
  69. bess_jpl-1.26.0.dist-info/METADATA +102 -0
  70. bess_jpl-1.26.0.dist-info/RECORD +73 -0
  71. bess_jpl-1.26.0.dist-info/WHEEL +5 -0
  72. bess_jpl-1.26.0.dist-info/licenses/LICENSE +201 -0
  73. 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