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.
@@ -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
+ )