AirNow and WRF-Chem

Our first example will demonstrate the basics available in MELODIES MONET to compare WRF-Chem model results against AirNow surface observations for ozone and PM2.5.

This example shows how one can compare results from two different model simulations against observations. This particular example compares WRF-Chem results using two different chemical mechanisms (RACM_ESRL and RACM_ESRL_VCP). Simulated surface ozone is slightly improved in WRF-Chem using the RACM_ESRL_VCP mechanism as compared to the AirNow observations.

First, we import the melodies_monet.driver module.

from melodies_monet import driver
Please install h5py to open files from the Amazon S3 servers.
Please install h5netcdf to open files from the Amazon S3 servers.

Analysis driver class

Now, lets create an instance of the analysis driver class, melodies_monet.driver.analysis. It consists of these main parts:

  • model instances

  • observation instances

  • a paired instance of both

an = driver.analysis()

Initially, most of our analysis object’s attributes are set to None, though some have meaningful defaults:

an
analysis(
    control='control.yaml',
    control_dict=None,
    models={},
    obs={},
    paired={},
    start_time=None,
    end_time=None,
    download_maps=True,
    output_dir=None,
    debug=False,
)

Control file

We set the YAML control file and begin by reading the file.

Note

Check out the Description of All YAML Options for info on how to create and modify these files.

an.control = "control_wrfchem_mech-0905_2.yaml"
an.read_control()
an.control_dict
{'analysis': {'start_time': '2019-09-05-06:00:00',
  'end_time': '2019-09-06-06:00:00',
  'output_dir': './output/airnow_wrfchem',
  'debug': True},
 'model': {'RACM_ESRL': {'files': 'example:wrfchem:racm_esrl',
   'mod_type': 'wrfchem',
   'mod_kwargs': {'mech': 'racm_esrl_vcp', 'surf_only_nc': True},
   'radius_of_influence': 12000,
   'mapping': {'airnow': {'PM2_5_DRY': 'PM2.5', 'o3': 'OZONE'}},
   'projection': 'None',
   'plot_kwargs': {'color': 'magenta', 'marker': 's', 'linestyle': '-'}},
  'RACM_ESRL_VCP': {'files': 'example:wrfchem:racm_esrl_vcp',
   'mod_type': 'wrfchem',
   'mod_kwargs': {'mech': 'racm_esrl_vcp', 'surf_only_nc': True},
   'radius_of_influence': 12000,
   'mapping': {'airnow': {'PM2_5_DRY': 'PM2.5', 'o3': 'OZONE'}},
   'projection': 'None',
   'plot_kwargs': {'color': 'gold', 'marker': 'o', 'linestyle': '-'}}},
 'obs': {'airnow': {'use_airnow': True,
   'filename': 'example:airnow:2019-09',
   'obs_type': 'pt_sfc',
   'variables': {'OZONE': {'unit_scale': 1,
     'unit_scale_method': '*',
     'nan_value': -1.0,
     'ylabel_plot': 'Ozone (ppbv)',
     'vmin_plot': 15.0,
     'vmax_plot': 55.0,
     'vdiff_plot': 20.0,
     'nlevels_plot': 21},
    'PM2.5': {'unit_scale': 1,
     'unit_scale_method': '*',
     'nan_value': -1.0,
     'ylabel_plot': 'PM2.5 (ug/m3)',
     'ty_scale': 2.0,
     'vmin_plot': 0.0,
     'vmax_plot': 22.0,
     'vdiff_plot': 15.0,
     'nlevels_plot': 23}}}},
 'plots': {'plot_grp1': {'type': 'timeseries',
   'fig_kwargs': {'figsize': [12, 6]},
   'default_plot_kwargs': {'linewidth': 2.0, 'markersize': 10.0},
   'text_kwargs': {'fontsize': 24.0},
   'domain_type': ['all', 'state_name', 'epa_region'],
   'domain_name': ['CONUS', 'CA', 'R9'],
   'data': ['airnow_RACM_ESRL', 'airnow_RACM_ESRL_VCP'],
   'data_proc': {'rem_obs_nan': True,
    'ts_select_time': 'time_local',
    'ts_avg_window': 'H',
    'set_axis': True}},
  'plot_grp2': {'type': 'taylor',
   'fig_kwargs': {'figsize': [8, 8]},
   'default_plot_kwargs': {'linewidth': 2.0, 'markersize': 10.0},
   'text_kwargs': {'fontsize': 16.0},
   'domain_type': ['all'],
   'domain_name': ['CONUS'],
   'data': ['airnow_RACM_ESRL', 'airnow_RACM_ESRL_VCP'],
   'data_proc': {'rem_obs_nan': True, 'set_axis': True}},
  'plot_grp3': {'type': 'spatial_bias',
   'fig_kwargs': {'states': True, 'figsize': [10, 5]},
   'text_kwargs': {'fontsize': 16.0},
   'domain_type': ['all'],
   'domain_name': ['CONUS'],
   'data': ['airnow_RACM_ESRL', 'airnow_RACM_ESRL_VCP'],
   'data_proc': {'rem_obs_nan': True, 'set_axis': True}},
  'plot_grp4': {'type': 'spatial_overlay',
   'fig_kwargs': {'states': True, 'figsize': [10, 5]},
   'text_kwargs': {'fontsize': 16.0},
   'domain_type': ['all', 'epa_region'],
   'domain_name': ['CONUS', 'R9'],
   'data': ['airnow_RACM_ESRL', 'airnow_RACM_ESRL_VCP'],
   'data_proc': {'rem_obs_nan': True, 'set_axis': True}},
  'plot_grp5': {'type': 'boxplot',
   'fig_kwargs': {'figsize': [8, 6]},
   'text_kwargs': {'fontsize': 20.0},
   'domain_type': ['all'],
   'domain_name': ['CONUS'],
   'data': ['airnow_RACM_ESRL', 'airnow_RACM_ESRL_VCP'],
   'data_proc': {'rem_obs_nan': True, 'set_axis': False}}},
 'stats': {'stat_list': ['MB', 'MdnB', 'R2', 'RMSE'],
  'round_output': 2,
  'output_table': False,
  'output_table_kwargs': {'figsize': [7, 3],
   'fontsize': 12.0,
   'xscale': 1.4,
   'yscale': 1.4,
   'edges': 'horizontal'},
  'domain_type': ['all'],
  'domain_name': ['CONUS'],
  'data': ['airnow_RACM_ESRL', 'airnow_RACM_ESRL_VCP']}}

