This chapter extracts Sentinel-3 OLCI Level-1 data. OLCI is a medium-resolution ocean and land color instrument. Like VIIRS, its native geometry is a swath, so the pipeline includes a swath-to-grid reprojection step.
Sentinel-3 data is distributed through NASA Earthdata as well as ESA sources. Here we use search_earthaccess and read with Satpy, just like the VIIRS example.
Environment setup¶
The first cell installs AerEO plus the Satpy reader and swath reprojection plugins. On Binder these are pre-installed, so pip will skip the download.
# Install AerEO and any required plugins for this notebook (Google Colab)
!pip install -q "aereo[swath,viz]" aereo-read-satpyNASA Earthdata authentication¶
This notebook reads NASA-hosted Sentinel-3 data, so a valid ~/.netrc is required. See the VIIRS chapter or the earthaccess authentication guide for setup instructions.
Config used in this notebook¶
job_sentinel3.yaml is structurally very similar to the VIIRS config, but targets OLCI:
aoi_path: config/aoi/chocon.geojson
name: sentinel3_sample
grid_dist: 50_000
grid_cells_margin: 10
target_aoi: ${aoi_path}
output_uri: /tmp/aereo_extraction
overwrite: false
search:
_target_: aereo.builtins.search_earthaccess
_partial_: true
collections:
S3A_OL_1_EFR: ["Oa08", "Oa17"]
S3B_OL_1_EFR: ["Oa08", "Oa17"]
intersects: ${aoi_path}
start_datetime: "2024-01-01T00:00:00Z"
end_datetime: "2024-01-02T23:59:59Z"
read:
_partial_: true
_target_: aereo.read_satpy.read_satpy
reader: olci_l1b
wishlist: [Oa08, Oa17]
downloader:
_target_: aereo.asset_downloader.core._download_with_earthaccess
_partial_: true
reproject:
_target_: aereo.builtins.reproject_swath
_partial_: true
reproject_mode: grid
resolution: 300
write:
_target_: aereo.builtins.write.write_geotiffKey points:
collectionslists both Sentinel-3A and Sentinel-3B OLCI EFR products.wishlistselects bands Oa08 (red) and Oa17 (NIR-like), useful for vegetation or red-edge analysis.resolution: 300matches the OLCI full-resolution swath spacing.
# 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_sentinel3.yaml",
"config/job_sentinel3.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.
A job is a declarative bundle of pipeline steps. Once loaded, the same job can be searched, can have tasks built from it, and can be executed.
from aereo.cache import TaskResultCache
from aereo.executors import LocalExecutor
from aereo.pipeline import ExtractionJob
job = ExtractionJob.load_from_config(
config_dir="config",
config_name="job_sentinel3",
)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, so the work can be parallelised later.
assets = job.search() # Use the search method from the job object to get the assets.
tasks = job.build_tasks(assets)
len(tasks)2026-07-08 15:00:15 [info ] search_called provider=search_earthaccess
/root/repos/aereo/.venv/lib/python3.13/site-packages/earthaccess/results.py:348: FutureWarning: As of version 1.0, `DataGranule.size` will be accessed as an attribute; e.g. use `DataCollection.size` **not** `DataCollection.size()`
self["size"] = self.size()
/root/repos/aereo/components/aereo/builtins/search.py:324: FutureWarning: As of version 1.0, `DataGranule.size` will be accessed as an attribute; e.g. use `DataCollection.size` **not** `DataCollection.size()`
size_mb = g.size()
2026-07-08 15:00:23 [info ] build_tasks_start assets=3 builder=build_grouped_tasks
/root/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))
3The executor¶
Because OLCI files are larger and swath reprojection is CPU-intensive, this notebook uses use_threads=False with two workers. Each task downloads the necessary files, reads them with Satpy, reprojects the swath to the grid, and writes a GeoTIFF.
# now we create an Executor, in this case a LocalExecutor to run
# each ExtractionTask using Threads
local_exec = LocalExecutor(workers=1, use_threads=False, cache=TaskResultCache())Running the extraction¶
job.execute(tasks, executor=...) hands the prepared tasks to the executor. The executor runs each task independently; inside every task AerEO performs the pipeline:
read -> preprocess -> reproject -> postprocess -> writeThe 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")Visualizing OLCI results¶
The output GeoTIFFs contain the requested OLCI bands reprojected to a regular 300 m grid.
from aereo.viz import plot_artifact_patches
plot_artifact_patches(
artifacts,
ds_factor=1,
cmap="viridis",
stretch="percentile",
aoi=job.target_aoi,
aoi_edgecolor="blue",
)(<Figure size 2000x1981.62 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.
