ECOv002-calval-tables 1.4.0__py3-none-any.whl → 1.5.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.
- ECOv002_calval_tables/ECOv002_calval_tables.py +5 -67
- ECOv002_calval_tables/ec_lib.py +736 -0
- ECOv002_calval_tables/error_funcs.py +376 -0
- ECOv002_calval_tables/load_tables.py +67 -0
- ECOv002_calval_tables/plot_funcs.py +710 -0
- ECOv002_calval_tables/plot_single_model.py +69 -0
- {ecov002_calval_tables-1.4.0.dist-info → ecov002_calval_tables-1.5.0.dist-info}/METADATA +7 -1
- ecov002_calval_tables-1.5.0.dist-info/RECORD +15 -0
- ecov002_calval_tables-1.4.0.dist-info/RECORD +0 -10
- {ecov002_calval_tables-1.4.0.dist-info → ecov002_calval_tables-1.5.0.dist-info}/WHEEL +0 -0
- {ecov002_calval_tables-1.4.0.dist-info → ecov002_calval_tables-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {ecov002_calval_tables-1.4.0.dist-info → ecov002_calval_tables-1.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains functions for plotting and evaluating meteorological and
|
|
3
|
+
flux tower data.
|
|
4
|
+
|
|
5
|
+
It provides several plotting functions for a quick look at data, including
|
|
6
|
+
scatter plots for evaluating different meteorological variables and energy
|
|
7
|
+
fluxes, and a function for quality assurance and quality control (QAQC) plots.
|
|
8
|
+
The module relies on libraries like pandas, numpy, and matplotlib for data
|
|
9
|
+
manipulation and visualization.
|
|
10
|
+
"""
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import matplotlib.lines as mlines
|
|
13
|
+
import numpy as np
|
|
14
|
+
import os
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import sys
|
|
17
|
+
from matplotlib.dates import DateFormatter
|
|
18
|
+
|
|
19
|
+
# Assuming 'error_funcs' is a local module in the same directory or on the
|
|
20
|
+
# Python path
|
|
21
|
+
from . import error_funcs
|
|
22
|
+
|
|
23
|
+
REL_PATH = os.getcwd() + '/'
|
|
24
|
+
FIG_PATH = REL_PATH + 'results/figures/'
|
|
25
|
+
LIB_PATH = REL_PATH + 'src'
|
|
26
|
+
sys.path.insert(0, LIB_PATH)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def quick_look_plots_met(big_df_ss, time):
|
|
30
|
+
"""
|
|
31
|
+
Generates a set of 3x2 scatter plots comparing various meteorological
|
|
32
|
+
variables from a model against observed data.
|
|
33
|
+
|
|
34
|
+
The function plots Net Radiation, Downwelling Shortwave Radiation, Air
|
|
35
|
+
Temperature, Relative Humidity, Surface Soil Moisture, and Root Zone Soil
|
|
36
|
+
Moisture. It calculates and displays key statistical metrics (RMSE, R²,
|
|
37
|
+
slope, intercept, and bias) for each variable.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
big_df_ss (pd.DataFrame): A DataFrame containing both model and
|
|
41
|
+
observed data.
|
|
42
|
+
time (str): A string representing the time period for the plot file
|
|
43
|
+
name.
|
|
44
|
+
"""
|
|
45
|
+
# Set plotting style and parameters
|
|
46
|
+
plt.rc('lines', linestyle='None')
|
|
47
|
+
plt.style.use('seaborn-v0_8-whitegrid')
|
|
48
|
+
plt.rcParams.update({'font.size': 14})
|
|
49
|
+
fig, axs = plt.subplots(3, 2, figsize=(9, 12))
|
|
50
|
+
one2one = np.arange(-250, 1200, 5)
|
|
51
|
+
|
|
52
|
+
def plot_metric(
|
|
53
|
+
ax,
|
|
54
|
+
x_data,
|
|
55
|
+
y_data,
|
|
56
|
+
title,
|
|
57
|
+
xlabel,
|
|
58
|
+
ylabel,
|
|
59
|
+
color,
|
|
60
|
+
ylim,
|
|
61
|
+
xlim,
|
|
62
|
+
plot_label,
|
|
63
|
+
text_x,
|
|
64
|
+
text_y,
|
|
65
|
+
):
|
|
66
|
+
"""Helper function to plot a single metric and its statistics."""
|
|
67
|
+
rmse = error_funcs.rmse(y_data, x_data)
|
|
68
|
+
r2 = error_funcs.R2_fun(y_data, x_data)
|
|
69
|
+
slope, intercept = error_funcs.lin_regress(y_data, x_data)
|
|
70
|
+
bias = error_funcs.BIAS_fun(y_data, x_data)
|
|
71
|
+
|
|
72
|
+
ax.scatter(x_data, y_data, c=color, marker='o', s=4)
|
|
73
|
+
ax.plot(one2one, one2one, '--', c='k')
|
|
74
|
+
ax.plot(one2one, one2one * slope + intercept, '--', c='gray')
|
|
75
|
+
ax.set_ylim(ylim)
|
|
76
|
+
ax.set_xlim(xlim)
|
|
77
|
+
ax.set_title(title)
|
|
78
|
+
ax.set_ylabel(ylabel)
|
|
79
|
+
ax.set_xlabel(xlabel)
|
|
80
|
+
ax.text(
|
|
81
|
+
text_x,
|
|
82
|
+
text_y,
|
|
83
|
+
f'y = {round(slope, 2)}x + {round(intercept, 1)}\n'
|
|
84
|
+
f'RMSE: {round(rmse, 1)} {ylabel.split(" ")[-1]}\n'
|
|
85
|
+
f'bias: {round(bias, 1)} {ylabel.split(" ")[-1]}\n'
|
|
86
|
+
f'R²: {round(r2, 2)}',
|
|
87
|
+
)
|
|
88
|
+
ax.text(
|
|
89
|
+
-0.2,
|
|
90
|
+
1.05,
|
|
91
|
+
f'{plot_label})',
|
|
92
|
+
transform=ax.transAxes,
|
|
93
|
+
fontsize=14,
|
|
94
|
+
weight='bold',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Net Radiation Plot
|
|
98
|
+
plot_metric(
|
|
99
|
+
axs[0, 0],
|
|
100
|
+
big_df_ss.NETRAD_filt.to_numpy(),
|
|
101
|
+
big_df_ss.Rn.to_numpy(),
|
|
102
|
+
'Net Radiation',
|
|
103
|
+
'Obs Rn Wm$^{-2}$',
|
|
104
|
+
'Model Rn Wm$^{-2}$',
|
|
105
|
+
'darkorange',
|
|
106
|
+
[-200, 1000],
|
|
107
|
+
[-200, 1000],
|
|
108
|
+
'a',
|
|
109
|
+
-150,
|
|
110
|
+
600,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Downwelling Shortwave Radiation Plot
|
|
114
|
+
plot_metric(
|
|
115
|
+
axs[0, 1],
|
|
116
|
+
big_df_ss.SW_IN.to_numpy(),
|
|
117
|
+
big_df_ss.Rg.to_numpy(),
|
|
118
|
+
'Downwelling Shortwave Radiation',
|
|
119
|
+
'Obs R$_{SD}$ Wm$^{-2}$',
|
|
120
|
+
'Model R$_{SD}$ Wm$^{-2}$',
|
|
121
|
+
'orange',
|
|
122
|
+
[-200, 1500],
|
|
123
|
+
[-200, 1500],
|
|
124
|
+
'b',
|
|
125
|
+
-150,
|
|
126
|
+
950,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Air Temperature Plot
|
|
130
|
+
plot_metric(
|
|
131
|
+
axs[1, 0],
|
|
132
|
+
big_df_ss.AirTempC.to_numpy(),
|
|
133
|
+
big_df_ss.Ta.to_numpy(),
|
|
134
|
+
'Air Temp (C)',
|
|
135
|
+
'Obs Ta $^{o}$C',
|
|
136
|
+
'Model Ta $^{o}$C',
|
|
137
|
+
'darkred',
|
|
138
|
+
[-25, 40],
|
|
139
|
+
[-25, 40],
|
|
140
|
+
'c',
|
|
141
|
+
10,
|
|
142
|
+
-22,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Relative Humidity Plot
|
|
146
|
+
plot_metric(
|
|
147
|
+
axs[1, 1],
|
|
148
|
+
big_df_ss.RH_percentage.to_numpy(),
|
|
149
|
+
big_df_ss.RH.to_numpy(),
|
|
150
|
+
'RH',
|
|
151
|
+
'Obs RH',
|
|
152
|
+
'Model RH',
|
|
153
|
+
'royalblue',
|
|
154
|
+
[0, 1],
|
|
155
|
+
[0, 1],
|
|
156
|
+
'd',
|
|
157
|
+
0.5,
|
|
158
|
+
0.05,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Surface Soil Moisture Plot
|
|
162
|
+
big_df_ss.loc[big_df_ss.SM_surf > 0.60, 'SM_surf'] = np.nan
|
|
163
|
+
plot_metric(
|
|
164
|
+
axs[2, 0],
|
|
165
|
+
big_df_ss.SM_surf.to_numpy(),
|
|
166
|
+
big_df_ss.SM.to_numpy(),
|
|
167
|
+
'SM$_{surf}$',
|
|
168
|
+
'Obs VWC m$^{3}$m$^{-3}$',
|
|
169
|
+
'Model VWC m$^{3}$m$^{-3}$',
|
|
170
|
+
'lightblue',
|
|
171
|
+
[0, 0.8],
|
|
172
|
+
[0, 0.8],
|
|
173
|
+
'e',
|
|
174
|
+
0.1,
|
|
175
|
+
0.55,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Root Zone Soil Moisture Plot
|
|
179
|
+
big_df_ss.loc[big_df_ss.SM_rz > 0.60, 'SM_rz'] = np.nan
|
|
180
|
+
plot_metric(
|
|
181
|
+
axs[2, 1],
|
|
182
|
+
big_df_ss.SM_rz.to_numpy(),
|
|
183
|
+
big_df_ss.SM.to_numpy(),
|
|
184
|
+
'SM$_{rz}$',
|
|
185
|
+
'Obs VWC m$^{3}$m$^{-3}$',
|
|
186
|
+
'Model VWC m$^{3}$m$^{-3}$',
|
|
187
|
+
'darkblue',
|
|
188
|
+
[0, 0.8],
|
|
189
|
+
[0, 0.8],
|
|
190
|
+
'f',
|
|
191
|
+
0.1,
|
|
192
|
+
0.55,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
fig.tight_layout()
|
|
196
|
+
fig.savefig(FIG_PATH + 'auxiliary/auxiliary_eval_' + time + '.png', dpi=300)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def quick_look_plots(big_df_ss, time, LE_var='LEcorr50'):
|
|
200
|
+
"""
|
|
201
|
+
Generates a set of 3x2 scatter plots comparing various energy flux models
|
|
202
|
+
against a chosen reference variable, with optional error bars and a legend
|
|
203
|
+
for vegetation types.
|
|
204
|
+
|
|
205
|
+
The function plots Latent Heat (LE) fluxes from six different models:
|
|
206
|
+
PT-JPL (C1), JET, PT-JPL_SM, BESS, STIC, and MOD16. It calculates and
|
|
207
|
+
displays key statistical metrics (RMSE, R², slope, intercept, and bias) for
|
|
208
|
+
each model's performance.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
big_df_ss (pd.DataFrame): A DataFrame containing model and observed
|
|
212
|
+
data.
|
|
213
|
+
time (str): A string representing the time period for the plot file
|
|
214
|
+
name.
|
|
215
|
+
LE_var (str, optional): The name of the column in big_df_ss to use as
|
|
216
|
+
the reference LE variable. Defaults to
|
|
217
|
+
'LEcorr50'.
|
|
218
|
+
"""
|
|
219
|
+
plt.rc('lines', linestyle='None')
|
|
220
|
+
plt.style.use('seaborn-v0_8-whitegrid')
|
|
221
|
+
|
|
222
|
+
colors = {
|
|
223
|
+
'CRO': '#FFEC8B', 'CSH': '#AB82FF', 'CVM': '#8B814C',
|
|
224
|
+
'DBF': '#98FB98', 'EBF': '#7FFF00', 'ENF': '#006400',
|
|
225
|
+
'GRA': '#FFA54F', 'MF': '#8FBC8F', 'OSH': '#FFE4E1',
|
|
226
|
+
'SAV': '#FFD700', 'WAT': '#98F5FF', 'WET': '#4169E1',
|
|
227
|
+
'WSA': '#CDAA7D',
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
scatter_colors = [colors.get(veg, 'gray') for veg in big_df_ss['vegetation']]
|
|
231
|
+
one2one = np.arange(-250, 1200, 5)
|
|
232
|
+
|
|
233
|
+
def calculate_metrics(x, y):
|
|
234
|
+
"""Helper function to calculate error metrics."""
|
|
235
|
+
rmse = error_funcs.rmse(y, x)
|
|
236
|
+
r2 = error_funcs.R2_fun(y, x)
|
|
237
|
+
slope, intercept = error_funcs.lin_regress(y, x)
|
|
238
|
+
bias = error_funcs.BIAS_fun(y, x)
|
|
239
|
+
return rmse, r2, slope, intercept, bias
|
|
240
|
+
|
|
241
|
+
metrics = {
|
|
242
|
+
'ETinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.ETinst),
|
|
243
|
+
'JET': calculate_metrics(big_df_ss[LE_var], big_df_ss.JET),
|
|
244
|
+
'PTJPLSMinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.PTJPLSMinst),
|
|
245
|
+
'BESSinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.BESSinst),
|
|
246
|
+
'STICinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.STICinst),
|
|
247
|
+
'MOD16inst': calculate_metrics(big_df_ss[LE_var], big_df_ss.MOD16inst),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
model_names = {
|
|
251
|
+
'ETinst': 'PT-JPL (C1)',
|
|
252
|
+
'JET': 'JET',
|
|
253
|
+
'PTJPLSMinst': 'PT-JPL$_{SM}$',
|
|
254
|
+
'BESSinst': 'BESS',
|
|
255
|
+
'STICinst': 'STIC',
|
|
256
|
+
'MOD16inst': 'MOD16',
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fig, axs = plt.subplots(3, 2, figsize=(9, 12))
|
|
260
|
+
plt.rcParams.update({'font.size': 14})
|
|
261
|
+
subplot_labels = ['a)', 'b)', 'c)', 'd)', 'e)', 'f)']
|
|
262
|
+
|
|
263
|
+
for i, (key, (rmse, r2, slope, intercept, bias)) in enumerate(metrics.items()):
|
|
264
|
+
x = big_df_ss[LE_var].to_numpy()
|
|
265
|
+
y = big_df_ss[f'{key}'].to_numpy()
|
|
266
|
+
err = big_df_ss['ETinstUncertainty'].to_numpy()
|
|
267
|
+
xerr = big_df_ss[['LE_filt', 'LEcorr50', 'LEcorr_ann']].std(axis=1).to_numpy()
|
|
268
|
+
|
|
269
|
+
ax = axs[i // 2, i % 2]
|
|
270
|
+
ax.errorbar(x, y, yerr=err, xerr=xerr, fmt='', ecolor='lightgray')
|
|
271
|
+
ax.scatter(x, y, c=scatter_colors, marker='o', s=4, zorder=4)
|
|
272
|
+
ax.plot(one2one, one2one, '--', c='k')
|
|
273
|
+
ax.plot(one2one, one2one * slope + intercept, '--', c='gray')
|
|
274
|
+
ax.set_title(model_names[key])
|
|
275
|
+
ax.set_xlim([-250, 1200])
|
|
276
|
+
ax.set_ylim([-250, 1200])
|
|
277
|
+
if i % 2 == 0:
|
|
278
|
+
ax.set_ylabel('Model LE Wm$^{-2}$', fontsize=14)
|
|
279
|
+
|
|
280
|
+
ax.text(
|
|
281
|
+
-0.1,
|
|
282
|
+
1.1,
|
|
283
|
+
subplot_labels[i],
|
|
284
|
+
transform=ax.transAxes,
|
|
285
|
+
fontsize=16,
|
|
286
|
+
fontweight='bold',
|
|
287
|
+
va='top',
|
|
288
|
+
ha='right',
|
|
289
|
+
)
|
|
290
|
+
ax.text(
|
|
291
|
+
500,
|
|
292
|
+
-200,
|
|
293
|
+
f'y = {slope:.1f}x + {intercept:.1f} \n'
|
|
294
|
+
f'RMSE: {rmse:.1f} Wm$^-$² \n'
|
|
295
|
+
f'bias: {bias:.1f} Wm$^-$² \n'
|
|
296
|
+
f'R$^2$: {r2:.2f}',
|
|
297
|
+
fontsize=12,
|
|
298
|
+
)
|
|
299
|
+
if i // 2 == 2:
|
|
300
|
+
ax.set_xlabel('Flux Tower LE Wm$^{-2}$', fontsize=14)
|
|
301
|
+
|
|
302
|
+
# Create legend
|
|
303
|
+
scatter_handles = [
|
|
304
|
+
mlines.Line2D(
|
|
305
|
+
[],
|
|
306
|
+
[],
|
|
307
|
+
color=color,
|
|
308
|
+
marker='o',
|
|
309
|
+
linestyle='None',
|
|
310
|
+
markersize=6,
|
|
311
|
+
label=veg,
|
|
312
|
+
)
|
|
313
|
+
for veg, color in colors.items()
|
|
314
|
+
]
|
|
315
|
+
fig.legend(
|
|
316
|
+
handles=scatter_handles,
|
|
317
|
+
loc='lower center',
|
|
318
|
+
bbox_to_anchor=(0.5, -0.05),
|
|
319
|
+
ncol=7,
|
|
320
|
+
title='Vegetation Type',
|
|
321
|
+
fontsize=10,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
fig.tight_layout()
|
|
325
|
+
fig.savefig(
|
|
326
|
+
f'{FIG_PATH}/le_fluxes/le_eval_{LE_var}_{time}.png',
|
|
327
|
+
dpi=600,
|
|
328
|
+
bbox_inches='tight',
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def quick_look_plots_filt(big_df_ss, time, LE_var='LEcorr50'):
|
|
333
|
+
"""
|
|
334
|
+
Generates a set of 3x2 scatter plots similar to `quick_look_plots`, but with
|
|
335
|
+
additional filtering on BESS and STIC data.
|
|
336
|
+
|
|
337
|
+
This function is a variant of `quick_look_plots` that specifically handles
|
|
338
|
+
filtering out zero and extreme values from 'BESSinst' and 'STICinst' data
|
|
339
|
+
before plotting, and presents the results for the same set of energy flux
|
|
340
|
+
models.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
big_df_ss (pd.DataFrame): A DataFrame containing model and observed
|
|
344
|
+
data.
|
|
345
|
+
time (str): A string representing the time period for the plot file
|
|
346
|
+
name.
|
|
347
|
+
LE_var (str, optional): The name of the column in big_df_ss to use as
|
|
348
|
+
the reference LE variable. Defaults to
|
|
349
|
+
'LEcorr50'.
|
|
350
|
+
"""
|
|
351
|
+
big_df_ss['BESSinst'].replace(0, np.nan, inplace=True)
|
|
352
|
+
big_df_ss['BESSinst'].replace(1000, np.nan, inplace=True)
|
|
353
|
+
big_df_ss['STICinst'].replace(0, np.nan, inplace=True)
|
|
354
|
+
|
|
355
|
+
plt.rc('lines', linestyle='None')
|
|
356
|
+
plt.style.use('seaborn-v0_8-whitegrid')
|
|
357
|
+
|
|
358
|
+
colors = {
|
|
359
|
+
'CRO': '#FFEC8B', 'CSH': '#AB82FF', 'CVM': '#8B814C',
|
|
360
|
+
'DBF': '#98FB98', 'EBF': '#7FFF00', 'ENF': '#006400',
|
|
361
|
+
'GRA': '#FFA54F', 'MF': '#8FBC8F', 'OSH': '#FFE4E1',
|
|
362
|
+
'SAV': '#FFD700', 'WAT': '#98F5FF', 'WET': '#4169E1',
|
|
363
|
+
'WSA': '#CDAA7D',
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
scatter_colors = [colors.get(veg, 'gray') for veg in big_df_ss['vegetation']]
|
|
367
|
+
one2one = np.arange(-250, 1200, 5)
|
|
368
|
+
|
|
369
|
+
def calculate_metrics(x, y):
|
|
370
|
+
"""Helper function to calculate error metrics."""
|
|
371
|
+
rmse = error_funcs.rmse(y, x)
|
|
372
|
+
r2 = error_funcs.R2_fun(y, x)
|
|
373
|
+
slope, intercept = error_funcs.lin_regress(y, x)
|
|
374
|
+
bias = error_funcs.BIAS_fun(y, x)
|
|
375
|
+
return rmse, r2, slope, intercept, bias
|
|
376
|
+
|
|
377
|
+
metrics = {
|
|
378
|
+
'ETinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.ETinst),
|
|
379
|
+
'PTJPLSMinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.PTJPLSMinst),
|
|
380
|
+
'BESSinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.BESSinst),
|
|
381
|
+
'STICinst': calculate_metrics(big_df_ss[LE_var], big_df_ss.STICinst),
|
|
382
|
+
'MOD16inst': calculate_metrics(big_df_ss[LE_var], big_df_ss.MOD16inst),
|
|
383
|
+
'JET': calculate_metrics(big_df_ss[LE_var], big_df_ss.JET),
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fig, axs = plt.subplots(3, 2, figsize=(9, 12))
|
|
387
|
+
plt.rcParams.update({'font.size': 14})
|
|
388
|
+
|
|
389
|
+
for i, (key, (rmse, r2, slope, intercept, bias)) in enumerate(metrics.items()):
|
|
390
|
+
x = big_df_ss[LE_var].to_numpy()
|
|
391
|
+
y = big_df_ss[f'{key}'].to_numpy()
|
|
392
|
+
err = big_df_ss['ETinstUncertainty'].to_numpy()
|
|
393
|
+
xerr = big_df_ss[['LE_filt', 'LEcorr50', 'LEcorr_ann']].std(axis=1).to_numpy()
|
|
394
|
+
|
|
395
|
+
ax = axs[i // 2, i % 2]
|
|
396
|
+
ax.errorbar(x, y, yerr=err, xerr=xerr, fmt='', ecolor='lightgray')
|
|
397
|
+
ax.scatter(x, y, c=scatter_colors, marker='o', s=4, zorder=4)
|
|
398
|
+
ax.plot(one2one, one2one, '--', c='k')
|
|
399
|
+
ax.plot(one2one, one2one * slope + intercept, '--', c='gray')
|
|
400
|
+
ax.set_title(f'{key}')
|
|
401
|
+
ax.set_xlim([-250, 1200])
|
|
402
|
+
ax.set_ylim([-250, 1200])
|
|
403
|
+
if i % 2 == 0:
|
|
404
|
+
ax.set_ylabel('Model LE Wm$^{-2}$', fontsize=14)
|
|
405
|
+
|
|
406
|
+
ax.text(
|
|
407
|
+
500,
|
|
408
|
+
-200,
|
|
409
|
+
f'y = {slope:.2f}x + {intercept:.2f}\n'
|
|
410
|
+
f'RMSE: {rmse:.2f} Wm$^{-2}$\n'
|
|
411
|
+
f'R$^2$: {r2:.3f}\n'
|
|
412
|
+
f'bias: {bias:.2f}Wm$^{-2}',
|
|
413
|
+
fontsize=12,
|
|
414
|
+
)
|
|
415
|
+
if i // 2 == 2:
|
|
416
|
+
ax.set_xlabel('Flux Tower LE Wm$^{-2}$', fontsize=14)
|
|
417
|
+
|
|
418
|
+
fig.tight_layout()
|
|
419
|
+
fig.savefig(
|
|
420
|
+
f'{FIG_PATH}/le_fluxes/le_eval_filt_{time}.png', dpi=600,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def plot_colocated(ground_site_df, eco_site_i_df, site, utc_offset):
|
|
425
|
+
"""
|
|
426
|
+
Generates a series of diurnal plots comparing ground-based flux data with
|
|
427
|
+
colocated model data for a specific site.
|
|
428
|
+
|
|
429
|
+
This function plots various observed energy fluxes (NETRAD, LE, H, G) for
|
|
430
|
+
each day and superimposes a point for the 'JET' model's LE flux at the
|
|
431
|
+
corresponding observation time, including an uncertainty bar.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
ground_site_df (pd.DataFrame): DataFrame containing ground-based
|
|
435
|
+
flux data.
|
|
436
|
+
eco_site_i_df (pd.DataFrame): DataFrame containing colocated model data.
|
|
437
|
+
site (str): The name of the site.
|
|
438
|
+
utc_offset (int): The UTC offset for the site.
|
|
439
|
+
"""
|
|
440
|
+
eco_site_i_df['JET'] = eco_site_i_df[
|
|
441
|
+
['PTJPLSMinst', 'BESSinst', 'STICinst', 'MOD16inst']
|
|
442
|
+
].median(axis=1)
|
|
443
|
+
|
|
444
|
+
eco_site_i_df['solar_time'] = eco_site_i_df.index + pd.DateOffset(
|
|
445
|
+
hours=utc_offset,
|
|
446
|
+
)
|
|
447
|
+
ground_site_df['solar_time'] = ground_site_df.index + pd.DateOffset(
|
|
448
|
+
hours=utc_offset,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
for idx in eco_site_i_df.index:
|
|
452
|
+
solar_day = (idx + pd.DateOffset(hours=utc_offset)).normalize()
|
|
453
|
+
df_ground_day = ground_site_df[
|
|
454
|
+
ground_site_df['solar_time'].dt.normalize() == solar_day
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
fig, ax = plt.subplots(figsize=(4, 4))
|
|
458
|
+
ax.plot(
|
|
459
|
+
df_ground_day['solar_time'],
|
|
460
|
+
df_ground_day['NETRAD_filt'],
|
|
461
|
+
label='NETRAD_filt',
|
|
462
|
+
)
|
|
463
|
+
ax.plot(
|
|
464
|
+
df_ground_day['solar_time'],
|
|
465
|
+
df_ground_day['LE_filt'],
|
|
466
|
+
label='LE_filt',
|
|
467
|
+
)
|
|
468
|
+
ax.plot(
|
|
469
|
+
df_ground_day['solar_time'],
|
|
470
|
+
df_ground_day['LEcorr50'],
|
|
471
|
+
label='LEcorr50',
|
|
472
|
+
)
|
|
473
|
+
ax.plot(
|
|
474
|
+
df_ground_day['solar_time'],
|
|
475
|
+
df_ground_day['H_filt'],
|
|
476
|
+
label='H_filt',
|
|
477
|
+
)
|
|
478
|
+
ax.plot(
|
|
479
|
+
df_ground_day['solar_time'],
|
|
480
|
+
df_ground_day['G_filt'],
|
|
481
|
+
label='G_filt',
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
y_value = eco_site_i_df.loc[idx, 'JET']
|
|
485
|
+
yerr_value = eco_site_i_df.loc[idx, 'ETinstUncertainty']
|
|
486
|
+
ax.errorbar(
|
|
487
|
+
eco_site_i_df.loc[idx, 'solar_time'],
|
|
488
|
+
y_value,
|
|
489
|
+
yerr=yerr_value,
|
|
490
|
+
fmt='ro',
|
|
491
|
+
label=f'JET {idx.time()}',
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
ax.set_xlabel('Time')
|
|
495
|
+
ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
|
|
496
|
+
plt.xticks(rotation=45)
|
|
497
|
+
ax.set_ylabel('Wm$^{-2}$')
|
|
498
|
+
ax.legend(fontsize='x-small')
|
|
499
|
+
plt.title(site + ' ' + f'{idx}')
|
|
500
|
+
plt.savefig(
|
|
501
|
+
FIG_PATH
|
|
502
|
+
+ 'supplementary/diurnal_observations/'
|
|
503
|
+
+ site
|
|
504
|
+
+ '_'
|
|
505
|
+
+ f'{idx}'
|
|
506
|
+
+ '.png',
|
|
507
|
+
dpi=300,
|
|
508
|
+
)
|
|
509
|
+
plt.close(fig)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def plot_blind_filter(ground_site_df, all_site_eco_data_df, site, utc_offset):
|
|
513
|
+
"""
|
|
514
|
+
Generates a series of diurnal plots for a specific site, comparing
|
|
515
|
+
ground-based flux data with a model's 'JET' flux.
|
|
516
|
+
|
|
517
|
+
This function plots observed energy fluxes (NETRAD, LE, H, G) for each day
|
|
518
|
+
and adds a vertical line at the observation time of the 'JET' model to
|
|
519
|
+
visualize the model's output in the context of the ground observations.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
ground_site_df (pd.DataFrame): DataFrame containing ground-based
|
|
523
|
+
flux data.
|
|
524
|
+
all_site_eco_data_df (pd.DataFrame): DataFrame containing model data
|
|
525
|
+
for all sites.
|
|
526
|
+
site (str): The name of the site.
|
|
527
|
+
utc_offset (int): The UTC offset for the site.
|
|
528
|
+
"""
|
|
529
|
+
all_site_eco_data_df['JET'] = all_site_eco_data_df[
|
|
530
|
+
['PTJPLSMinst', 'BESSinst', 'STICinst', 'MOD16inst']
|
|
531
|
+
].median(axis=1)
|
|
532
|
+
|
|
533
|
+
all_site_eco_data_df['solar_time'] = all_site_eco_data_df.index + pd.DateOffset(
|
|
534
|
+
hours=utc_offset,
|
|
535
|
+
)
|
|
536
|
+
ground_site_df['solar_time'] = ground_site_df.index + pd.DateOffset(
|
|
537
|
+
hours=utc_offset,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
for idx in all_site_eco_data_df.index:
|
|
541
|
+
solar_day = (idx + pd.DateOffset(hours=utc_offset)).normalize()
|
|
542
|
+
df_ground_day = ground_site_df[
|
|
543
|
+
ground_site_df['solar_time'].dt.normalize() == solar_day
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
fig, ax = plt.subplots(figsize=(4, 4))
|
|
547
|
+
ax.plot(
|
|
548
|
+
df_ground_day['solar_time'],
|
|
549
|
+
df_ground_day['NETRAD_filt'],
|
|
550
|
+
label='NETRAD_filt',
|
|
551
|
+
)
|
|
552
|
+
ax.plot(
|
|
553
|
+
df_ground_day['solar_time'],
|
|
554
|
+
df_ground_day['LE_filt'],
|
|
555
|
+
label='LE_filt',
|
|
556
|
+
)
|
|
557
|
+
ax.plot(
|
|
558
|
+
df_ground_day['solar_time'],
|
|
559
|
+
df_ground_day['LEcorr50'],
|
|
560
|
+
label='LEcorr50',
|
|
561
|
+
)
|
|
562
|
+
ax.plot(
|
|
563
|
+
df_ground_day['solar_time'],
|
|
564
|
+
df_ground_day['H_filt'],
|
|
565
|
+
label='H_filt',
|
|
566
|
+
)
|
|
567
|
+
ax.plot(
|
|
568
|
+
df_ground_day['solar_time'],
|
|
569
|
+
df_ground_day['G_filt'],
|
|
570
|
+
label='G_filt',
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
ax.axvline(
|
|
574
|
+
x=all_site_eco_data_df.loc[idx, 'solar_time'],
|
|
575
|
+
color='red',
|
|
576
|
+
linestyle='--',
|
|
577
|
+
label='Observation time',
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
ax.set_xlabel('Time')
|
|
581
|
+
ax.xaxis.set_major_formatter(DateFormatter('%H:%M'))
|
|
582
|
+
plt.xticks(rotation=45)
|
|
583
|
+
ax.set_ylabel('Wm$^{-2}$')
|
|
584
|
+
ax.legend(fontsize='x-small')
|
|
585
|
+
plt.title(site + ' ' + f'{idx}')
|
|
586
|
+
plt.savefig(
|
|
587
|
+
FIG_PATH
|
|
588
|
+
+ 'supplementary/blind_filter/'
|
|
589
|
+
+ site
|
|
590
|
+
+ '_'
|
|
591
|
+
+ f'{idx}'
|
|
592
|
+
+ '.png',
|
|
593
|
+
dpi=300,
|
|
594
|
+
)
|
|
595
|
+
plt.close(fig)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def qaqc_plots(site):
|
|
599
|
+
"""
|
|
600
|
+
Generates a series of quality assurance and quality control (QAQC) plots
|
|
601
|
+
for a specified site's flux data.
|
|
602
|
+
|
|
603
|
+
This function reads a filtered flux tower data file and creates plots for
|
|
604
|
+
NETRAD, LE, H, and G. The final subplot provides text-based statistics
|
|
605
|
+
about energy balance closure ratios and data availability after filtering.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
site (str): The name of the site for which to generate the plots.
|
|
609
|
+
"""
|
|
610
|
+
plt.style.use('seaborn-v0_8-whitegrid')
|
|
611
|
+
|
|
612
|
+
data_path = REL_PATH + 'data/cleaned_data/'
|
|
613
|
+
file_name = data_path + site + '_qaqc_filt_ebc.csv'
|
|
614
|
+
site_df = pd.read_csv(file_name)
|
|
615
|
+
|
|
616
|
+
site_df['index_time'] = pd.to_datetime(site_df['local_time'])
|
|
617
|
+
site_df.set_index('index_time', inplace=True)
|
|
618
|
+
|
|
619
|
+
fig, axs = plt.subplots(5, 1, figsize=(12, 12))
|
|
620
|
+
|
|
621
|
+
def plot_data(ax, data, label, ylabel):
|
|
622
|
+
"""Helper function to plot a single data series."""
|
|
623
|
+
if data is not None and not data.empty:
|
|
624
|
+
data.plot(label=label, ax=ax, x_compat=True)
|
|
625
|
+
ax.legend()
|
|
626
|
+
ax.set_ylabel(ylabel)
|
|
627
|
+
else:
|
|
628
|
+
ax.set_xticks([])
|
|
629
|
+
|
|
630
|
+
plot_data(axs[0], site_df.get('NETRAD_filt'), 'NETRAD', 'NETRAD Wm-2')
|
|
631
|
+
|
|
632
|
+
if 'LE' in site_df.columns and 'LE_filt' in site_df.columns:
|
|
633
|
+
plot_data(axs[1], site_df['LE'], 'LE', 'LE Wm-2')
|
|
634
|
+
plot_data(axs[1], site_df['LE_filt'], 'LE_filt', 'LE_filt Wm-2')
|
|
635
|
+
|
|
636
|
+
plot_data(axs[2], site_df.get('H_filt'), 'H', 'H Wm-2')
|
|
637
|
+
plot_data(axs[3], site_df.get('G_filt'), 'G', 'G Wm-2')
|
|
638
|
+
|
|
639
|
+
axs[4].set_title('Statistics')
|
|
640
|
+
axs[4].axis('off')
|
|
641
|
+
|
|
642
|
+
if all(
|
|
643
|
+
col in site_df.columns
|
|
644
|
+
for col in ['LE_filt', 'LEcorr_ann', 'LEcorr50', 'LEcorr25', 'LEcorr75']
|
|
645
|
+
):
|
|
646
|
+
closure_ratio = round(np.mean(site_df['LE_filt'] / site_df['LEcorr_ann']), 2)
|
|
647
|
+
le_corr50_mean = round(np.mean(site_df['LE_filt'] / site_df['LEcorr50']), 2)
|
|
648
|
+
le_corr25_mean = round(np.mean(site_df['LE_filt'] / site_df['LEcorr25']), 2)
|
|
649
|
+
le_corr75_mean = round(np.mean(site_df['LE_filt'] / site_df['LEcorr75']), 2)
|
|
650
|
+
|
|
651
|
+
data_availability = {
|
|
652
|
+
'NETRAD': round(site_df['NETRAD_filt'].count() / len(site_df.index), 2),
|
|
653
|
+
'LE': round(site_df['LE_filt'].count() / len(site_df.index), 2),
|
|
654
|
+
'H': round(site_df['H_filt'].count() / len(site_df.index), 3),
|
|
655
|
+
'G': round(site_df['G_filt'].count() / len(site_df.index), 3),
|
|
656
|
+
'SM': round(site_df['SM_surf'].count() / len(site_df.index), 2),
|
|
657
|
+
'Ta': round(site_df['AirTempC'].count() / len(site_df.index), 2),
|
|
658
|
+
'RH': round(site_df['RH'].count() / len(site_df.index), 3),
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
data_availability = {
|
|
662
|
+
k: v for k, v in data_availability.items() if not np.isnan(v)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
axs[4].text(0.0, 0.65, 'Closure Ratio')
|
|
666
|
+
axs[4].text(0.0, 0.5, f'Annual Closure: {closure_ratio}')
|
|
667
|
+
axs[4].text(0.0, 0.35, f'LEcorr50 mean: {le_corr50_mean}')
|
|
668
|
+
axs[4].text(0.0, 0.2, f'LEcorr25 mean: {le_corr25_mean}')
|
|
669
|
+
axs[4].text(0.0, 0.05, f'LEcorr75 mean: {le_corr75_mean}')
|
|
670
|
+
axs[4].text(0.25, 0.65, 'Data Availability After Filtering')
|
|
671
|
+
for i, (label, value) in enumerate(data_availability.items()):
|
|
672
|
+
axs[4].text(0.25, 0.5 - i * 0.15, f'{label}: {value}')
|
|
673
|
+
|
|
674
|
+
fig.tight_layout()
|
|
675
|
+
fig.savefig(
|
|
676
|
+
FIG_PATH + 'supplementary/AMF_qaqc/' + site + '_qaqc.png', dpi=250,
|
|
677
|
+
)
|
|
678
|
+
plt.close(fig)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def bias_eval(big_df_ss, var):
|
|
682
|
+
"""
|
|
683
|
+
Generates a scatter plot to evaluate the bias of different models against
|
|
684
|
+
the 'LEcorr50' reference, as a function of another variable.
|
|
685
|
+
|
|
686
|
+
For each model, the function calculates the bias (model - reference) and
|
|
687
|
+
plots it against a specified variable (e.g., 'NDVI-UQ'). This helps
|
|
688
|
+
visualize how model bias might be correlated with other environmental or
|
|
689
|
+
site characteristics.
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
big_df_ss (pd.DataFrame): A DataFrame containing model data,
|
|
693
|
+
'LEcorr50' reference data, and the
|
|
694
|
+
variable of interest.
|
|
695
|
+
var (str): The name of the column in big_df_ss to use for the x-axis.
|
|
696
|
+
"""
|
|
697
|
+
models = ['JET', 'STICinst', 'PTJPLSMinst', 'MOD16inst', 'BESSinst', 'ETinst']
|
|
698
|
+
for model in models:
|
|
699
|
+
bias = big_df_ss[model] - big_df_ss['LEcorr50']
|
|
700
|
+
plt.style.use('seaborn-v0_8-whitegrid')
|
|
701
|
+
plt.figure(figsize=(10, 6))
|
|
702
|
+
plt.scatter(big_df_ss[var], bias, c='blue', marker='o', alpha=0.5)
|
|
703
|
+
plt.title('Bias vs ' + var)
|
|
704
|
+
plt.xlabel(var)
|
|
705
|
+
plt.ylabel(f'Bias ({model} - LEcorr50)')
|
|
706
|
+
plt.grid(True)
|
|
707
|
+
plt.savefig(
|
|
708
|
+
FIG_PATH + 'supplementary/bias_eval/' + model + '_' + var + '.png',
|
|
709
|
+
dpi=250,
|
|
710
|
+
)
|