Now, some of our analysis object’s attributes are populated:

an
analysis(
    control='control_wrfchem_mech-0905_2.yaml',
    control_dict=...,
    models={},
    obs={},
    paired={},
    start_time=Timestamp('2019-09-05 06:00:00'),
    end_time=Timestamp('2019-09-06 06:00:00'),
    download_maps=True,
    output_dir='./output/airnow_wrfchem',
    debug=True,
)

Load the model data

The driver will automatically loop through the “models” found in the model section of the YAML file and create an instance of melodies_monet.driver.model for each that includes the

  • label

  • mapping information

  • file names (can be expressed using a glob expression)

  • xarray object

an.open_models()
RACM_ESRL
{'files': 'example:wrfchem:racm_esrl', 'mod_type': 'wrfchem', 'mod_kwargs': {'mech': 'racm_esrl_vcp', 'surf_only_nc': True}, 'radius_of_influence': 12000, 'mapping': {'airnow': {'PM2_5_DRY': 'PM2.5', 'o3': 'OZONE'}}, 'projection': 'None', 'plot_kwargs': {'color': 'magenta', 'marker': 's', 'linestyle': '-'}}
example:wrfchem:racm_esrl
**** Reading WRF-Chem model output...
RACM_ESRL_VCP
{'files': 'example:wrfchem:racm_esrl_vcp', 'mod_type': 'wrfchem', 'mod_kwargs': {'mech': 'racm_esrl_vcp', 'surf_only_nc': True}, 'radius_of_influence': 12000, 'mapping': {'airnow': {'PM2_5_DRY': 'PM2.5', 'o3': 'OZONE'}}, 'projection': 'None', 'plot_kwargs': {'color': 'gold', 'marker': 'o', 'linestyle': '-'}}
example:wrfchem:racm_esrl_vcp
**** Reading WRF-Chem model output...

