Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/NASAARSET/PM2.5_AQ_Online_2026/llms.txt

Use this file to discover all available pages before exploring further.

The MERRA2_CNN_HAQAST_PM25 product provides hourly, global estimates of near-surface PM₂.₅ concentrations derived by applying a convolutional neural network to NASA’s Modern-Era Retrospective analysis for Research and Applications Version 2 (MERRA-2) reanalysis fields. The native grid is the MERRA-2 atmospheric grid: 0.5° latitude × 0.625° longitude. Unlike the monthly SatPM product, MERRA-2 CNN resolves the full diel (24-hour) cycle, making it suitable for hourly comparison with ground monitors and for investigating episodic events such as the February 2023 Chilean megafires.

NASA Earthdata DOI

Persistent DOI landing page with citation and metadata

Earthdata Search

Browse and manually download files via the Earthdata Search UI

Key variables

VariableDescription
MERRA2_CNN_Surface_PM25Hourly near-surface PM₂.₅ estimate (µg/m³)
QFLAGQuality flag: values ≥ 3 are considered reliable
PM25_filteredMERRA2_CNN_Surface_PM25 masked where QFLAG < 3
Only retain observations where QFLAG >= 3. Lower flags indicate retrievals influenced by missing or low-confidence MERRA-2 input fields; including them can inflate or suppress concentration estimates.

Authentication

Data access requires a free NASA Earthdata account. The earthaccess Python library handles OAuth token exchange automatically after a one-time interactive login:
import earthaccess
earthaccess.login()   # prompts once per Colab session; caches credentials
Do not hard-code your Earthdata username or password in a shared notebook. Use earthaccess.login() (interactive prompt) or a .netrc file stored in your home directory.

Downloading data with earthaccess

earthaccess.search_data accepts the dataset short_name, a bounding box, and a temporal range and returns a list of granule metadata objects. earthaccess.download then fetches only the granules that intersect your region and time window, skipping files already on disk.
import earthaccess

s_folder_MERRA2_CNN = (
    '/content/drive/MyDrive/Colab Notebooks/ARSET/PM25_Training_2026/MERRA2_CNN_Data'
)

# Search for granules covering the bounding box and time window:
l_results = earthaccess.search_data(
    short_name='MERRA2_CNN_HAQAST_PM25',
    bounding_box=(n_lon_min, n_lat_min, n_lon_max, n_lat_max),
    temporal=(str(t_start), str(t_end))
)

# Download to local storage (skips already-present files):
l_files_MERRA2_CNN = earthaccess.download(l_results, s_folder_MERRA2_CNN)
The time dimension in the resulting Dataset is already populated from the NetCDF files — no F_time_from_file function is needed. See F_subset_and_combine for the full signature.

Plotting

F_plot_map(
    a_data_plot=a_data_MERRA2_CNN['PM25_filtered'],
    n_lat_min=n_lat_min,
    n_lat_max=n_lat_max,
    n_lon_min=n_lon_min,
    n_lon_max=n_lon_max,
    Colorbar_Label='PM$_{2.5}$ [µg/m$^{3}$]',
    Plot_Title=f'MERRA-2 CNN\n(Average {t_start} to {t_end})',
    Colorbar_Limits=[0, 40],
)

Monthly comparison with SatPM

To compare MERRA-2 CNN against the monthly SatPM product, average the hourly MERRA-2 CNN values over each calendar month at each monitor grid cell:
f_data_combined_monthly['MERRA2_CNN'] = np.nan
for i_row in range(len(f_data_combined_monthly)):
    t_month = f_data_combined_monthly.index[i_row][2]
    n_lat   = f_data_combined_monthly.index[i_row][0]
    n_lon   = f_data_combined_monthly.index[i_row][1]
    f_data_combined_monthly.loc[(n_lat, n_lon, t_month), 'MERRA2_CNN'] = (
        a_data_MERRA2_CNN
        .sel(lat=n_lat, lon=n_lon, method='nearest')
        ['PM25_filtered']
        .sel(time=slice(t_month, t_month + pd.DateOffset(months=1)))
        .mean('time')
        .values
    )

Hourly comparison against gridded monitor data

Because MERRA-2 CNN is hourly, it can be compared directly to hourly monitor observations after the monitors are spatially aggregated onto the MERRA-2 0.5° × 0.625° grid. The snippet below snaps each monitor to the nearest MERRA-2 grid centre:
# Snap monitor locations to nearest MERRA-2 grid node:
f_data_monitors_gridded['lat_grid'] = (
    round(f_data_monitors_gridded['lat'] / 0.5) * 0.5
)
f_data_monitors_gridded['lon_grid'] = (
    round(f_data_monitors_gridded['lon'] / 0.625) * 0.625
)
After grouping by (lat_grid, lon_grid, time) and requiring at least n_minimum_samples_per_grid = 3 monitors per cell, the MERRA-2 CNN value for each row is fetched with:
a_data_MERRA2_CNN.sel(
    lat=n_lat, lon=n_lon,
    time=np.datetime64(t_time.split('+')[0]),
    method='nearest'
)['PM25_filtered'].values
The MERRA-2 CNN grid is coarser than urban monitor spacing. Requiring at least 3 monitor samples per grid cell before including a comparison point helps ensure the gridded monitor average is spatially representative of the ~55 km × ~55 km MERRA-2 pixel rather than a single street-level location.

Diel (24-hour) cycle analysis

One advantage of the hourly MERRA-2 CNN product is the ability to examine how well the model reproduces the diel pattern observed at monitors. Median and inter-quartile ranges by local hour of day reveal systematic biases in the boundary-layer parameterisation:
f_data_combined_gridded_by_timeofday = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].median()
f_data_combined_gridded_by_timeofday['Count'] = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].count()
f_data_combined_gridded_by_timeofday['75th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].quantile(0.75)
f_data_combined_gridded_by_timeofday['25th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['Average']].quantile(0.25)
f_data_combined_gridded_by_timeofday['MERRA2_CNN'] = f_data_combined_gridded.groupby('local_hour_of_day')[['MERRA2_CNN']].median()
f_data_combined_gridded_by_timeofday['MERRA2_CNN_75th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['MERRA2_CNN']].quantile(0.75)
f_data_combined_gridded_by_timeofday['MERRA2_CNN_25th_percentile'] = f_data_combined_gridded.groupby('local_hour_of_day')[['MERRA2_CNN']].quantile(0.25)

Build docs developers (and LLMs) love