Explore HARP2 L2 V4.0 Land Products (FastMAPOL)#
Authors: Meng Gao (NASA/SSAI), Skye Caplan (NASA/SSAI)
Summary#
This notebook explores the HARP2 Level-2 (L2) land products generated by FastMAPOL. For more detailed information about the algorithm and data products, please refer to the FastMAPOL ATBD, including the data product list & land surface model.
You should already be familiar with exploring the FastMAPOL aerosol products from the other notebooks. Beginning with Version 4.0, FastMAPOL provides a new HARP2 aerosol and land surface product under the MAPOL_LAND data suite. This product is still undergoing evaluation and should be used with caution, as the retrievals may exhibit significant biases.
In this notebook, we focus on the retrieved land surface reflectance and its spectral and angular dependencies.
How to Cite#
If you use the PACE HARP2 MAPOL_LAND Version 4.0 data, please refer to the dataset information page for the latest citation and DOI:
https://www.earthdata.nasa.gov/data/catalog/ob-cloud-pace-harp2-l2-mapol-land-4.0
The dataset may be cited as:
NASA Ocean Biology Processing Group. (2026). PACE HARP2 Level-2 Regional Aerosol Over LAND Optical Properties, FastMAPOL Algorithm, Version 4.0. NASA Ocean Biology Distributed Active Archive Center (OB.DAAC). DOI: 10.5067/PACE/HARP2/L2/MAPOL_LAND/4.0. Accessed on: YYYY-MM-DD.
Learning Objectives#
By the end of this notebook, you will understand:
How to acquire HARP2 L2 data
What land surface products are available
How to visualize land surface reflectance
How to examine the angular dependence of surface reflectance and BRDF correction
How to evaluate basic retrieval quality metrics
1. Setup#
Begin by importing all the packages used in this notebook. If your kernel uses an environment defined following the guidance in the tutorials, the imports should be successful.
import math
from pathlib import Path
import earthaccess
import numpy as np
import pandas as pd
import xarray as xr
auth = earthaccess.login(persist=True)
2. Get Level-2 Data#
HARP2 L2 data are available through both OB.DAAC and the Earthdata Cloud. Please refer to the L1C notebook for additional information on accessing data from the cloud. The following example retrieves a single HARP2 L2 MAPOL_LAND Version 4.0 granule.
Download the HARP2 MAPOL_LAND data over Rail Road Valley (38.4958, -115.6964) as one of the validaiton site part of RadCalNet. Further analysis on spectral and anglar information will be discussed in later section.
results = earthaccess.search_data(
short_name="PACE_HARP2_L2_MAPOL_LAND",
temporal=("2024-09-28T19:55:26", "2024-09-28T19:55:27"),
granule_name='*V4_0*',
count=1,
)
paths = earthaccess.open(results)
datatree = xr.open_datatree(paths[0])
datatree
<xarray.DataTree>
Group: /
│ Attributes: (12/38)
│ title: PACE HARP2 Level-2 aerosol and land product
│ platform: PACE
│ instrument: HARP2
│ startDirection: Ascending
│ endDirection: Ascending
│ product_name: PACE_HARP2.20240928T195526.L2.MAPOL_LAND.V4_0.nc
│ ... ...
│ time_coverage_start: 2024-09-28T19:55:26Z
│ time_coverage_end: 2024-09-28T20:00:26Z
│ reference: https://doi.org/10.5194/amt-16-5863-2023
│ comments:
│ sun_earth_distance: 1.001814
│ day_night_flag: Day
├── Group: /geolocation_data
│ Dimensions: (number_of_lines: 395, pixels_per_line: 519)
│ Dimensions without coordinates: number_of_lines, pixels_per_line
│ Data variables:
│ latitude (number_of_lines, pixels_per_line) float32 820kB ...
│ longitude (number_of_lines, pixels_per_line) float32 820kB ...
├── Group: /geophysical_data
│ Dimensions: (number_of_lines: 395, pixels_per_line: 519,
│ wavelength: 4, number_of_views: 90,
│ intensity_bands_per_view: 1)
│ Dimensions without coordinates: number_of_lines, pixels_per_line, wavelength,
│ number_of_views, intensity_bands_per_view
│ Data variables: (12/46)
│ alh (number_of_lines, pixels_per_line) float32 820kB ...
│ vd_mode1 (number_of_lines, pixels_per_line) float32 820kB ...
│ vd_mode2 (number_of_lines, pixels_per_line) float32 820kB ...
│ vd_mode3 (number_of_lines, pixels_per_line) float32 820kB ...
│ vd_mode4 (number_of_lines, pixels_per_line) float32 820kB ...
│ vd_mode5 (number_of_lines, pixels_per_line) float32 820kB ...
│ ... ...
│ rhos_angular_std (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
│ rhos_nadir_mean (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
│ rhos_nadir_std (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
│ rhos_angular (number_of_lines, pixels_per_line, number_of_views, intensity_bands_per_view) float32 74MB ...
│ rhos_nadir (number_of_lines, pixels_per_line, number_of_views, intensity_bands_per_view) float32 74MB ...
│ land_ndvi (number_of_lines, pixels_per_line) float32 820kB ...
├── Group: /diagnostic_data
│ Dimensions: (number_of_lines: 395, pixels_per_line: 519,
│ number_of_views: 90, intensity_bands_per_view: 1,
│ polarization_bands_per_view: 1)
│ Dimensions without coordinates: number_of_lines, pixels_per_line,
│ number_of_views, intensity_bands_per_view,
│ polarization_bands_per_view
│ Data variables:
│ chi2 (number_of_lines, pixels_per_line) float32 820kB ...
│ chi2_first_guess (number_of_lines, pixels_per_line) float32 820kB ...
│ timing (number_of_lines, pixels_per_line) float32 820kB ...
│ nv_ref (number_of_lines, pixels_per_line) float64 2MB ...
│ nv_dolp (number_of_lines, pixels_per_line) float64 2MB ...
│ nfev (number_of_lines, pixels_per_line) float64 2MB ...
│ njev (number_of_lines, pixels_per_line) float64 2MB ...
│ quality_flag (number_of_lines, pixels_per_line) float64 2MB ...
│ ozone (number_of_lines, pixels_per_line) float32 820kB ...
│ surface_pressure (number_of_lines, pixels_per_line) float32 820kB ...
│ mask_ref (number_of_lines, pixels_per_line, number_of_views, intensity_bands_per_view) float32 74MB ...
│ mask_dolp (number_of_lines, pixels_per_line, number_of_views, polarization_bands_per_view) float32 74MB ...
├── Group: /processing_control
│ └── Group: /processing_control/input_parameters
│ Attributes: (12/141)
│ version: v4.6_var1
│ surface: land
│ prefix: global_
│ global_title: PACE HARP2 Level-2 aerosol and land p...
│ global_comments:
│ global_history:
│ ... ...
│ nall: 90
│ number_of_views: 90
│ wavelength: 4
│ polarization_bands_per_view: 1
│ intensity_bands_per_view: 1
│ sensor_view_angle: [ 56.94 43.98 33.22 20.75 6.6 ...
└── Group: /sensor_band_parameters
Dimensions: (number_of_views: 90, intensity_bands_per_view: 1,
wavelength: 4)
Coordinates:
* wavelength (wavelength) float32 16B 440.2 549.5 664.6 865.3
Dimensions without coordinates: number_of_views, intensity_bands_per_view
Data variables:
intensity_wavelength (number_of_views, intensity_bands_per_view) float32 360B ...
sensor_view_angle (number_of_views) float32 360B ...Here, we merge all data groups for convenience in subsequent data manipulation and mark the latitude and longitude variables as coordinates.
dataset = xr.merge(datatree.to_dict().values())
dataset=dataset.set_coords(("latitude", "longitude"))
dataset
<xarray.Dataset> Size: 397MB
Dimensions: (number_of_lines: 395, pixels_per_line: 519,
wavelength: 4, number_of_views: 90,
intensity_bands_per_view: 1,
polarization_bands_per_view: 1)
Coordinates:
latitude (number_of_lines, pixels_per_line) float32 820kB ...
longitude (number_of_lines, pixels_per_line) float32 820kB ...
* wavelength (wavelength) float32 16B 440.2 549.5 664.6 865.3
Dimensions without coordinates: number_of_lines, pixels_per_line,
number_of_views, intensity_bands_per_view,
polarization_bands_per_view
Data variables: (12/60)
alh (number_of_lines, pixels_per_line) float32 820kB ...
vd_mode1 (number_of_lines, pixels_per_line) float32 820kB ...
vd_mode2 (number_of_lines, pixels_per_line) float32 820kB ...
vd_mode3 (number_of_lines, pixels_per_line) float32 820kB ...
vd_mode4 (number_of_lines, pixels_per_line) float32 820kB ...
vd_mode5 (number_of_lines, pixels_per_line) float32 820kB ...
... ...
ozone (number_of_lines, pixels_per_line) float32 820kB ...
surface_pressure (number_of_lines, pixels_per_line) float32 820kB ...
mask_ref (number_of_lines, pixels_per_line, number_of_views, intensity_bands_per_view) float32 74MB ...
mask_dolp (number_of_lines, pixels_per_line, number_of_views, polarization_bands_per_view) float32 74MB ...
intensity_wavelength (number_of_views, intensity_bands_per_view) float32 360B ...
sensor_view_angle (number_of_views) float32 360B ...
Attributes: (12/38)
title: PACE HARP2 Level-2 aerosol and land product
platform: PACE
instrument: HARP2
startDirection: Ascending
endDirection: Ascending
product_name: PACE_HARP2.20240928T195526.L2.MAPOL_LAND.V4_0.nc
... ...
time_coverage_start: 2024-09-28T19:55:26Z
time_coverage_end: 2024-09-28T20:00:26Z
reference: https://doi.org/10.5194/amt-16-5863-2023
comments:
sun_earth_distance: 1.001814
day_night_flag: Day3. Understanding the HARP2 L2 Product Structure#
The HARP2 FastMAPOL L2 MAPOL_LAND product includes aerosol optical properties for both fine and coarse aerosol modes, as well as retrieved land surface properties.
Land surface product variables include:
Land surface reflectance (
rhos*)BRDF parameters based on the Ross–Li model
Land NDVI
Land white-sky albedo
Multi-angle land surface reflectance is derived by applying atmospheric correction independently to each HARP2 viewing angle. rhos_angular represents the retrieved surface reflectance before BRDF correction, while rhos_nadir represents the reflectance after BRDF correction and adjustment to nadir viewing geometry. Angular means and standard deviations are also provided as rhos_nadir_mean, rhos_nadir_std, rhos_angular_mean, and rhos_angular_std.
The following lists variables related to the land surface model (names beginning with land) and land surface reflectance (names beginning with rhos).
vars_keep = [
v for v in dataset.data_vars
if v.lower().startswith(("land", "rhos"))
]
dataset_sub = dataset[vars_keep]
dataset_sub
<xarray.Dataset> Size: 172MB
Dimensions: (number_of_lines: 395, pixels_per_line: 519,
wavelength: 4, number_of_views: 90,
intensity_bands_per_view: 1)
Coordinates:
latitude (number_of_lines, pixels_per_line) float32 820kB ...
longitude (number_of_lines, pixels_per_line) float32 820kB ...
* wavelength (wavelength) float32 16B 440.2 549.5 664.6 865.3
Dimensions without coordinates: number_of_lines, pixels_per_line,
number_of_views, intensity_bands_per_view
Data variables:
land_kvol (number_of_lines, pixels_per_line) float32 820kB ...
land_kgeo (number_of_lines, pixels_per_line) float32 820kB ...
land_bpol (number_of_lines, pixels_per_line) float32 820kB ...
land_fiso (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
land_white_sky_albedo (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
rhos_angular_mean (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
rhos_angular_std (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
rhos_nadir_mean (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
rhos_nadir_std (number_of_lines, pixels_per_line, wavelength) float32 3MB ...
rhos_angular (number_of_lines, pixels_per_line, number_of_views, intensity_bands_per_view) float32 74MB ...
rhos_nadir (number_of_lines, pixels_per_line, number_of_views, intensity_bands_per_view) float32 74MB ...
land_ndvi (number_of_lines, pixels_per_line) float32 820kB ...
Attributes: (12/38)
title: PACE HARP2 Level-2 aerosol and land product
platform: PACE
instrument: HARP2
startDirection: Ascending
endDirection: Ascending
product_name: PACE_HARP2.20240928T195526.L2.MAPOL_LAND.V4_0.nc
... ...
time_coverage_start: 2024-09-28T19:55:26Z
time_coverage_end: 2024-09-28T20:00:26Z
reference: https://doi.org/10.5194/amt-16-5863-2023
comments:
sun_earth_distance: 1.001814
day_night_flag: Day4. Visualize HARP2 L2 Land Surface Properties#
In this example, we visualize the retrieved land surface reflectance. We first read the angular means and standard deviations before and after BRDF correction.
rhos_angular_mean = dataset["rhos_angular_mean"].values
rhos_angular_std = dataset["rhos_angular_std"].values
rhos_nadir_mean = dataset["rhos_nadir_mean"].values
rhos_nadir_std = dataset["rhos_nadir_std"].values
rhos_nadir_mean.shape
(395, 519, 4)
We also read the spatial coordinates and wavelength information.
lat = dataset["latitude"].values
lon = dataset["longitude"].values
plot_range = [lon.min(), lon.max(), lat.min(), lat.max()]
wavelength = dataset["wavelength"].values
wavelength
array([440.159, 549.465, 664.564, 865.283], dtype=float32)
Define helper functions for visualization#
The following helper functions are used to visualize single-band and RGB maps, examine the angular dependence of surface reflectance, and calculate reflectance ratios between selected viewing angles.
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
import cartopy.crs as ccrs
import cartopy.feature as cfeature
def _prepare_rgba(data, vmin=None, vmax=None):
"""Normalize RGB/RGBA data and make invalid pixels transparent."""
data = np.asarray(data, float)
rgb = data[..., :3].copy()
alpha = data[..., 3].copy() if data.shape[-1] == 4 else np.ones(data.shape[:2])
valid = np.all(np.isfinite(rgb), axis=-1) & np.isfinite(alpha)
if vmin is not None or vmax is not None:
vmin1 = 0 if vmin is None else vmin
vmax1 = np.nanmax(rgb) if vmax is None else vmax
rgb = (rgb - vmin1) / (vmax1 - vmin1)
rgb = np.clip(np.nan_to_num(rgb), 0, 1)
alpha = np.where(valid, alpha, 0)
return np.dstack([rgb, np.clip(alpha, 0, 1)])
def plot_l2_product(
lon, lat, data, plot_range, label="", title="",
vmin=None, vmax=None, figsize=(12, 4),
cmap="viridis", log_scale=False,
land_color="#f2efe9", ocean_color="#dbe9f6",
):
"""Plot scalar map + histogram, or RGB/RGBA map."""
data = np.asarray(data)
# Remove single-band dimension
if data.ndim == 3 and data.shape[-1] == 1:
data = data[..., 0]
is_rgb = data.ndim == 3 and data.shape[-1] in (3, 4)
# ---------------- Figure ----------------
if is_rgb:
fig, ax = plt.subplots(
figsize=(figsize[0] * 0.75, figsize[1]),
subplot_kw={"projection": ccrs.PlateCarree()},
)
else:
if data.ndim != 2:
raise ValueError("data must be 2D scalar or 3/4-band RGB.")
values = data[np.isfinite(data)]
if log_scale:
values = values[values > 0]
if values.size == 0:
raise ValueError("No valid data.")
vmin = np.percentile(values, 2) if vmin is None else vmin
vmax = np.percentile(values, 98) if vmax is None else vmax
fig = plt.figure(figsize=figsize)
gs = fig.add_gridspec(1, 2, width_ratios=[3, 1], wspace=0.3)
ax = fig.add_subplot(gs[0], projection=ccrs.PlateCarree())
# ---------------- Background ----------------
ax.set_extent(plot_range, crs=ccrs.PlateCarree())
ax.add_feature(cfeature.OCEAN, facecolor=ocean_color, zorder=0)
ax.add_feature(cfeature.LAND, facecolor=land_color, zorder=0)
# ---------------- RGB ----------------
if is_rgb:
ax.pcolormesh(
lon, lat, data,
shading="nearest",
transform=ccrs.PlateCarree(),
zorder=1,
)
# ---------------- Scalar ----------------
else:
norm = LogNorm(vmin=vmin, vmax=vmax) if log_scale else None
pm = ax.pcolormesh(
lon, lat, data,
cmap=cmap,
norm=norm,
vmin=None if log_scale else vmin,
vmax=None if log_scale else vmax,
shading="auto",
transform=ccrs.PlateCarree(),
zorder=1,
)
plt.colorbar(pm, ax=ax, pad=0.1).set_label(label)
# Histogram
axh = fig.add_subplot(gs[1])
bins = (
np.logspace(np.log10(vmin), np.log10(vmax), 40)
if log_scale else 40
)
axh.hist(
values,
bins=bins,
range=None if log_scale else (vmin, vmax),
color="gray",
edgecolor="black",
)
if log_scale:
axh.set_xscale("log")
axh.set(
xlabel=label,
ylabel="Count",
title=f"Histogram: N={values.size}",
)
# ---------------- Decoration ----------------
ax.coastlines(resolution="110m", linewidth=0.8, zorder=3)
ax.gridlines(draw_labels=True, zorder=4)
ax.set_title(title)
plt.show()
########### Plot angular view for a pixel (or within a box)
def plot_rhos_angular(
dataset, var, iy, ix, n=2,
title=None, ylim=None
):
rhos = dataset[var].values[..., 0]
angle = dataset["sensor_view_angle"].values
wv = dataset["intensity_wavelength"].values.squeeze()
# Spatial mean/std
box = rhos[iy-n:iy+n+1, ix-n:ix+n+1]
mean = np.nanmean(box, axis=(0, 1))
std = np.nanstd(box, axis=(0, 1))
center = rhos[iy, ix]
# Group by nominal wavelength
wv_group = np.round(wv)
wv_unique = np.unique(wv_group[np.isfinite(wv_group)])
ncols = 2
nrows = math.ceil(len(wv_unique) / ncols)
fig, axes = plt.subplots(
nrows, ncols,
figsize=(10, 3.5*nrows),
sharex=True
)
axes = np.atleast_1d(axes).ravel()
for ax, wv1 in zip(axes, wv_unique):
ind = wv_group == wv1
order = np.argsort(angle[ind])
a = angle[ind][order]
m = mean[ind][order]
s = std[ind][order]
c = center[ind][order]
ax.fill_between(a, m-s, m+s, alpha=0.25)
ax.plot(a, m, "o-", label=f"{2*n+1}×{2*n+1} mean")
ax.plot(a, c, "o--", label="Center pixel")
ax.set_title(f"{wv1:.0f} nm")
ax.set_xlabel("Sensor View Angle (°)")
ax.set_ylabel(r"$\rho_s$")
ax.grid(alpha=0.3)
if ylim is not None:
ax.set_ylim(ylim)
# Remove unused panels
for ax in axes[len(wv_unique):]:
ax.remove()
axes[0].legend()
fig.suptitle(
title or
f"{var}\n"
f"({dataset['latitude'].values[iy, ix]:.4f}°, "
f"{dataset['longitude'].values[iy, ix]:.4f}°)"
)
plt.tight_layout()
plt.show()
########### Compute ratios between two angles
def rhos_angle_ratio(dataset, var, iy, ix, angle1, angle2):
rhos = dataset[var].values[..., 0]
angle = dataset["sensor_view_angle"].values
wv = np.round(dataset["intensity_wavelength"].values.squeeze())
result = []
for wv1 in np.unique(wv[np.isfinite(wv)]):
ind = (wv == wv1)
# Nearest observation within this wavelength band
j1 = np.where(ind)[0][np.argmin(np.abs(angle[ind] - angle1))]
j2 = np.where(ind)[0][np.argmin(np.abs(angle[ind] - angle2))]
r1 = rhos[iy, ix, j1]
r2 = rhos[iy, ix, j2]
result.append([
wv1,
angle[j1],
angle[j2],
r1,
r2,
r2 / r1
])
return pd.DataFrame(
result,
columns=[
"wavelength",
"angle1",
"angle2",
"rhos1",
"rhos2",
"ratio"
]
)
Visualizations#
The following example shows land surface reflectance at a single wavelength. Note that rhos_nadir_mean represents the default reflectance to play with, which is the land surface reflectance after BRDF correction and nadir adjustment, followed by averaging over the available viewing angles per band.
wavelength_index = 1
title = "Land surface reflectance: " + str(wavelength[wavelength_index]) + " nm"
label = "Rhos"
data = rhos_nadir_mean[:, :, wavelength_index]
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=0.5,
cmap="viridis"
)
The following example creates an RGB composite of the land surface reflectance. The selected bands are reordered from BGR to RGB, negative reflectances are clipped, and a gamma adjustment is applied for visualization.
# BGR -> RGB
data_rgb = rhos_nadir_mean[:, :, [2, 1, 0]].copy()
# Valid pixels
valid = np.all(np.isfinite(data_rgb), axis=-1)
# Gamma correction and scaling
data_rgb = np.clip(data_rgb, 0, None) ** 0.6
data_rgb = np.clip(data_rgb / 0.5, 0, 1)
# Add transparency
data_rgba = np.dstack([
data_rgb,
valid.astype(float),
])
plot_l2_product(
lon,
lat,
data_rgba,
plot_range=plot_range,
title="Land surface reflectance (RGB)",
)
5. Spectral Surface Reflectance#
The HARP2 land product also provides spectral information. Here, we examine a location near Railroad Valley at approximately 38.4958°N, 115.6964°W.
Find the center pixel#
The nearest HARP2 pixel to the target latitude and longitude is identified from the two-dimensional geolocation arrays.
lat_target = 38.4958
lon_target = -115.6964
dist2 = (lat - lat_target)**2 + (lon - lon_target)**2
iy, ix = np.unravel_index(np.nanargmin(dist2), dist2.shape)
print("Index:", iy, ix)
print("Lat/Lon:", lat[iy, ix], lon[iy, ix])
Index: 394 192
Lat/Lon: 36.775658 -115.321365
Extract the reflectance#
To explore the influence of pixel-to-pixel variability, we calculate the mean and standard deviation within a ±n-pixel neighborhood, corresponding to a (2n+1) × (2n+1) pixel box centered on the target location. The center-pixel reflectance is retained for comparison. The variability within the box can be used to assess the homogeneity of the surface properties.
rhos = dataset["rhos_nadir_mean"].values
# explore near by pixels
n = 1
npixel = 2*n+1
y0 = max(0, iy - n)
y1 = min(rhos.shape[0], iy + n + 1)
x0 = max(0, ix - n)
x1 = min(rhos.shape[1], ix + n + 1)
# shape: (ny, nx, wavelength)
rhos_box = rhos[y0:y1, x0:x1, :]
# Center pixel
rhos_target = rhos[iy, ix, :]
print("center pixel rhos:", rhos_target)
# Spatial statistics over the 5x5 neighborhood
rhos_mean = np.nanmean(rhos_box, axis=(0, 1))
rhos_std = np.nanstd(rhos_box, axis=(0, 1))
print("average pixel rhos:", rhos_mean)
center pixel rhos: [0.12967816 0.19959638 0.2597795 0.3515005 ]
average pixel rhos: [0.12498599 0.19541222 0.2492357 0.3379093 ]
Plot the reflectance spectrum#
The figure compares the center-pixel spectrum with the 5 × 5 spatial mean. The shaded region represents ±1 standard deviation within the neighborhood.
plt.figure(figsize=(7, 4))
# Variation among neighboring pixels
plt.fill_between(
wavelength,
rhos_mean - rhos_std,
rhos_mean + rhos_std,
alpha=0.25,
label=r"$\pm$1 STD"
)
# Neighborhood mean
plt.plot(
wavelength, rhos_mean,
"o-", lw=2,
label=f"{npixel}×{npixel} mean"
)
# Center pixel
plt.plot(
wavelength, rhos_target,
"o--", lw=1.5,
label="Center pixel"
)
plt.xlabel("Wavelength (nm)")
plt.ylabel(r"Surface Reflectance, $\rho_s$")
plt.title(
f"Land Surface Reflectance\n"
f"({lat[iy, ix]:.4f}°, {lon[iy, ix]:.4f}°)"
)
plt.ylim(bottom=0)
plt.grid(alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
6. Analyze the Angular Dependence#
The HARP2 land product preserves multi-angle information, allowing the angular dependence of the retrieved surface reflectance to be examined directly. Here, we again use the location near Railroad Valley.
rhos_angular represents surface reflectance derived independently at each viewing angle before BRDF correction. The figure below shows the angular variation for each spectral band, together with the spatial variability within the 5 × 5 neighborhood.
plot_rhos_angular(
dataset,
"rhos_angular",
iy, ix,
title="Angular Surface Reflectance (Before BRDF Correction)",
ylim=(0, 0.4)
)
We can also compare the surface reflectance at two selected viewing angles. The ratio between the two angles provides a simple measure of angular variation. Comparing this ratio among spectral bands can help assess whether the angular dependence is spectrally consistent.
The ratio reported below is defined as the reflectance at angle2 divided by the reflectance at angle1.
angle1, angle2 = 0, 30
ratio = rhos_angle_ratio(
dataset,
"rhos_angular",
iy, ix,
angle1=angle1,
angle2=angle2
)
ratio
| wavelength | angle1 | angle2 | rhos1 | rhos2 | ratio | |
|---|---|---|---|---|---|---|
| 0 | 440.0 | -5.29 | 35.040001 | 0.095634 | 0.111968 | 1.170801 |
| 1 | 549.0 | 6.60 | 33.220001 | 0.135480 | 0.161774 | 1.194085 |
| 2 | 665.0 | 0.60 | 30.180000 | 0.172352 | 0.192212 | 1.115228 |
| 3 | 865.0 | 4.20 | 31.070000 | 0.225939 | 0.256636 | 1.135868 |
The reverse ratio can be calculated by exchanging the two viewing angles.
angle1, angle2 = 30, 0
ratio = rhos_angle_ratio(
dataset,
"rhos_angular",
iy, ix,
angle1=angle1,
angle2=angle2
)
ratio
| wavelength | angle1 | angle2 | rhos1 | rhos2 | ratio | |
|---|---|---|---|---|---|---|
| 0 | 440.0 | 35.040001 | -5.29 | 0.111968 | 0.095634 | 0.854116 |
| 1 | 549.0 | 33.220001 | 6.60 | 0.161774 | 0.135480 | 0.837461 |
| 2 | 665.0 | 30.180000 | 0.60 | 0.192212 | 0.172352 | 0.896677 |
| 3 | 865.0 | 31.070000 | 4.20 | 0.256636 | 0.225939 | 0.880384 |
7. Angular Correction#
Angular correction is applied to rhos_angular to produce the nadir-adjusted surface reflectance, rhos_nadir. This remains an experimental product under evaluation.
Comparing rhos_nadir with rhos_angular provides a direct way to examine how the angular correction reduces the angular dependence of the retrieved surface reflectance.
plot_rhos_angular(
dataset,
"rhos_nadir",
iy, ix,
title="Angular Surface Reflectance (After BRDF Correction)",
ylim=(0, 0.4)
)
The reflectance ratios between selected viewing angles can also be recalculated using rhos_nadir. Ratios closer to unity after correction indicate reduced angular dependence.
angle1, angle2 = 0, 30
ratio = rhos_angle_ratio(
dataset,
"rhos_nadir",
iy, ix,
angle1=angle1,
angle2=angle2
)
ratio
| wavelength | angle1 | angle2 | rhos1 | rhos2 | ratio | |
|---|---|---|---|---|---|---|
| 0 | 440.0 | -5.29 | 35.040001 | 0.140894 | 0.140117 | 0.994483 |
| 1 | 549.0 | 6.60 | 33.220001 | 0.204868 | 0.206515 | 1.008040 |
| 2 | 665.0 | 0.60 | 30.180000 | 0.263601 | 0.252687 | 0.958598 |
| 3 | 865.0 | 4.20 | 31.070000 | 0.343762 | 0.340318 | 0.989983 |
angle1, angle2 = 30, 0
ratio = rhos_angle_ratio(
dataset,
"rhos_nadir",
iy, ix,
angle1=angle1,
angle2=angle2
)
ratio
| wavelength | angle1 | angle2 | rhos1 | rhos2 | ratio | |
|---|---|---|---|---|---|---|
| 0 | 440.0 | 35.040001 | -5.29 | 0.140117 | 0.140894 | 1.005548 |
| 1 | 549.0 | 33.220001 | 6.60 | 0.206515 | 0.204868 | 0.992024 |
| 2 | 665.0 | 30.180000 | 0.60 | 0.252687 | 0.263601 | 1.043190 |
| 3 | 865.0 | 31.070000 | 4.20 | 0.340318 | 0.343762 | 1.010118 |
For this example, the BRDF correction reduces a substantial portion of the angular variation, although residual angular dependence remains. The performance of this correction is still under evaluation.
8. Advanced: Quality Assessment#
As with the aerosol products, retrieval quality metrics are important for evaluating the land products, particularly when analyzing multi-angle information.
The primary metrics examined here are the retrieval cost function (chi2), the number of retained reflectance measurements (nv_ref), the number of retained DoLP (degree of linear polarization) measurements (nv_dolp), and the overall quality_flag. Please see the aerosol product tutorial or ATBD for more details.
chi2 = dataset["chi2"].values
nv_ref = dataset["nv_ref"].values
nv_dolp = dataset["nv_dolp"].values
quality_flag = dataset["quality_flag"].values
The retrieval cost function, \(\chi^2\), provides an overall measure of the agreement between the observations and the fitted forward model.
title = r"Retrieval cost function: $\chi^2$"
label = r"$\chi^2$"
data = chi2
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=3,
cmap="viridis"
)
nv_ref gives the number of reflectance measurements retained in the retrieval.
title = r"Total number of reflectance measurements"
label = r"$N_{ref}$"
data = nv_ref
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=90,
cmap="viridis"
)
nv_dolp gives the number of DoLP measurements retained in the retrieval.
title = r"Total number of DoLP measurements"
label = r"$N_{dolp}$"
data = nv_dolp
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=90,
cmap="viridis"
)
The following provides simple granule-level averages of these retrieval metrics.
np.nanmean(chi2), np.nanmean(nv_ref), np.nanmean(nv_dolp)
(np.float32(1.5042068),
np.float64(79.1215328027014),
np.float64(66.33074047274481))
The overall retrieval quality is summarized by quality_flag.
The min and max value of the quality flags:
np.nanmin(data), np.nanmax(data)
(np.float64(10.0), np.float64(90.0))
title = "Retrieval quality flag"
label = "quality_flag"
data = quality_flag
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=6,
cmap="viridis"
)
The quality_flag is determined using retrieval metrics including \(\chi^2\) and the number of retained measurements. In this example, only a relatively small fraction of the retrievals, primarily near the center of the swath, meet the criteria for the highest-quality category (quality_flag = 0). Future improvements in instrument calibration and retrieval performance may increase the fraction of retrievals meeting the highest-quality criteria.
9. Advanced: Multi-Angle Data Mask#
To better understand the fitting and screening of individual angular measurements, we can examine the adaptive data masks.
mask_ref indicates which reflectance measurements are retained or excluded during the retrieval, while mask_dolp provides the corresponding information for DoLP measurements.
mask_ref = dataset["mask_ref"].values
mask_dolp = dataset["mask_dolp"].values
mask_ref.shape, mask_dolp.shape
((395, 519, 90, 1), (395, 519, 90, 1))
The following example shows the reflectance data mask for one viewing-angle index.
angle_index = 5
title = "Adaptive data mask on reflectance: angle index " + str(angle_index)
label = "mask_ref"
data = mask_ref[:, :, angle_index, 0]
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=1,
cmap="viridis"
)
The corresponding DoLP data mask can be examined in the same way.
angle_index = 5
title = "Adaptive data mask on DoLP: angle index " + str(angle_index)
label = "mask_DOLP"
data = mask_dolp[:, :, angle_index, 0]
plot_l2_product(
lon, lat, data,
plot_range=plot_range,
label=label,
title=title,
vmin=0,
vmax=1,
cmap="viridis"
)
Examining these masks for individual viewing angles can help identify where angular observations have been screened from the inversion.
10. References#
[Algorithm] Gao, M., Franz, B. A., Zhai, P.-W., Knobelspiesse, K., Sayer, A. M., Xu, X., Martins, J. V., Cairns, B., Castellanos, P., Fu, G., Hannadige, N., Hasekamp, O., Hu, Y., Ibrahim, A., Patt, F., Puthukkudy, A., and Werdell, P. J.: Simultaneous retrieval of aerosol and ocean properties from PACE HARP2 with uncertainty assessment using cascading neural network radiative transfer models, Atmos. Meas. Tech., 16, 5863–5881, https://doi.org/10.5194/amt-16-5863-2023, 2023.