Applying open_models() populates the models attribute.

an.models
{'RACM_ESRL': model(
     model='wrfchem',
     radius_of_influence=12000,
     mod_kwargs={'mech': 'racm_esrl_vcp', 'surf_only_nc': True, 'var_list': ['o3', 'PM2_5_DRY']},
     file_str='example:wrfchem:racm_esrl',
     label='RACM_ESRL',
     obj=...,
     mapping={'airnow': {'PM2_5_DRY': 'PM2.5', 'o3': 'OZONE'}},
     label='RACM_ESRL',
     ...
 ),
 'RACM_ESRL_VCP': model(
     model='wrfchem',
     radius_of_influence=12000,
     mod_kwargs={'mech': 'racm_esrl_vcp', 'surf_only_nc': True, 'var_list': ['o3', 'PM2_5_DRY']},
     file_str='example:wrfchem:racm_esrl_vcp',
     label='RACM_ESRL_VCP',
     obj=...,
     mapping={'airnow': {'PM2_5_DRY': 'PM2.5', 'o3': 'OZONE'}},
     label='RACM_ESRL_VCP',
     ...
 )}

We can access the underlying dataset with the obj attribute.

an.models['RACM_ESRL'].obj
<xarray.Dataset>
Dimensions:    (y: 284, x: 440, time: 31, z: 1)
Coordinates:
    longitude  (y, x) float32 -122.3 -122.2 -122.1 ... -60.68 -60.52 -60.37
    latitude   (y, x) float32 21.19 21.22 21.24 21.27 ... 50.33 50.28 50.24 50.2
  * time       (time) datetime64[ns] 2019-09-05 ... 2019-09-06T06:00:00
Dimensions without coordinates: y, x, z
Data variables:
    o3         (time, z, y, x) float32 30.0 30.0 30.0 30.0 ... 34.72 36.87 37.13
    PM2_5_DRY  (time, z, y, x) float32 3.046 3.048 2.84 ... 0.45 0.4506 0.4512
