Skip to content

frandorr/aereo

Repository files navigation

AerEO banner

AerEO

Access, extract, reproject for Earth Observation — locally or remotely, with a pluggable pipeline, without reinventing the wheel.

Install Docs Tutorials License

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.

AerEO pipeline animation: from ExtractionJob to MajorTOM artifacts

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 on the Major TOM grid

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.

Install

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
  • pip or uv
  • 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]

Which example should I run?

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

Examples

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 Open In Colab
01b — Sentinel-2 NDVI Sentinel-2 L2A (NDVI) Open In Colab
01c — Sentinel-2 NDWI Sentinel-2 L2A (NDWI) Open In Colab
02 — VIIRS VIIRS Open In Colab
03 — Sentinel-3 OLCI Sentinel-3 OLCI Open In Colab
03b — Sentinel-3 NDVI Sentinel-3 OLCI (NDVI) Open In Colab
04 — GeoTessera GeoTessera Open In Colab
05 — GOES-19 ABI GOES-19 ABI Open In Colab
06 — Multiple constellations VIIRS + GOES-19 Open In Colab
Step by step raw pipeline Sentinel-2 (raw API) Open In Colab
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.")

Quickstart with a YAML config

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.geojson
Step 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_geotiff

Key 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, B08 for 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.py

You will get GeoTIFFs in ./output plus output/artifacts.parquet, where each row is one Major TOM grid cell.

Copy/paste example

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-2 for 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.

Configuration with Hydra

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_geotiff

Load 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.

How it works

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"]
Loading
  1. Search — query a catalog and get a validated GeoDataFrame[AssetSchema].
  2. Prepare — group assets by time and native CRS into ExtractionTask objects.
  3. 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.

Why AerEO?

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

Core concepts
  1. ExtractionJob — a validated bundle of grid size, output URI, AOI, and reader/writer callables.
  2. Search function — e.g. search_stac. Pass it to job.search(...) with kwargs.
  3. Task builder function — e.g. build_grouped_tasks. Groups assets into ExtractionTask objects.
  4. ExtractionTask — one unit of work: assets + grid patches + stage pipeline.
  5. Stage functionsread_odc_stac, reproject_odc, ndvi, write_geotiff, etc. Passed directly to ExtractionJob(read=..., write=...).
  6. LocalExecutor — runs tasks locally. Swap for LambdaExecutor later without changing the pipeline.

What you get

These outputs come straight from the tutorial notebooks. Every plot shows grid-aligned patches on the Major TOM grid, with the target AOI overlaid.

Sentinel-2 extracted patches

Sentinel-2 NDVI patches

VIIRS extracted patches

VIIRS vs GOES-19 ABI — same grid, different sensors

The same Major TOM grid cells extracted from two very different sensors:

GOES-19 ABI VIIRS
GOES-19 ABI on the shared grid VIIRS on the shared grid

See the full walkthrough in 06 — Multiple constellations.

For ML users

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.

Troubleshooting

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

Docs

Acknowledgments


Apache License 2.0

About

Access, extract, reproject for Earth Observation — locally or remotely, without reinventing the wheel.

Resources

License

Code of conduct

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors