Exploring nitrogen dioxide (NO2) data from PACE/OCI#

Author(s): Anna Windle (NASA, SSAI), Zachary Fasnacht (NASA, SSAI)

Last updated: March 5, 2026

Summary#

This tutorial describes how to access and download trace gas data products developed from PACE OCI data. More information on how these products were created can be found in Fasnacht et al. (2025). This notebook includes examples on how to plot L3M NO2 data as a static and interactive map, as well as how to plot an interactive time series plot. It also provides examples on how to open and plot L2 NO2 data.

Learning Objectives#

At the end of this notebook you will know:

  • How to access PACE OCI trace gas data products

  • Where to find high NO2 vertical column retrievals (hint: it’s a big city)

  • How to create a time series of NO2 data collected at a single location

  • Open and plot L2 NO2 data

1. Setup#

Begin by importing all of the packages used in this notebook and setting your Earthdata Login credentials.

from datetime import datetime

import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import earthaccess
import holoviews
import hvplot.xarray
import matplotlib.colors
import matplotlib.pyplot as plt
import pandas as pd
import xarray as xr

holoviews.config.image_rtol = 1e-2
auth = earthaccess.login()

2. Search for L3M PACE Trace Gas Data#

Level-2 (L2) and Level-3 mapped (L3M) trace gas (TRGAS) data products are available on NASA Earthdata.

Let’s search and open up the first L3M file collected during the temporal span of January 2025 to December 2025:

tspan = ("2025-01-01", "2025-12-31")

results = earthaccess.search_data(
    short_name="PACE_OCI_L3M_TRGAS",
    temporal=tspan,
    count=1,
)
paths = earthaccess.open(results)
paths
[<File-like object S3FileSystem, ob-cumulus-prod-public/PACE_OCI.20250101_20250131.L3m.MO.TRGAS.V3_0.0p1deg.nc>]

You can see that this search opened PACE_OCI.20250101_20250131.L3m.MO.TRGAS.V3_0.0p1deg.nc which is a L3M monthly (MO) composite with a 0.1° (0p1deg) pixel resolution.”

3. Open as an XArray dataset#

We will use xarray.open_dataset to open the NetCDF and then merge them into a single dataset.

dataset = xr.open_dataset(paths[0])
dataset
<xarray.Dataset> Size: 52MB
Dimensions:           (lat: 1800, lon: 3600)
Coordinates:
  * lat               (lat) float32 7kB 89.95 89.85 89.75 ... -89.85 -89.95
  * lon               (lon) float32 14kB -179.9 -179.9 -179.8 ... 179.9 180.0
Data variables:
    total_column_o3   (lat, lon) float32 26MB ...
    total_column_no2  (lat, lon) float32 26MB ...
Attributes: (12/54)
    product_name:                      PACE_OCI.20250101_20250131.L3m.MO.TRGA...
    instrument:                        OCI
    title:                             OCI Level-3 Standard Mapped Image
    project:                           Ocean Biology Processing Group (NASA/G...
    platform:                          PACE
    source:                            satellite observations from OCI-PACE
    ...                                ...
    keywords:                          
    id:                                L3/PACE_OCI.20250101_20250131.L3m.MO.T...
    history:                           /sdps/sdpsoper/Science/OCSSW/V2026.2/b...
    processing_version:                3.0
    identifier_product_doi_authority:  https://dx.doi.org
    identifier_product_doi:            10.5067/PACE/OCI/L3M/TRGAS/3.0

If you expand the total_column_no2 variable, you’ll see that it is a 2D variable consisting of total vertical column NO2 measurements with units of molecules cm-2. Let’s plot it!

fig, ax = plt.subplots(figsize=(9, 5), subplot_kw={"projection": ccrs.PlateCarree()})
ax.coastlines(linewidth=0.2)
ax.add_feature(cfeature.BORDERS, linewidth=0.1)
ax.add_feature(cfeature.OCEAN, linewidth=0.2)
ax.add_feature(cfeature.LAKES, linewidth=0.2)
ax.add_feature(cfeature.LAND, facecolor="oldlace", linewidth=0.2)
cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
    name="no2",
    colors=["lightgrey", "cyan", "yellow", "orange", "red", "darkred"],
)
dataset["total_column_no2"].plot(x="lon", y="lat", vmin=3e15, vmax=10e15, cmap=cmap)
ax.gridlines(
    draw_labels={"left": "y", "bottom": "x"},
    linewidth=0.25,
    color="grey",
    alpha=0.8,
)
plt.show()
../../_images/38be48cd592a6f59268ce87c330703425db758b06dbafa3c28cc058a572e7216.png

Let’s zoom in to Los Angeles, California.

plt.sca(ax)
plt.xlim(-125, -110)
plt.ylim(30, 40)
plt.show()
../../_images/8f233b6196a4c2eecd39c002cf21e0c31514083f3903aa465aa4ee7ee7b62979.png

4. Interactive NO2 plot#

An interative plot allows you to engage with the data such as zooming, panning, and hovering over for more information. We will use the hvplot accessor on XArray data structures to make an interactive plot of the single file we accessed above.

dataset["total_column_no2"].hvplot(
    x="lon",
    y="lat",
    cmap=cmap,
    clim=(3e15, 10e15),
    aspect=2,
    title="Total vertical column NO₂ April 2024",
    widget_location="top",
    geo=True,
    coastline=True,
    tiles="EsriWorldStreetMap",
)