Attributes: (12/17)
    FieldType:                                       104
    MemoryOrder:                                     XYZ
    description:                                     O3 mixing ratio
    units:                                           ppmv
    stagger:                                         
    coordinates:                                     XLONG XLAT XTIME
    ...                                              ...
    MOAD_CEN_LAT:                                    39.617638
    STAND_LON:                                       -97.0
    MAP_PROJ:                                        1
    CEN_LAT:                                         39.617638
    CEN_LON:                                         -97.77487
    mapping_tables_to_airnow:                        {'OZONE': 'o3', 'PM2.5':...

Load the observational data

As with the model data, the driver will loop through the “observations” found in the obs section of the YAML file and create an instance of melodies_monet.driver.observation for each.

an.open_obs()
an.obs
{'airnow': observation(
     obs='airnow',
     label='airnow',
     file='example:airnow:2019-09',
     obj=...,
     type='pt_src',
     variable_dict={'OZONE': {'unit_scale': 1, 'unit_scale_method': '*', 'nan_value': -1.0, 'ylabel_plot': 'Ozone (ppbv)', 'vmin_plot': 15.0, 'vmax_plot': 55.0, 'vdiff_plot': 20.0, 'nlevels_plot': 21}, 'PM2.5': {'unit_scale': 1, 'unit_scale_method': '*', 'nan_value': -1.0, 'ylabel_plot': 'PM2.5 (ug/m3)', 'ty_scale': 2.0, 'vmin_plot': 0.0, 'vmax_plot': 22.0, 'vdiff_plot': 15.0, 'nlevels_plot': 23}},
 )}
an.obs['airnow'].obj
<xarray.Dataset>
Dimensions:     (x: 3786, time: 2091, y: 1)
Coordinates:
  * x           (x) int64 0 1 2 3 4 5 6 7 ... 3779 3780 3781 3782 3783 3784 3785
  * time        (time) datetime64[ns] 2019-09-01 ... 2019-09-30T00:30:00
    latitude    (y, x) float64 47.65 47.51 49.02 53.3 ... 36.92 47.93 41.37
    longitude   (y, x) float64 -52.82 -52.79 -55.8 -60.36 ... -94.84 106.9 69.27
    siteid      (y, x) object '000010102' '000010401' ... 'UZB010001'
Dimensions without coordinates: y
Data variables: (12/30)
    BARPR       (time, y, x) float64 ...
    BC          (time, y, x) float64 ...
    CO          (time, y, x) float64 ...
    NO          (time, y, x) float64 ...
    NO2         (time, y, x) float64 ...
    NO2Y        (time, y, x) float64 ...
    ...          ...
    cmsa_name   (y, x) float64 -1.0 -1.0 -1.0 -1.0 -1.0 ... -1.0 -1.0 -1.0 -1.0
    msa_code    (y, x) float64 -1.0 -1.0 -1.0 -1.0 ... -1.0 3.306e+04 -1.0 -1.0
    msa_name    (y, x) object '' '' '' '' '' '' ... '' '' '' ' Miami, OK ' '' ''
    state_name  (y, x) object 'CC' 'CC' 'CC' 'CC' 'CC' 'CC' ... '' '' '' '' ''
    epa_region  (y, x) object 'CA' 'CA' 'CA' 'CA' 'CA' ... '' 'R6' 'DSMG' 'DSUZ'
    time_local  (time, y, x) datetime64[ns] ...
Attributes:
    title:         
    format:        NetCDF-4
    date_created:  2021-06-07

Pair model and observational data

Now, we create a melodies_monet.driver.pair for each model–obs pair using the pair_data() routine.

%%time

an.pair_data()
[########################################] | 100% Completed |  0.1s
[########################################] | 100% Completed |  0.1s
[########################################] | 100% Completed |  0.1s
[########################################] | 100% Completed |  0.2s
[########################################] | 100% Completed |  0.1s
[########################################] | 100% Completed |  0.2s
[########################################] | 100% Completed |  0.2s
[########################################] | 100% Completed |  0.2s
Wall time: 47.5 s
an.paired
{'airnow_RACM_ESRL': pair(
     type='pt_sfc',
     radius_of_influence=1000000.0,
     obs='airnow',
     model='RACM_ESRL',
     model_vars=['PM2_5_DRY', 'o3'],
     obs_vars=['PM2.5', 'OZONE'],
     filename='airnow_RACM_ESRL.nc',
 ),
 'airnow_RACM_ESRL_VCP': pair(
     type='pt_sfc',
     radius_of_influence=1000000.0,
     obs='airnow',
     model='RACM_ESRL_VCP',
     model_vars=['PM2_5_DRY', 'o3'],
     obs_vars=['PM2.5', 'OZONE'],
     filename='airnow_RACM_ESRL_VCP.nc',
 )}
an.paired['airnow_RACM_ESRL']
pair(
    type='pt_sfc',
    radius_of_influence=1000000.0,
    obs='airnow',
    model='RACM_ESRL',
    model_vars=['PM2_5_DRY', 'o3'],
    obs_vars=['PM2.5', 'OZONE'],
    filename='airnow_RACM_ESRL.nc',
)

Plot

The plotting() routine produces plots.

%%time

an.plotting()
Reference std: 5.409628418207226
Warning: ty_scale not specified for OZONE, so default used.
Reference std: 16.454896847070792
Warning: ty_scale not specified for OZONE, so default used.
c:\users\zmoon\git\melodies-monet\melodies_monet\plots\surfplots.py:734: RuntimeWarning: More than 20 figures have been opened. Figures created through the pyplot interface (`matplotlib.pyplot.figure`) are retained until explicitly closed and may consume too much memory. (To control this warning, see the rcParam `figure.max_open_warning`).
  f,ax = plt.subplots(**fig_dict)
Wall time: 2min 33s
../_images/ce10a5081128cca79f39b19daa971ccf7011c820b4ef5ffc1a6e0dd03dcf22a4.png ../_images/8bcb360d8576f83b420e5000eeab3fcd304245cc4f10a72a3d521d3fb495fbac.png ../_images/c8a7ba3da9efb1b9d3659a46a63f4663a97c43422bed984d777b415699092e03.png ../_images/66ff2e5042ce4e52990442dbbf1d77986c9b93177393156d5db0afa12027645a.png ../_images/8440272578c1e768952a72fe43d3c5f1f72d658f01783fb1c7f548f5ac6bfd90.png ../_images/362f3039e31063f71a32e5744feb25139506518c6bd11e1d82e3bd02dab5443e.png ../_images/66ce5b20224fecdd1767522d40d95f7e22e3c2497d5131c22eacdc7f6e639d40.png ../_images/271ab32d7d096c757f2b0eb0882ebd27a06178e9b9ad327e0d37f4a75e73fe48.png ../_images/73eeb5fcf885822e68de1f6982ab56ed4be6b3ccf76e1a8cf3c9eb1752e0e3f4.png ../_images/6fecbe4c51ddbb487d2df63b9ddf1fb36daa79380ec27cc596ba25981f625eda.png ../_images/a01c58460c686085fbb54c49c8c1020bb9f976dcc0f4ddc8f8b3f80069190c56.png ../_images/52875e5850e8da6d160fd668f2dc684a944cbf3411d4d409876de9374e064614.png ../_images/036fdbc28a971ee27d16b12aad6ab91d97d2a5a34dcc51dc3354ac74b7929ea8.png ../_images/7295b4b486d7781f46102ef1968db6f3e0b4b710bbec82809eda1ba1fc7b63f5.png ../_images/50d2e2f88f22ddc2958d47eb6959f9509f9380650d4536897eef142b73f5b5f2.png ../_images/7ce3506f8cec4d83471b8e3370b2388976f170b163a5a53f2fdcbd3570211222.png ../_images/a32fbd978516faf6f3c581b37755cacfa75005acc0da5e438e109a6653f0b324.png ../_images/848e3a4f3c178b0ecbd0cd892f7a434c6f88c8f152a3a77da5d18303f92b77ff.png ../_images/e995f5b1cebca657c75571a4af18da941d39dad8a48a53d1048869cbd7800c1e.png ../_images/93a2f894e75f439475224cce64143609e01faabc2759530a56f0035a40445f8e.png ../_images/88843f1eb31282f04d1be5f6e9bab7e655e53be82015a2b6193d58f567b1012a.png ../_images/6ac3ea9217c7de0bf6daed31a9011d89fa7d1f6b64931b4de4d45e1aa141f806.png

The figures are saved in the directory specified by the analysis instance’s output_dir attribute.

Statistics

The stats() routine produces tables of statistics.

%%time

an.stats()
Wall time: 32.9 s

The stats routine has produced two files (one for each data variable). This is one of them:

output/airnow_wrfchem/stats.OZONE.all.CONUS.2019-09-05_06.2019-09-06_06.csv
Stat_ID,Stat_FullName,airnow_RACM_ESRL,airnow_RACM_ESRL_VCP
MB,Mean_Bias,3.93,3.23
MdnB,Median_Bias,3.86,3.27
R2,Coefficient_of_Determination_(R2),0.56,0.54
RMSE,Root_Mean_Square_Error,11.64,11.59