Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Sentinel-1 SAR extraction with AerEO

This chapter extracts Sentinel-1 Ground Range Detected (GRD) radar backscatter (vv and vh polarizations) on the Major TOM grid — using the exact same built-ins as the optical chapters: search_stac, read_odc_stac, reproject_odc, and write_geotiff.

That is the point of the chapter: in AerEO a new sensor is usually just a new STAC collection, not a new plugin. SAR adds all-weather, day-and-night coverage to the same grid-aligned outputs you already know.

The data comes from Microsoft Planetary Computer’s sentinel-1-grd collection, served as Cloud-Optimized GeoTIFFs, so no credentials are required.

Environment setup

The first cell installs AerEO and any required plugins. If you are reading the rendered book or running on Binder, where the environment is already prepared, pip will report that the requirements are satisfied and skip the download.

# Install AerEO and any required plugins for this notebook (Google Colab)
!pip install -q "aereo[viz,pc]"

Config files used in this chapter

The next cell downloads two files:

  1. job_sentinel1.yaml — the AerEO job configuration.

  2. chocon.geojson — the AOI over Argentina’s Chocón region.

Two SAR-specific details live in the read section. Planetary Computer’s Sentinel-1 items carry no proj metadata, so odc-stac cannot auto-guess the native grid — we declare it explicitly (crs: "EPSG:32719", UTM 19S, and resolution: 10 meters). And patch_url: planetary_computer.sign signs asset URLs at load time, the same pattern used by the Planetary Computer Sentinel-2 quickstart. Here is the full YAML we are loading:

# Common vars to reuse in the config
target_bands: [vv, vh]
aoi_path: config/aoi/chocon.geojson
# Common Job config, like name, MajorTOM grid distance, etc

name: sentinel1_sample
grid_dist: 10_000
grid_cells_margin: 10
target_aoi: ${aoi_path}
output_uri: /tmp/aereo_extraction_s1
overwrite: false

search:
  _target_: aereo.builtins.search_stac
  _partial_: true
  stac_api_url: "https://planetarycomputer.microsoft.com/api/stac/v1"
  collections:
    sentinel-1-grd: ${target_bands}
  intersects: ${aoi_path}
  start_datetime: "2024-03-20T00:00:00Z"
  end_datetime: "2024-03-31T23:59:59Z"
  pystac_open_params:
    modifier:
      _target_: planetary_computer.sign_inplace

read:
  _partial_: true
  _target_: aereo.builtins.read_odc_stac
  patch_url:
    _target_: planetary_computer.sign
  dtype: "float32"
  # Planetary Computer's sentinel-1-grd items carry no proj metadata, so
  # odc-stac cannot auto-guess the grid. Chocon is UTM 19S; GRD IW is 10 m.
  crs: "EPSG:32719"
  resolution: 10

write:
  _target_: aereo.builtins.write.write_geotiff
# Download config files and AOIs from the GitHub repository so this
# notebook can run outside the repo (e.g. Google Colab).
import os
import urllib.request

GITHUB_RAW = "https://raw.githubusercontent.com/frandorr/aereo/main"

os.makedirs("config/aoi", exist_ok=True)

# Config files
urllib.request.urlretrieve(
    f"{GITHUB_RAW}/examples/config/job_sentinel1.yaml",
    "config/job_sentinel1.yaml",
)

# AOI files
urllib.request.urlretrieve(
    f"{GITHUB_RAW}/examples/config/aoi/chocon.geojson",
    "config/aoi/chocon.geojson",
)

Loading the job

ExtractionJob.load_from_config() parses the YAML with Hydra, resolves every _target_ callable, and validates the resulting ExtractionJob. The config_name argument is the YAML filename without extension, and config_dir is the folder that contains it.

from aereo.cache import TaskResultCache
from aereo.executors import LocalExecutor
from aereo.pipeline import ExtractionJob

# Load the job from the Hydra config package.
job = ExtractionJob.load_from_config(
    config_dir="config",
    config_name="job_sentinel1",
)

Search and task building

job.search() calls the configured search provider and returns a GeoDataFrame of matched assets. This is a separate step from execution: it only discovers what data is available, without reading or writing anything.

job.build_tasks(assets) turns those assets into a list of ExtractionTask objects. Each task groups the assets needed for one grid cell and time slice.

assets = job.search()  # Use the search method from the job object to get the assets.
tasks = job.build_tasks(assets)
len(tasks)
2026-07-27 10:56:21 [info     ] search_called                  provider=search_stac
2026-07-27 10:56:23 [info     ] build_tasks_start              assets=4 builder=build_grouped_tasks
/home/fran/repos/aereo/.venv/lib/python3.13/site-packages/pydantic/_internal/_validate_call.py:137: UserWarning: assets has no 'crs' column; assuming all assets share the same native CRS. Mixed-CRS assets in one task may fail or produce incorrect results.
  res = self.__pydantic_validator__.validate_python(pydantic_core.ArgsKwargs(args, kwargs))
2

The executor

AerEO separates what to run (the tasks) from how to run them. LocalExecutor runs tasks on the local machine. The workers argument controls parallelism, and use_threads=True uses a threading backend, which is a good default for I/O-bound COG readers.

# now we create an Executor, in this case a LocalExecutor to run
# each ExtractionTask using Threads
local_exec = LocalExecutor(workers=-1, use_threads=True, cache=TaskResultCache())

Running the extraction

job.execute(tasks, executor=...) hands the prepared tasks to the executor. Inside every task AerEO performs the pipeline:

read -> preprocess -> reproject -> postprocess -> write

The returned object is a GeoDataFrame of artifacts — one row per output GeoTIFF with its metadata, CRS, and footprint.

# Extract!
print("Extracting...")
artifacts = job.execute(tasks, executor=local_exec)
print(f"✓ Extracted {len(artifacts)} artifacts")
Extracting...
2026-07-27 10:56:23 [info     ] execute_start                  executor=LocalExecutor task_count=2
✓ Extracted 20 artifacts

Visualizing results

plot_artifact_patches renders the extracted patches over the AOI. We plot the vv band in linear backscatter units with a grayscale percentile stretch. Note the classic SAR signature: dark open water in the reservoir against brighter land — acquired regardless of clouds or daylight.

from aereo.viz import plot_artifact_patches

plot_artifact_patches(
    artifacts,
    ds_factor=1,
    cmap="gray",
    stretch="percentile",
    aoi=job.target_aoi,
    aoi_edgecolor="blue",
)
(<Figure size 2000x1495.93 with 2 Axes>, <Axes: title={'center': 'Extracted Patches Spatial Overview'}, xlabel='UTM X', ylabel='UTM Y'>)
Ignoring fixed y limits to fulfill fixed data aspect with adjustable data limits.
<Figure size 2000x1495.93 with 2 Axes>