Visualize the PACE-OCI Level-2 CLOUD Products#

Author(s): Chamara Rajapakshe (NASA, SSAI), Andy Sayer (NASA, UMBC), Kirk Knobelspiesse (NASA), Meng Gao (NASA, SSAI), Sean Foley (NASA, MSU)

Last updated: Oct 15, 2026

Summary#

This notebook summarizes how to access PACE-OCI cloud optical properties (CLOUD) in Level-2 (L2) granules. The CLOUD suite provides cloud optical and microphysical retrievals derived from multi-spectral observations. Note that this notebook is based on an early preliminary version of the product and is therefore subject to future optimizations and changes.

Learning Objectives#

By the end of this notebook, you will understand:

  • How to acquire PACE-OCI L2 CLOUD data

  • Available variables in the CLOUD suite

  • How to visualize cloud properties from PACE-OCI

1. Setup#

Begin by importing all of the packages used in this notebook. If you followed the guidance on the Getting Started page, then the imports will be successful.

import re

import cartopy.crs as ccrs
import earthaccess
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import xarray as xr

Global settings and variables used throughout the notebook.

pd.options.display.max_colwidth = None
plt.style.use("seaborn-v0_8-notebook")
plt.rcParams.update(
    {
        "xtick.labelsize": 12,
        "ytick.labelsize": 12,
    }
)
projection = ccrs.PlateCarree()

Define your area and time of interest. Note that the bounding box is supplied as longitudes and latitudes in the order west, south, east, then north.

bbox = (-90, -13, -89, -12)
tspan = ("2025-07-02", "2025-07-02")

Set (and optionally persist to your home directory as a “netrc” file) your Earthdata Login credentials.

auth = earthaccess.login()

Define functions for some repeatable tasks performed in the tutorial.

def print_variable_description(dataset, variables_pattern=None):
    """
    Print a table of variables, units, and descriptions from a dataset.

    Long descriptions are wrapped to 100 characters; only the first line
    prints the variable name and units.

    Parameters
    ----------
    dataset : xarray.Dataset
        Dataset containing variables whose metadata are stored in `attrs`
        (e.g., `units` and `long_name`).
    variables_pattern : str, optional
        Pattern to filter variable names. If None, all variables are shown.

    Returns
    -----
    pandas.DataFrame
        Table describing variables in the dataset
    """
    import pandas as pd

    df = pd.DataFrame(columns=("Units", "Description"))
    for key, value in dataset.data_vars.items():
        if variables_pattern is None or variables_pattern in key:
            units = value.attrs.get("units", "")
            desc = value.attrs.get("long_name", "")
            df.loc[key, :] = [units, desc]
    return df
def extremes_removed_limits(array):
    """
    Return suggested min and max axis limits based on the interquartile range (IQR) rule.

    Parameters
    ----------
    array : array-like
        Input numeric array.

    Returns
    -------
    tuple
        Suggested min and max for axis limits in plots
    """
    q0, q1, q3, q4 = array.quantile([0, 0.25, 0.75, 1])
    vmin = q1 - 1.5 * (q3 - q1)
    vmax = q3 + 1.5 * (q3 - q1)
    return max(q0, vmin), min(q4, vmax)
def geo_axis_tags(ax, crs=None):
    """
    Add coastlines and labeled latitude/longitude gridlines to a Cartopy axis.

    Parameters
    ----------
    ax : cartopy.mpl.geoaxes.GeoAxes
        Axes object on which to draw the gridlines and coastlines.
    crs : cartopy.crs.CRS, optional
        Coordinate reference system used for the gridlines.
        Default is a Plate Carrée projection with central longitude = 0.
    """
    if crs is None:
        crs = ccrs.PlateCarree()
    gl = ax.gridlines(
        crs=crs,
        draw_labels=["left", "bottom"],
        xlabel_style={"size": 12, "color": "k"},
        ylabel_style={"size": 12, "color": "k"},
    )
    ax.coastlines()
    return
def plot_cloud_phase(dataset, name, ax):
    """Plot a map of cloud phase, a categorical variable, on a cartopy GeoAxes

    Parameters
    ----------
    dataset: xarray.Dataset
        The granule with cloud phase variables
    name: str
        The name of a variable with categorical values
    ax : cartopy.mpl.geoaxes.GeoAxes
        Axes object on which to draw the gridlines and coastlines.
    """
    array = dataset[name]
    try:
        flag_values = array.attrs["flag_values"]
        flag_meanings = array.attrs["flag_meanings"].split(" ")
    except KeyError:
        description = array.attrs["long_name"]
        long_name, flags = description.split(": ")
        flag_pairs = re.findall(r"([0-9]) - ([^,]+)", flags)
        flag_values = np.array([int(a) for a, b in flag_pairs], dtype=array.dtype)
        flag_meanings = [b.replace(" ", "_") for a, b in flag_pairs]
    cmap = plt.get_cmap("tab20", flag_values.size)
    cmap.set_bad("grey")
    ctf = array.plot.pcolormesh(
        x="longitude",
        y="latitude",
        cmap=cmap,
        vmin=flag_values.min() - 0.5,
        vmax=flag_values.max() + 0.5,
        add_colorbar=False,
    )
    cbar = ax.figure.colorbar(ctf, ax=ax, orientation="vertical")
    cbar.set_ticks(flag_values)
    cbar.set_ticklabels(flag_meanings)
    ax.gridlines(draw_labels=["left", "bottom"])
    ax.coastlines()
    ax.set_title(name)

