Overview¶
The Hyperspectral Thermal Emission Spectrometer (HyTES) is now available through NASA Earthdata and the Earthdata Cloud as airborne Facility Instrument data.
In this tutorial, we will use earthaccess to discover, access, and visualize HyTES data over a region of interest — the Langebaan Lagoon Marine Protected Area in South Africa, acquired during the BioSCape campaign.
The HyTES instrument is an airborne imaging spectrometer with 256 spectral channels between 7.5 and 12 micrometers in the thermal infrared (TIR) part of the electromagnetic spectrum and 512 pixels cross-track. HyTES provides high spatial and high spectral resolution data on surface temperature and emissivity. TIR data are used to measure land surface temperature (LST), which informs models of water flux from land surface through processes such as evapotranspiration and supports applications like point-source pollution detection and volcano monitoring.
Datasets Used¶
| Dataset | DOI | Concept ID |
|---|---|---|
| HyTES Level 1 Radiance, Facility Instrument Collection | Hook et al. (2026) | C4205447735-ORNL_CLOUD |
| HyTES L1 Geolocation, Facility Instrument Collection | Hook et al. (2026) | C4254189775-ORNL_CLOUD |
| HyTES: Level 2 Emissivity and Land Surface Temperature | Hulley et al. (2026) | C4247014847-ORNL_CLOUD |
Learning Objectives¶
Discover available HyTES datasets on NASA Earthdata using
earthaccessSearch for granules by bounding box and date range, and extract CMR footprint geometries to map flight lines
Stream HyTES L1 Radiance and L2 Land Surface Temperature data directly from the Earthdata Cloud
Visualize flight paths, L1 radiance bands, and L2 LST/emissivity
Prerequisites¶
A free NASA Earthdata Login account is required. See the prerequisites page for setup instructions.
Citation¶
Hook, S. J., Hulley, G. C., La, T. T., Rivera, G., Johnson, W. R., & Eng, B. T. (2026). HyTES Level 1 Radiance, Facility Instrument Collection (Version 1). ORNL DAAC. Hook et al. (2026)
Hook, S. J., Hulley, G. C., La, T. T., Rivera, G., Johnson, W. R., & Eng, B. T. (2026). HyTES: Level 2 Emissivity and Land Surface Temperature (Version 1). ORNL DAAC. Hulley et al. (2026)
Import Libraries¶
import earthaccess
import xarray as xr
import numpy as np
import pandas as pd
import geopandas as gpd
import hvplot.xarray
import hvplot.pandas
import holoviews as hv
import panel as pn
from os import path
hv.extension('bokeh')
import warnings
warnings.filterwarnings('ignore')
# ESRI World Imagery basemap
xyz = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
attr = "ESRI"Authenticate with NASA Earthdata¶
earthaccess.login() handles authentication with your NASA Earthdata Login credentials. It will look for stored credentials first, then prompt if not found. Credentials can be persisted with persist=True.
auth = earthaccess.login()Discover HyTES Datasets¶
We can use earthaccess.search_datasets() to list all HyTES datasets available on NASA Earthdata.
datasets = earthaccess.search_datasets(instrument="HyTES")
print(f"HyTES datasets found: {len(datasets)}\n")
for ds in datasets:
summary = ds.summary()
print(f" Short name : {summary.get('short-name', 'N/A')}")HyTES datasets found: 11
Short name : HyTES_Calibration_Data_2468
Short name : HyTES_L1_Geolocation_2459
Short name : HyTES_L1_Geolocation_2013_2016_2467
Short name : HyTES_L1_Flight_Path_2463
Short name : HyTES_L1_Radiance_Overlays_2466
Short name : HyTES_L1_Radiance_2462
Short name : HyTES_L1_Radiance_2013_2016_2476
Short name : HyTES_L2_Emissivity_Overlays_2469
Short name : HyTES_L2_Emissivity_LST_2460
Short name : SHIFT_HyTES_L1_2022_2245
Short name : SHIFT_HyTES_L2_2022_2246
Study Area¶
We will use the boundary of Langebaan Lagoon Marine Protected Area as our region of interest (ROI). Langebaan is a coastal conservation area in South Africa declared as a Ramsar Site. This area was surveyed during the 2023 BioSCape campaign.
A local GeoJSON of the study area polygon is available in the data/ directory.
# Load the study area boundary
roi_gdf = gpd.read_file(path.join("data", "langebaan.json"))
roi_gdf.explore(color='red', tiles=xyz, attr=attr)Each granule returned by earthaccess.search_data() carries a CMR spatial footprint — the bounding polygon that NASA’s Common Metadata Repository (CMR) stores for that granule. We can extract these footprint polygons directly from the L1 Radiance search results (DOI: Hook et al. (2026)) to map the flight lines without opening any files.
# BioSCape campaign dates: October–November 2023
date_range = ("2023-10-01", "2023-11-30")
# Extract the bounding box of the ROI for earthaccess queries
# bounding_box = (min_lon, min_lat, max_lon, max_lat)
bounds = roi_gdf.total_bounds # [minx, miny, maxx, maxy]
# Search for L1 Radiance granules over the study area
# We will reuse l1_granules in later sections, so this search covers both purposes
l1_granules = earthaccess.search_data(
doi="10.3334/ORNLDAAC/2462",
bounding_box=tuple(bounds),
temporal=date_range,
)
print(f"L1 Radiance granules found: {len(l1_granules)}")
for g in l1_granules:
print(f" {g['meta']['native-id']}")L1 Radiance granules found: 26
20231026t124346_LangebaanBox9ZA_L1_B110_V01
20231026t125601_LangebaanBox9ZA_L1_B110_V01
20231026t130800_LangebaanBox9ZA_L1_B110_V01
20231026t132008_LangebaanBox9ZA_L1_B110_V01
20231026t133258_LangebaanBox9ZA_L1_B110_V01
20231026t134517_LangebaanBox9ZA_L1_B110_V01
20231026t135720_LangebaanBox9ZA_L1_B110_V01
20231026t140938_LangebaanBox9ZA_L1_B110_V01
20231026t142132_LangebaanBox9ZA_L1_B110_V01
20231026t143401_LangebaanBox9ZA_L1_B110_V01
20231026t144600_LangebaanBox9ZA_L1_B110_V01
20231029t071408_LangebaanBox9ZA_L1_B110_V01
20231029t071516_LangebaanBox9ZA_L1_B110_V01
20231029t072634_LangebaanBox9ZA_L1_B110_V01
20231029t072742_LangebaanBox9ZA_L1_B110_V01
20231029t074013_LangebaanBox9ZA_L1_B110_V01
20231029t075159_LangebaanBox9ZA_L1_B110_V01
20231029t075313_LangebaanBox9ZA_L1_B110_V01
20231029t080449_LangebaanBox9ZA_L1_B110_V01
20231029t080559_LangebaanBox9ZA_L1_B110_V01
20231029t081958_LangebaanBox9ZA_L1_B110_V01
20231029t082109_LangebaanBox9ZA_L1_B110_V01
20231029t083439_LangebaanBox9ZA_L1_B110_V01
20231029t083545_LangebaanBox9ZA_L1_B110_V01
20231029t084700_LangebaanBox9ZA_L1_B110_V01
20231029t084759_LangebaanBox9ZA_L1_B110_V01
# Plot CMR footprints over the study area
flight_paths_gdf = gpd.GeoDataFrame(l1_granules, geometry=gpd.GeoSeries(l1_granules, crs=4326))
m = flight_paths_gdf.geometry.explore(
tiles=xyz, attr=attr,
color="yellow",
style_kwds={"weight": 2, "fillOpacity": 0.1},
name="L1 Radiance Footprints",
)
roi_gdf.explore(m=m,color="red", name="ROI")SRF and WMX files¶
Also notice there are *.csv files that are named as HyTES_SRF_* and HyTES_WMX_* available at https://
# csv file paths
srf_f = 'https://data.ornldaac.earthdata.nasa.gov/public/hytes/HyTES_L1_Radiance/comp/HyTES_SRF_v6.csv'
wmx_f = 'https://data.ornldaac.earthdata.nasa.gov/public/hytes/HyTES_L1_Radiance/comp/HyTES_WMX_v6.csv'
# open srf files
srf_df = pd.read_csv(srf_f, skiprows=1, index_col='BND')
srf_df.head()# open wmx file
wmx_df = pd.read_csv(wmx_f, skiprows=1, index_col='BND')
wmx_df.head()We can now plot to show how the SRF is distributed for each HyTES bands. Use the slider in the plot to scroll across the bands.
def srf_plot(n):
"""returns wmx srf values for a band"""
return pd.concat([wmx_df.loc[n], srf_df.loc[n]],
keys = ['WMX','SRF'], axis=1)
# band slider
nbands = pn.widgets.IntSlider(name='Band', start=1, end=256)
# interactive SRF plot
temp_df = hvplot.bind(srf_plot, nbands).interactive(width=600)
temp_df.hvplot.line(x='WMX', y='SRF')Search for HyTES L1 Radiance and L2 Granules¶
As we see above, there are two levels of HyTES products currently available for download: HyTES Level 1 (L1) product: Radiance and Locational Information and HyTES Level 2 (L2) product: Emissivity and Land Surface Temperature. The l1_granules from the previous section will be reused here to open and stream the actual radiance data. We also search for co-located L2 Emissivity and LST granules (DOI: 10.3334/ORNLDAAC/2460) over the same area and date range.
# Search for co-located L2 Emissivity and LST granules
l2_granules = earthaccess.search_data(
doi="10.3334/ORNLDAAC/2460",
bounding_box=tuple(bounds),
temporal=date_range,
)
print(f"L2 Emissivity/LST granules found: {len(l2_granules)}")
for g in l2_granules:
print(f" {g['meta']['native-id']}")L2 Emissivity/LST granules found: 26
20231026t124346_LangebaanBox9ZA_L2_B200_V06
20231026t125601_LangebaanBox9ZA_L2_B200_V06
20231026t130800_LangebaanBox9ZA_L2_B200_V06
20231026t132008_LangebaanBox9ZA_L2_B200_V06
20231026t133258_LangebaanBox9ZA_L2_B200_V06
20231026t134517_LangebaanBox9ZA_L2_B200_V06
20231026t135720_LangebaanBox9ZA_L2_B200_V06
20231026t140938_LangebaanBox9ZA_L2_B200_V06
20231026t142132_LangebaanBox9ZA_L2_B200_V06
20231026t143401_LangebaanBox9ZA_L2_B200_V06
20231026t144600_LangebaanBox9ZA_L2_B200_V06
20231029t071408_LangebaanBox9ZA_L2_B200_V06
20231029t071516_LangebaanBox9ZA_L2_B200_V06
20231029t072634_LangebaanBox9ZA_L2_B200_V06
20231029t072742_LangebaanBox9ZA_L2_B200_V06
20231029t074013_LangebaanBox9ZA_L2_B200_V06
20231029t075159_LangebaanBox9ZA_L2_B200_V06
20231029t075313_LangebaanBox9ZA_L2_B200_V06
20231029t080449_LangebaanBox9ZA_L2_B200_V06
20231029t080559_LangebaanBox9ZA_L2_B200_V06
20231029t081958_LangebaanBox9ZA_L2_B200_V06
20231029t082109_LangebaanBox9ZA_L2_B200_V06
20231029t083439_LangebaanBox9ZA_L2_B200_V06
20231029t083545_LangebaanBox9ZA_L2_B200_V06
20231029t084700_LangebaanBox9ZA_L2_B200_V06
20231029t084759_LangebaanBox9ZA_L2_B200_V06
We’ll work with one of the matching L1 and L2 granule. earthaccess.open() returns file-like objects that stream data directly from the cloud — no local download needed.
# Open the first L1 and L2 granule from the cloud
l1_files = earthaccess.open([l1_granules[4]], provider="ORNL_CLOUD")
l2_files = earthaccess.open([l2_granules[4]], provider="ORNL_CLOUD")
print(f"L1 file: {l1_files[0].path}")
print(f"L2 file: {l2_files[0].path}")L1 file: ornl-cumulus-prod-protected/hytes/HyTES_L1_Radiance/data/20231026t133258_LangebaanBox9ZA_L1_B110_V01.hdf5
L2 file: ornl-cumulus-prod-protected/hytes/HyTES_L2_Emissivity_LST/data/20231026t133258_LangebaanBox9ZA_L2_B200_V06.hdf5
HyTES L1 Radiance¶
The L1 data file (HDF-5) provides calibrated radiance in band-interleaved-by-pixel (BIP) format with three dimensions:
line— along-track scan lines (variable length)sample— 512 cross-track pixelsband— 256 spectral channels from 7.5–12 µm
The file also includes per-pixel geolocation (latitude, longitude).
Let’s open it with xarray and explore its structure.
# Open L1 HDF5 file via xarray (streaming from cloud)
ds_l1_raw = xr.open_datatree(l1_files[0], engine="h5netcdf", chunks="auto", phony_dims="sort")
# Flatten to a single dataset and rename phony dims to meaningful names
ds_l1 = ds_l1_raw.to_dataset().rename({
"phony_dim_0": "line",
"phony_dim_1": "sample",
"phony_dim_2": "band",
})
# # Attach lat/lon as coordinates for easy spatial indexing
# ds_l1.coords["latitude"] = ds_l1["latitude"]
# ds_l1.coords["longitude"] = ds_l1["longitude"]
print(f"Lines: {ds_l1.sizes['line']}, Samples: {ds_l1.sizes['sample']}, Bands: {ds_l1.sizes['band']}")
ds_l1Lines: 4456, Samples: 512, Bands: 256
Plot a Single Radiance Band¶
Band 179 is near 10.8 µm, a commonly used window channel for LST retrieval.
ds_l1["radiance_data"].isel(band=179).hvplot.image(
data_aspect=1,
aspect="equal",
cmap="Greys",
clim=(7, 12),
frame_width=150,
title="L1 Radiance — Band 179 (~10.8 µm)",
).opts(invert_yaxis=True)False-Color Radiance Composite¶
We create a false-color RGB composite using three TIR channels:
R: Band 150 (~10.1 µm)
G: Band 100 (~9.2 µm)
B: Band 58 (~8.5 µm)
ds_rgb = ds_l1["radiance_data"].isel(band=[150, 100, 58])
# Assign integer coords so hvplot can index the band dimension
ds_rgb.coords["sample"] = np.arange(ds_rgb.sizes["sample"])
ds_rgb.coords["line"] = np.arange(ds_rgb.sizes["line"])
ds_rgb.coords["band"] = np.arange(ds_rgb.sizes["band"])
ds_rgb.hvplot.rgb(
x="sample", y="line", bands="band",
rasterize=True, flip_yaxis=True,
robust=True, data_aspect=1, aspect="equal",
frame_width=150,
title="False-color composite (R:10.1µm, G:9.2µm, B:8.5µm)",
)Radiance Spectra for Selected Surface Types¶
Let’s compare radiance spectra from three surface types within the scene: inshore water, land, and open water. We find the pixel nearest to each reference location using the per-pixel latitude/longitude arrays.
# Three reference locations (lon, lat)
points_df = pd.DataFrame({
"Type": ["Inshore", "Land", "Water"],
"lat": [-33.15014, -33.21875, -33.04839],
"lon": [ 18.06706, 18.11642, 18.00005],
})
points_gdf = gpd.GeoDataFrame(
points_df,
geometry=gpd.points_from_xy(points_df.lon, points_df.lat),
crs="EPSG:4326",
)
points_gdf.explore("Type", marker_kwds={"radius": 6}, tiles=xyz, attr=attr)def get_nearest_pixel(ds, lat, lon):
"""Return the pixel in ds whose lat/lon is closest to the given coordinates."""
r2 = (ds["latitude"] - lat) ** 2 + (ds["longitude"] - lon) ** 2
idx = np.where(r2.values == r2.values.min())
return ds.sel(line=idx[0][0], sample=idx[1][0])
# assign coordinates to the xarray dataset
ds_l1.coords['longitude'] = ds_l1.longitude
ds_l1.coords['latitude'] = ds_l1.latitude
# Retrieve spectra for all three points
spectra = []
for row in points_gdf.itertuples():
px = get_nearest_pixel(ds_l1, row.lat, row.lon)
spectra.append(px)
pts = xr.concat(spectra, pd.Index(points_df["Type"].tolist(), name="Surface Type"))
pts["radiance_data"].hvplot.scatter(
x="band",
by="Surface Type",
xlabel="Band index",
ylabel="Radiance (W m⁻² µm⁻¹ sr⁻¹)",
title="HyTES L1 Radiance Spectra by Surface Type",
frame_width=500,
)HyTES L2 Land Surface Temperature and Emissivity¶
The HyTES Level 2 dataset (DOI: 10.3334/ORNLDAAC/2460) provides three key variables in HDF-5 format, derived using the Temperature Emissivity Separation (TES) algorithm:
| Variable | Description | Spectral Range | Bands |
|---|---|---|---|
L2_LST | Land surface temperature in Kelvin. Derived from atmospherically corrected level-1 radiance data using the TES algorithm. | — | 1 |
L2_Emissivity | Emissivity spectral data from 8.2-11.5 micrometers, retrieved on 164 Temperature Emissivity Separation (TES) bands instead of 256. | 8.2–11.5 µm | 164 |
L2_Emissivity_PC | Principal Component (PC) eigenvector regression emissivity data from 7.4-12 micrometers for all 256 channels. | 7.4–12.0 µm | 256 |
Note some HyTES campaigns are flown on “ER2” and others campaigns are “Twin Otter” flights. The L2 products are slightly different between these two flights. Please refer to the user guide and the L2 ATBD for more information.
# Open L2 HDF5 file via xarray (streaming from cloud)
ds_l2_raw = xr.open_datatree(l2_files[0], engine="h5netcdf", chunks="auto", phony_dims="sort")
# The L2 file has a different dim ordering: band, line, sample, bands_emis
ds_l2 = ds_l2_raw.to_dataset().rename({
"phony_dim_0": "band",
"phony_dim_1": "line",
"phony_dim_2": "sample",
"phony_dim_3": "bands_emis",
})
print(f"L2 variables: {list(ds_l2.data_vars)}")
ds_l2L2 variables: ['ISAC_Path_Rad', 'ISAC_Skydown_Rad', 'L2_Emissivity', 'L2_Emissivity_PC', 'L2_Emissivity_PC_Wavelengths', 'L2_Emissivity_Wavelengths', 'L2_LST', 'Transmission']
Land Surface Temperature¶
LST is stored in Kelvin. We plot the full spatial image to see the temperature distribution across the flight line.
ds_l2["L2_LST"].hvplot.image(
width=300, height=600,
cmap="seismic",
clim=(250, 350),
title="L2 Land Surface Temperature (K)",
).opts(invert_yaxis=True)Emissivity Spectra for Selected Surface Types¶
Using the same reference locations as before, we compare the two emissivity products (L2_Emissivity on 164 TES bands and L2_Emissivity_PC on all 256 channels) at the inshore pixel.
# Select the inshore pixel in the L2 dataset (using fixed indices from the L1 lookup)
# L2 dimensions correspond to the same spatial grid as L1
ds_px = ds_l2.sel(sample=218, line=2164)
plot_tes = ds_px.hvplot.scatter(
y="L2_Emissivity",
x="L2_Emissivity_Wavelengths",
color="red",
label="L2_Emissivity (TES, 164 bands)",
xlabel="Wavelength (µm)",
ylabel="Emissivity",
)
plot_pc = ds_px.hvplot.scatter(
y="L2_Emissivity_PC",
x="L2_Emissivity_PC_Wavelengths",
color="green",
label="L2_Emissivity_PC (PC regression, 256 bands)",
)
(plot_tes * plot_pc).opts(
title="HyTES L2 Emissivity — Inshore pixel (Langebaan)",
frame_width=500,
legend_position="bottom_right",
)Summary¶
In this tutorial we used earthaccess.search_datasets(instrument="HyTES") to discover all HyTES collections on NASA Earthdata. We then searched for L1 Radiance granules over Langebaan Lagoon during the BioSCape 2023 campaign, and plotted the CMR granule footprint polygons as flight lines — no extra file download needed. We also streamed HDF-5 files directly from the Earthdata Cloud using earthaccess.open() — no local download required. We opened and explored the L1 radiance dataset with xarray, plotted a single band and a false-color TIR composite, and compared radiance spectra across surface types. We finally opened the L2 dataset to visualize the Land Surface Temperature image and compare TES vs. PC-regression emissivity products.
- Hook, S. J., Hulley, G. C., La, T. T., Rivera, G., Johnson, W. R., & Eng, B. T. (2026). HyTES Level 1 Radiance, Facility Instrument Collection. ORNL Distributed Active Archive Center. 10.3334/ORNLDAAC/2462
- Hook, S. J., Hulley, G. C., La, T. T., Rivera, G., Johnson, W. R., & Eng, B. T. (2026). HyTES L1 Geolocation, Facility Instrument Collection. ORNL Distributed Active Archive Center. 10.3334/ORNLDAAC/2459
- Hulley, G. C., Hook, S. J., La, T. T., Rivera, G., Johnson, W. R., & Eng, B. T. (2026). HyTES: Level 2 Emissivity and Land Surface Temperature. ORNL Distributed Active Archive Center. 10.3334/ORNLDAAC/2460