Access, extract, reproject for Earth Observation — locally or remotely, with a pluggable pipeline, without reinventing the wheel.
AerEO is a plugin-based satellite data extraction framework. It wires together
the catalog, reading, reprojection, and writing tools you already trust (STAC,
Earthaccess, Satpy, odc-geo) behind a single pipeline where every step can be
replaced. The result: analysis-ready GeoTIFFs aligned to the Major TOM
grid, ready for ML or downstream analysis.
Each stage below is a plain Python function you can swap. You can keep the built-ins, replace one step, or plug in an entirely different block at any point in the pipeline.
Solid borders = required stages. Dashed borders = optional stages. Every stage is interchangeable. The animation loops through the flow: configure → search → build tasks → extract per task → MajorTOM artifacts.
Sentinel-2 NDWI extracted as Major TOM grid cells. Every job writes an
artifacts.parquet catalog where each row is a Major TOM grid cell referencing
the file that was just extracted; the default writer emits GeoTIFFs, but you
can swap in any writer plugin. Because everything is aligned to the same grid,
outputs from different sensors and dates can be merged directly into ML
datasets.
The fastest way to get started is to install AerEO with all optional extras:
uv add "aereo[all]"
# or
pip install "aereo[all]"Sensor-specific search and I/O plugins are separate packages, so you only ship what you need. For per-sensor install commands and credentials, see Install.
First-run checklist
- Python 3.12 or newer
piporuv- Credentials for any catalog that requires them (e.g. NASA Earthdata for VIIRS / Sentinel-3)
- Run in the same AWS region as your data source for large extractions
Performance tip: Run AerEO in the same region as your data source. During extraction, data is downloaded from the source catalog; if your runtime is not in the same AWS region as the data, downloads can be very slow. Being in the same region is HIGHLY recommended to avoid slow transfers and egress charges.
Full documentation: https://frandorr.github.io/aereo
Optional extras
AerEO's core install covers STAC search, ODC-based reprojection, GeoTIFF writing, and local execution. A few built-in capabilities need extra dependencies:
| Extra | Enables | Install |
|---|---|---|
serverless |
LambdaExecutor and S3 staging (via boto3) |
uv add aereo[serverless] |
swath |
reproject_swath / reproject_pyresample for 2-D lat/lon swath data |
uv add aereo[swath] |
viz |
Cartopy-backed plots in aereo.viz |
uv add aereo[viz] |
pc |
Microsoft Planetary Computer integration | uv add aereo[pc] |
all |
Everything above in one command | uv add aereo[all] |
| I want to... | Start with | Why |
|---|---|---|
| Try without credentials | 01 — Sentinel-2 | STAC, public data, no auth |
| Learn the raw API | Step by step raw pipeline | No Hydra, no config files |
| Compute an index (NDVI) | 01b — Sentinel-2 NDVI | Shows the postprocess stage |
| Use NASA data (VIIRS / Sentinel-3) | 02 — VIIRS or 03 — Sentinel-3 OLCI | Earthaccess + Satpy |
| Run multiple sensors on the same grid | 06 — Multiple constellations | Compares VIIRS and GOES-19 ABI |
All tutorial notebooks can be opened directly in Google Colab, or read as an executable book at frandorr.github.io/aereo-notebooks. Each notebook starts with a setup cell that installs AerEO and any sensor-specific plugins it needs.
| Notebook | Sensor(s) | Open in Colab |
|---|---|---|
| 01 — Sentinel-2 | Sentinel-2 L2A | |
| 01b — Sentinel-2 NDVI | Sentinel-2 L2A (NDVI) | |
| 01c — Sentinel-2 NDWI | Sentinel-2 L2A (NDWI) | |
| 02 — VIIRS | VIIRS | |
| 03 — Sentinel-3 OLCI | Sentinel-3 OLCI | |
| 03b — Sentinel-3 NDVI | Sentinel-3 OLCI (NDVI) | |
| 04 — GeoTessera | GeoTessera | |
| 05 — GOES-19 ABI | GOES-19 ABI | |
| 06 — Multiple constellations | VIIRS + GOES-19 | |
| Step by step raw pipeline | Sentinel-2 (raw API) |
NASA Earthaccess authentication for Colab
The VIIRS, Sentinel-3 OLCI, and Sentinel-3 NDVI notebooks use earthaccess to query NASA data. You must configure authentication first. The recommended way is to create a ~/.netrc file — follow the earthaccess authentication guide.
For Google Colab, run this cell once to create ~/.netrc:
import os
from getpass import getpass
earthdata_username = getpass("Earthdata username: ")
earthdata_password = getpass("Earthdata password: ")
netrc_path = os.path.expanduser("~/.netrc")
with open(netrc_path, "w") as f:
f.write("machine urs.earthdata.nasa.gov login {username} password {password}\n".format(
username=earthdata_username,
password=earthdata_password
))
os.chmod(netrc_path, 0o600)
print(f"Successfully created {netrc_path} for Earthdata authentication.")This five-step guide extracts Sentinel-2 red + nir bands from Microsoft Planetary Computer over the Chocón reservoir in Argentina. Planetary Computer serves data from Azure Blob Storage, so this works well even when your runtime is not in an AWS data region. No repo clone required.
Step 1: create a project (30 seconds)
mkdir my_first_job && cd my_first_job
uv init
uv add "aereo[pc]"aereo[pc] includes the core framework plus the Planetary Computer signing helper, which gives fast global access to Sentinel-2 data.
Step 2: download a sample AOI
curl -L -o aoi.geojson https://raw.githubusercontent.com/frandorr/aereo/main/examples/config/aoi/chocon.geojsonStep 3: write the job config
Create job.yaml:
name: pc_s2_demo
grid_dist: 10_000
output_uri: ./output
target_aoi: ./aoi.geojson
search:
_target_: aereo.builtins.search_stac
_partial_: true
stac_api_url: "https://planetarycomputer.microsoft.com/api/stac/v1"
collections:
sentinel-2-l2a: [B04, B08]
intersects: ./aoi.geojson
start_datetime: "2024-01-01T00:00:00Z"
end_datetime: "2024-01-03T23: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: "uint16"
nodata: 0
write:
_target_: aereo.builtins.write.write_geotiffKey fields:
grid_dist: Major TOM grid spacing in meters (10_000= 10 km cells).target_aoi: Path to a GeoJSON polygon.collections: Map of STAC collection names to the bands you want (B04,B08for Sentinel-2 red / nir).pystac_open_params.modifier/read.patch_url: Planetary Computer URL signing; required to fetch assets from Azure Blob Storage.
Step 4: write the runner script
Create run_job.py:
from aereo.pipeline import ExtractionJob
from aereo.builtins import build_grouped_tasks
from aereo.executors import LocalExecutor
job = ExtractionJob.load_from_config(".", config_name="job")
assets = job.search()
if assets.empty:
raise SystemExit("No assets found.")
tasks = job.build_tasks(assets, build_grouped_tasks)
# Run only the first task for demo speed.
artifacts = job.execute(tasks[:1], executor=LocalExecutor(workers=1))
catalog_uri = job.write_catalog(artifacts)
print(f"Catalog: {catalog_uri}")Why tasks[:1]? A real extraction runs every task; slicing to one task keeps the first demo fast and avoids downloading more data than needed.
Step 5: run it
uv run run_job.pyYou will get GeoTIFFs in ./output plus output/artifacts.parquet, where each row is one Major TOM grid cell.
Save this as quickstart.py and run it with uv run quickstart.py:
Network speed note: This example downloads Sentinel-2 data from Earth Search over the public internet. From a local machine the download can be a bottleneck. For the fastest first experience, run it in Google Colab or an AWS compute instance in the same region as the data (
us-west-2for Earth Search).
"""Pure-Python quickstart for AerEO.
To run the full pipeline:
uv run python examples/quickstart_pure_python.py
"""
from __future__ import annotations
from datetime import datetime, timezone
from shapely.geometry import Polygon
from aereo.builtins import (
build_grouped_tasks,
read_odc_stac,
search_stac,
write_geotiff,
)
from aereo.executors import LocalExecutor
from aereo.pipeline import ExtractionJob
def main() -> None:
"""Build a job in pure Python and run the extraction pipeline."""
# Tiny AOI around Chocón reservoir, Argentina.
aoi = Polygon(
[
(-68.90986824592407, -39.23705421799603),
(-68.65925870907353, -39.23705421799603),
(-68.65925870907353, -39.41589522092947),
(-68.90986824592407, -39.41589522092947),
(-68.90986824592407, -39.23705421799603),
]
)
job = ExtractionJob(
name="quickstart",
grid_dist=10_000,
output_uri="/tmp/aereo_quickstart",
search=search_stac,
read=read_odc_stac,
write=write_geotiff,
target_aoi=aoi,
)
print("--- ExtractionJob ---")
print(f"name: {job.name}")
print(f"output_uri: {job.output_uri}")
print(f"grid_dist: {job.grid_dist}")
print("\n--- Search ---")
assets = job.search(
stac_api_url="https://earth-search.aws.element84.com/v1",
collections={"sentinel-2-l2a": ["red", "nir"]},
intersects=aoi,
start_datetime=datetime(2024, 1, 1, tzinfo=timezone.utc),
end_datetime=datetime(2024, 1, 10, tzinfo=timezone.utc),
)
print(f"Found {len(assets)} asset rows")
if assets.empty:
print("No assets found; nothing to extract.")
return
print("\n--- Build tasks ---")
tasks = job.build_tasks(assets, build_grouped_tasks)
print(f"Built {len(tasks)} task(s)")
print("\n--- Extract ---")
# Run only the first task for demo speed.
artifacts = job.execute(tasks[:1], executor=LocalExecutor(workers=1))
print(f"Extracted {len(artifacts)} artifact(s)")
catalog_uri = job.write_catalog(artifacts)
print(f"\nCatalog written to: {catalog_uri}")
if __name__ == "__main__":
main()Open /tmp/aereo_quickstart — you have GeoTIFFs on the Major TOM grid. The script also calls job.write_catalog(artifacts), so an artifacts.parquet catalog is written next to the GeoTIFFs.
For reusable jobs, put YAML configs in a Hydra package and load them with
ExtractionJob.load_from_config. This is the same Sentinel-2 job as the
quickstart, expressed as config:
# examples/config/job_sentinel2.yaml
target_bands: [red, nir]
aoi_path: config/aoi/chocon.geojson
name: sentinel2_sample
grid_dist: 10_000
grid_cells_margin: 10
target_aoi: ${aoi_path}
output_uri: /tmp/aereo_extraction
overwrite: false
search:
_target_: aereo.builtins.search_stac
_partial_: true
stac_api_url: "https://earth-search.aws.element84.com/v1"
collections:
sentinel-2-l2a: ${target_bands}
intersects: ${aoi_path}
start_datetime: "2024-01-01T00:00:00Z"
end_datetime: "2024-01-10T23:59:59Z"
read:
_partial_: true
_target_: aereo.builtins.read_odc_stac
write:
_target_: aereo.builtins.write.write_geotiffLoad and override values from Python:
from aereo.pipeline import ExtractionJob
job = ExtractionJob.load_from_config(
config_dir="examples/config",
config_name="job_sentinel2",
overrides=[
"grid_dist=50_000",
"search.start_datetime=2024-02-01T00:00:00Z",
],
)The overrides use Hydra dot notation, so any field in the YAML can be changed
without editing the file.
Pipeline overview
flowchart LR
Search["Search provider"] --> Prepare["Prepare tasks\n(grid + grouping)"]
Prepare --> Execute["Execute\n(local / Lambda)"]
Execute --> Catalog["Output catalog\n+ GeoTIFFs"]
- Search — query a catalog and get a validated
GeoDataFrame[AssetSchema]. - Prepare — group assets by time and native CRS into
ExtractionTaskobjects. - Execute — run each task through
read → preprocess → reproject → postprocess → write, producing grid-aligned artifacts and a catalog.
Any stage can be replaced by a function you write. Learn how in Build a Plugin.
| Problem | How AerEO solves it |
|---|---|
| Every catalog has a different API | One job.search(...) call with swappable search functions. |
| Tiles do not line up across sensors | Built-in Major TOM grid + local UTM patch geoboxes. |
| Reprojection boilerplate | Readers/writers can call reproject_odc (or any reprojector) as needed. |
| Mixed-CRS scenes fail | build_grouped_tasks groups assets by native CRS. |
| Notebook → production is hard | Same config package runs in Python and AWS Lambda. |
| Plugin frameworks force inheritance | AerEO plugins are @validate_call functions + standard entry points. |
Core concepts
ExtractionJob— a validated bundle of grid size, output URI, AOI, and reader/writer callables.- Search function — e.g.
search_stac. Pass it tojob.search(...)with kwargs. - Task builder function — e.g.
build_grouped_tasks. Groups assets intoExtractionTaskobjects. ExtractionTask— one unit of work: assets + grid patches + stage pipeline.- Stage functions —
read_odc_stac,reproject_odc,ndvi,write_geotiff, etc. Passed directly toExtractionJob(read=..., write=...). LocalExecutor— runs tasks locally. Swap forLambdaExecutorlater without changing the pipeline.
These outputs come straight from the tutorial notebooks. Every plot shows grid-aligned patches on the Major TOM grid, with the target AOI overlaid.
The same Major TOM grid cells extracted from two very different sensors:
| GOES-19 ABI | VIIRS |
|---|---|
![]() |
![]() |
See the full walkthrough in 06 — Multiple constellations.
AerEO outputs are designed to be loaded directly into ML pipelines. After a run you have:
- GeoTIFFs aligned to the Major TOM grid, so multi-sensor and multi-date stacks line up without manual reprojection.
artifacts.parquet, a per-cell catalog with columns:id,source_ids,start_time,end_time,uri,collection,geometry,grid_cell,grid_dist,cell_geometry,cell_utm_crs,cell_utm_footprint.
Load the catalog and read the rasters:
import geopandas as gpd
import rasterio
df = gpd.read_parquet("output/artifacts.parquet")
print(df[["grid_cell", "start_time", "uri"]].head())
with rasterio.open(df.iloc[0].uri) as src:
print(src.shape, src.count, src.crs)Because every sensor writes the same grid cells, you can join rows by grid_cell and start_time to build multi-sensor training samples.
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
No assets found |
Date range or AOI too restrictive | Widen the time range or check the AOI geometry |
| Downloads are very slow | Running in a different AWS region than the data | Move your runtime to the same region as the catalog (e.g. us-west-2 for Earth Search) |
earthaccess authentication error |
Missing .netrc or expired credentials |
Create ~/.netrc following the earthaccess guide |
grid_dist looks wrong |
It is in meters, not pixels or degrees | Use values like 10_000 for 10 km cells |
| Outputs do not line up | Different sensors without a shared grid | Ensure all jobs use the same grid_dist and Major TOM grid |
- Install — per-sensor install and credentials
- Pure Python Quickstart — first extraction in 5 minutes
- Configuration — Hydra config package and YAML schema
- Tutorials — Sentinel-2, VIIRS, Sentinel-3, Tessera, GOES-19
- Jupyter Book — the same tutorials as an interactive executable book
- Build a Plugin — add a search, reader, or processing step
- Run on AWS Lambda — go serverless by changing one line
-
AerEO is inspired by the work done in FDL sat-extractor.
-
It is built upon the Major TOM grid from ESA.
Apache License 2.0