2. Get Level-2 Data#

Search Earthdata collections with earthaccess.search_datasets to find likely candidates, based on a free-text search for “clouds”, among the L2 products from the PACE-OCI instrument.

results = earthaccess.search_datasets(
    keyword="clouds",
    platform="PACE",
    instrument="OCI",
    processing_level_id="2",
)
for item in results:
    title = item["umm"]["EntryTitle"].replace("PACE OCI Level-2 Regional", "")
    print(f'{item.summary["short-name"]}: {title}')
PACE_OCI_L2_CLOUD_MASK:   Cloud Mask Data, version 3.1
PACE_OCI_L2_CLOUD:   Cloud Optical Properties Data, version 3.1
PACE_OCI_L2_CLOUD_MASK_NRT:   Cloud Mask - Near Real-time (NRT) Data, version 3.1
PACE_OCI_L2_CLOUD_NRT:   Cloud Optical Properties - Near Real-time (NRT) Data, version 3.1
PACE_OCI_L2_AOP:   Apparent Optical Properties Data, version 3.2
PACE_OCI_L2_SFREFL:   Surface Reflectance Data, version 3.1
PACE_OCI_L2_AOP_NRT:   Apparent Optical Properties - Near Real-time (NRT) Data, version 3.2
PACE_OCI_L2_LANDVI:   Land Vegetation Indices Data, version 3.1
PACE_OCI_L2_SFREFL_NRT:   Surface Reflectance - Near Real-time (NRT) Data, version 3.1
PACE_OCI_L2_LANDVI_NRT:   Land Vegetation Indices - Near Real-time (NRT) Data, version 3.1

You can use the short name PACE_OCI_L2_CLOUD to get the most recent version available for the CLOUD product at Level-2 for the PACE-OCI instrument. Add the Near Real Time (NRT) suffix for the most recent observations (i.e. PACE_OCI_L2_CLOUD_NRT).

Search for available granules within a time range and geospatial area of interest. The sort_key parameter can help when multiple versions are available; the -revision_date key will show the most recent versions first.

results = earthaccess.search_data(
    short_name="PACE_OCI_L2_CLOUD",
    temporal=tspan,
    bounding_box=bbox,
    sort_key="-revision_date",
)
for item in results:
    display(item)

Data: PACE_OCI.20250702T184325.L2.CLD.V3_1.nc

Size: 114.84 MB

Cloud Hosted: True

Data Preview
paths = earthaccess.open(results)

Extract the science variables from groups within the granule into an xarray.Dataset for convenience in data manipulations.

datatree = xr.open_datatree(paths[0])
dataset = datatree["geophysical_data"].to_dataset()
for item in ("latitude", "longitude"):
    dataset.coords[item] = datatree["navigation_data"][item]
dataset
<xarray.Dataset> Size: 278MB
Dimensions:           (number_of_lines: 1709, pixels_per_line: 1272)
Coordinates:
    latitude          (number_of_lines, pixels_per_line) float32 9MB ...
    longitude         (number_of_lines, pixels_per_line) float32 9MB ...
Dimensions without coordinates: number_of_lines, pixels_per_line
Data variables: (12/29)
    cer_22            (number_of_lines, pixels_per_line) float32 9MB ...
    cot_22            (number_of_lines, pixels_per_line) float32 9MB ...
    cwp_22            (number_of_lines, pixels_per_line) float32 9MB ...
    cer_21            (number_of_lines, pixels_per_line) float32 9MB ...
    cot_21            (number_of_lines, pixels_per_line) float32 9MB ...
    cwp_21            (number_of_lines, pixels_per_line) float32 9MB ...
    ...                ...
    cth_dctp          (number_of_lines, pixels_per_line) float32 9MB ...
    cth_dctt          (number_of_lines, pixels_per_line) float32 9MB ...
    cth_dalb          (number_of_lines, pixels_per_line) float32 9MB ...
    ice_cloud         (number_of_lines, pixels_per_line) float32 9MB ...
    water_cloud       (number_of_lines, pixels_per_line) float32 9MB ...
    l2_flags          (number_of_lines, pixels_per_line) int32 9MB ...

3. Understanding PACE-OCI L2 CLOUD Variables#

PACE-OCI CLOUD products provide various cloud optical and microphysical properties. The product structure includes multiple variables with descriptive metadata. Given the early stage of the product, improvements and changes are expected in future versions.

We’ll use our own print_variable_description function to inspect the dataset.

help(print_variable_description)
Help on function print_variable_description in module __main__:

print_variable_description(dataset, variables_pattern=None)
    Print a table of variables, units, and descriptions from a dataset.

    Long descriptions are wrapped to 100 characters; only the first line
    prints the variable name and units.

    Parameters
    ----------
    dataset : xarray.Dataset
        Dataset containing variables whose metadata are stored in `attrs`
        (e.g., `units` and `long_name`).
    variables_pattern : str, optional
        Pattern to filter variable names. If None, all variables are shown.

    Returns
    -----
    pandas.DataFrame
        Table describing variables in the dataset

Cloud Optical Properties#

Cloud optical thickness and effective radius are fundamental properties retrieved from multi-spectral observations.

print_variable_description(dataset, "cot")
Units Description
cot_22 none Cloud Optical Thickness at 670 nm two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cot_21 none Cloud Optical Thickness at 670 nm two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cot_16 none Cloud Optical Thickness at 670 nm two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cth_cot Cloud optical thickness at 760 nm from cloud top height algorithm"
cth_dcot Estimated 1-sigma uncertainty on retrieved COT at 760 nm from cloud top height algorithm
print_variable_description(dataset, "cer")
Units Description
cer_22 micron Cloud Particle Effective Radius two-channel retrieval using the 2.2 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cer_21 micron Cloud Particle Effective Radius two-channel retrieval using the 2.1 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cer_16 micron Cloud Particle Effective Radius two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)

Cloud Phase and Other Properties#

Additional variables include cloud phase information and other cloud properties.

print_variable_description(dataset)
Units Description
cer_22 micron Cloud Particle Effective Radius two-channel retrieval using the 2.2 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cot_22 none Cloud Optical Thickness at 670 nm two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cwp_22 g/m^2 Cloud Column Water Path two-channel retrieval using the 2.2 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cer_21 micron Cloud Particle Effective Radius two-channel retrieval using the 2.1 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cot_21 none Cloud Optical Thickness at 670 nm two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cwp_21 g/m^2 Cloud Column Water Path two-channel retrieval using the 2.1 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cer_16 micron Cloud Particle Effective Radius two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cot_16 none Cloud Optical Thickness at 670 nm two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
cwp_16 g/m^2 Cloud Column Water Path two-channel retrieval using the 1.6 um band and either the 0.65 um, 0.86 um, or 1.2 um band (specified in cld_non_abs_band)
ctp mbar Air pressure at the cloud top
ctt degree K Air temperature at the cloud top
cth kilometers Height at the cloud top
cld_phase_21 Index of the cloud phase derived for the 2100 nm products: 0 - no cloud mask, 1 - clear, 2 - water cloud, 3 - ice cloud, 4 - unknown, assume water cloud
cld_phase_22 Index of the cloud phase derived for the 2200 nm products: 0 - no cloud mask, 1 - clear, 2 - water cloud, 3 - ice cloud, 4 - unknown, assume water cloud
cld_phase_16 Index of the cloud phase derived for the 1600 nm products: 0 - no cloud mask, 1 - clear, 2 - water cloud, 3 - ice cloud, 4 - unknown, assume water cloud
cld_non_abs_band Non-absorbing band used for 2-channel cloud retrieval: 0 - none, 1 - around 650 nm (for land surface), 2 = around 860 (for water surface), 3 - around 1200 (ice/snow surface)
cth_phase none Cloud phase determined by cloud top height algorithm: 0 - water, 1 - ice
cth_cot Cloud optical thickness at 760 nm from cloud top height algorithm"
cth_cost Retrieval cost at solution for best-fit cloud optical model
cth_iter Number of iterations to convergence for best-fit cloud optical model
cth_alb Best surface albedo made by cloud top height algorithm
cth_dcot Estimated 1-sigma uncertainty on retrieved COT at 760 nm from cloud top height algorithm
cth_dcth kilometers Estimated 1sigma uncertainty on retrieved CTH for best-fit cloud optical model, km
cth_dctp kilometers Estimated 1sigma uncertainty on retrieved CTP for best-fit cloud optical model, mb
cth_dctt degree K Estimated 1sigma uncertainty on retrieved CTT for best-fit cloud optical model, K
cth_dalb Estimated 1sigma uncertainty on retrieved surface albedo for best-fit cloud optical model
ice_cloud Ice cloud fraction for this pixel: 0 - none, 1 - cloud
water_cloud Water cloud fraction for this pixel: 0 - none, 1 - cloud
l2_flags Level-2 Processing Flags

4. Visualizing Variables#

To visualize the L2 variables, we plot them on cartopy GeoAxes dressed up with our geo_axis_tags function with the color range limited by our extremes_removed_limits function.

help(geo_axis_tags)
Help on function geo_axis_tags in module __main__:

geo_axis_tags(ax, crs=None)
    Add coastlines and labeled latitude/longitude gridlines to a Cartopy axis.

    Parameters
    ----------
    ax : cartopy.mpl.geoaxes.GeoAxes
        Axes object on which to draw the gridlines and coastlines.
    crs : cartopy.crs.CRS, optional
        Coordinate reference system used for the gridlines.
        Default is a Plate Carrée projection with central longitude = 0.
help(extremes_removed_limits)
Help on function extremes_removed_limits in module __main__:

extremes_removed_limits(array)
    Return suggested min and max axis limits based on the interquartile range (IQR) rule.

    Parameters
    ----------
    array : array-like
        Input numeric array.

    Returns
    -------
    tuple
        Suggested min and max for axis limits in plots

Cloud Effective Radius#

Visualize the cloud effective radius retrieved from OCI observations. The colorbar limits are adjusted based on the range of observed effective radii, excluding extreme values.

fig, ax = plt.subplots(figsize=(10, 6), subplot_kw={"projection": projection})
var = "cer_21"
array = dataset[var]
vmin, vmax = extremes_removed_limits(array)
cmap = plt.get_cmap("viridis", 20)
img = array.plot.pcolormesh(
    x="longitude",
    y="latitude",
    cmap=cmap,
    vmin=vmin,
    vmax=vmax,
)
img.colorbar.set_label(array.attrs.get("units", ""))
ax.set_title(var, size=12)
geo_axis_tags(ax, projection)
plt.show()
../../_images/d5abd7fae5ded9ff72171305656d378e2fb7eec3fd5d4d044f58c815fb23c7c5.png

Cloud Optical Thickness#

Cloud optical thickness provides information about the opacity of clouds to solar radiation.

fig, ax = plt.subplots(figsize=(10, 6), subplot_kw={"projection": projection})
var = "cot_22"
array = dataset[var]
vmin, vmax = extremes_removed_limits(array)
cmap = plt.get_cmap("viridis", 20)
img = array.plot.pcolormesh(
    x="longitude",
    y="latitude",
    cmap=cmap,
    vmin=vmin,
    vmax=vmax,
)
img.colorbar.set_label(array.attrs.get("units", ""))
ax.set_title(var, size=12)
geo_axis_tags(ax, projection)
plt.show()
../../_images/cb573dd5e84b8189bcff032947c48d9bc764112eb3b52865bb839fef54e5e990.png

Cloud Phase#

Cloud phase classification distinguishes between liquid and ice clouds. These variables are discrete, also known as categorical, which we visualize with our plot_cloud_phase function.

help(plot_cloud_phase)
Help on function plot_cloud_phase in module __main__:

plot_cloud_phase(dataset, name, ax)
    Plot a map of cloud phase, a categorical variable, on a cartopy GeoAxes

    Parameters
    ----------
    dataset: xarray.Dataset
        The granule with cloud phase variables
    name: str
        The name of a variable with categorical values
    ax : cartopy.mpl.geoaxes.GeoAxes
        Axes object on which to draw the gridlines and coastlines.
fig, ax = plt.subplots(figsize=(10, 6), subplot_kw={"projection": projection})
plot_cloud_phase(dataset, "cld_phase_21", ax)
plt.show()
../../_images/596777cb9bc53b57ae4510adcb76cd02aaf304c9dc35a6373c6d0b2007ad7182.png

5. Reference#

  • Platnick, S., Meyer, K., King, M. D., Wind, G., Amarasinghe, N., Marchant, B., Arnold, G. T., Zhang, Z., Hubanks, P. A., Ridgway, B., & Riedi, J. (2017). The MODIS Cloud Optical and Microphysical Products: Collection 6 Updates and Examples From Terra and Aqua. IEEE Transactions on Geoscience and Remote Sensing, 55(1), 502–525. https://doi.org/10.1109/TGRS.2016.2610522

  • Sayer, A. M., Cairns, B., Knobelspiesse, K. D., Lelli, L., Rajapakshe, C., Giangrande, S. E., Thomas, G. E., and Zhang, D.: Evaluation of cloud height, optical thickness, and phase retrievals from the CHROMA algorithm applied to Sentinel-3 OLCI data, Atmos. Meas. Tech., 18, 6681–6703, https://doi.org/10.5194/amt-18-6681-2025, 2025.