This chapter shows how to combine two independent AerEO jobs — one for VIIRS and one for GOES-19 ABI — and align them to the same area and time window. This is useful when you want to compare or fuse observations from different satellites.
The key idea is that each ExtractionJob is a self-contained pipeline. You can load several jobs, run their searches independently, align the results temporally, build tasks for each job, and then execute both sets of tasks with the same executor.
Environment setup¶
The first cell installs AerEO plus the plugins for reading VIIRS swath data, reprojecting swaths, and searching AWS GOES. 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-satpy aereo-search-aws-goesNASA Earthdata authentication and configs¶
The VIIRS part of this notebook reads NASA-hosted data, so a valid ~/.netrc is required. The GOES-19 part uses AWS open data and does not need NASA credentials. See the VIIRS chapter or the earthaccess authentication guide for setup instructions.
This notebook loads two jobs and aligns them to the same AOI:
config/job_viirs.yaml(see the VIIRS tutorial for the full file)config/job_goes19.yaml(see the GOES-19 tutorial for the full file)
Both jobs are loaded with Hydra overrides so they use the same AOI and the VIIRS job uses a coarser alignment resolution:
viirs_job = ExtractionJob.load_from_config(
config_dir="config",
config_name="job_viirs",
overrides=[
"target_aoi=config/aoi/cordoba.geojson",
"search.intersects=config/aoi/cordoba.geojson",
"resolution=400",
"+alignment_resolution=2000",
],
)
goes_job = ExtractionJob.load_from_config(
config_dir="config",
config_name="job_goes19",
overrides=[
"target_aoi=config/aoi/cordoba.geojson",
"search.intersects=config/aoi/cordoba.geojson",
],
)# 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_viirs.yaml",
"config/job_viirs.yaml",
)
urllib.request.urlretrieve(
f"{GITHUB_RAW}/examples/config/job_goes19.yaml",
"config/job_goes19.yaml",
)
# AOI files
urllib.request.urlretrieve(
f"{GITHUB_RAW}/examples/config/aoi/chocon.geojson",
"config/aoi/chocon.geojson",
)
urllib.request.urlretrieve(
f"{GITHUB_RAW}/examples/config/aoi/cordoba.geojson",
"config/aoi/cordoba.geojson",
)Loading both jobs¶
ExtractionJob.load_from_config() is called twice, once per config. Each returned job is an independent pipeline with its own search, read, reproject, and write settings. The overrides make both jobs use the same AOI.
from aereo.cache import TaskResultCache
from aereo.executors import LocalExecutor
from aereo.pipeline import ExtractionJob
target_aoi_path = "config/aoi/cordoba.geojson"
# Load the job from the Hydra config package.
viirs_job = ExtractionJob.load_from_config(
config_dir="config",
config_name="job_viirs",
overrides=[
"target_aoi=config/aoi/cordoba.geojson",
"search.intersects=config/aoi/cordoba.geojson",
"resolution=400",
"+alignment_resolution=2000",
],
)
goes_job = ExtractionJob.load_from_config(
config_dir="config",
config_name="job_goes19",
overrides=[
"target_aoi=config/aoi/cordoba.geojson",
"search.intersects=config/aoi/cordoba.geojson",
],
)Aligning VIIRS and GOES-19 in time¶
Instead of searching GOES-19 across an arbitrary time window, we first search VIIRS and then use the VIIRS result’s start and end times to drive the GOES-19 search. This guarantees that the two extracted datasets cover the same period.
start_time = viirs_assets.iloc[0].start_time
end_time = viirs_assets.iloc[0].end_time
goes19_assets = goes_job.search(start_datetime=start_time, end_datetime=end_time)This pattern is common in multi-sensor workflows: use the sensor with the narrower swath or rarer coverage to define the time window, then pull the other sensor for the same window.
# lets change daterange for the viirs search
viirs_assets = viirs_job.search(
start_datetime="2026-01-01T10:00:00Z",
end_datetime="2026-01-01T23:00:00Z",
intersects=viirs_job.target_aoi,
)2026-07-08 17:00:28 [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()
# now, lets get the viirs asset start_time and end_time and search the corresponding goes-19 assets
start_time = viirs_assets.iloc[0].start_time
end_time = viirs_assets.iloc[0].end_timegoes19_assets = goes_job.search(start_datetime=start_time, end_datetime=end_time)2026-07-08 17:00:37 [info ] search_called provider=search_aws_goes
Building tasks for both jobs¶
Tasks are built independently for each job because they have different grids, readers, and reprojectors. The resulting task lists can be executed by the same executor.
viirs_tasks = viirs_job.build_tasks(viirs_assets)
goes_tasks = goes_job.build_tasks(goes19_assets)2026-07-08 17:00:39 [info ] build_tasks_start assets=2 builder=build_grouped_tasks
2026-07-08 17:00:39 [info ] build_tasks_start assets=2 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))
/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))
The executor¶
We use a LocalExecutor with use_threads=False and workers=2 to run tasks in separate processes. This is preferable when tasks are CPU-bound (e.g., swath reprojection) and you want to avoid the Python GIL.
# now we create an Executor, in this case a LocalExecutor to run
local_exec = LocalExecutor(workers=2, use_threads=False, cache=TaskResultCache())Executing both pipelines¶
viirs_job.execute(...) and goes_job.execute(...) run their respective tasks through the shared executor. Inside each task the pipeline is read → preprocess/reproject → write. Search already happened in the earlier cells.
# Extract!
viirs_artifacts = viirs_job.execute(viirs_tasks, executor=local_exec)# Extract!
goes_artifacts = goes_job.execute(goes_tasks, executor=local_exec)2026-07-08 17:01:59 [info ] execute_start executor=LocalExecutor task_count=2
2026-07-08 17:02:02 [debug ] file_downloaded engine=satpy local_path=/tmp/aereo_extraction/OR_ABI-L1b-RadF-M6C07_G19_s20260011810226_e20260011819546_c20260011819586.nc
2026-07-08 17:02:14 [debug ] file_downloaded engine=satpy local_path=/tmp/aereo_extraction/OR_ABI-L1b-RadF-M6C07_G19_s20260011800226_e20260011809546_c20260011809589.nc
Visualizing GOES-19 ABI¶
First we plot the GOES-19 ABI channel. Because GOES is geostationary, the patch may cover a larger area than the VIIRS swath.
from aereo.viz import plot_artifact_patches
fig, ax = plot_artifact_patches(
goes_artifacts,
ds_factor=1,
stretch="percentile",
cmap="RdYlGn",
aoi=goes_job.target_aoi,
aoi_edgecolor="blue",
)Ignoring fixed y limits to fulfill fixed data aspect with adjustable data limits.

Visualizing VIIRS¶
Next we plot the VIIRS I-band. The swath reprojection gives a local grid centered on the AOI.
fig, ax = plot_artifact_patches(
viirs_artifacts,
ds_factor=1,
stretch="percentile",
cmap="RdYlGn",
aoi=viirs_job.target_aoi,
aoi_edgecolor="blue",
)Ignoring fixed y limits to fulfill fixed data aspect with adjustable data limits.

Side-by-side comparison¶
By extracting both sensors for the same AOI and time window, you can directly compare observations, stack them as layers, or feed them into a downstream fusion model.
from aereo.viz import plot_artifact_patches
fig, ax = plot_artifact_patches(
goes_artifacts,
ds_factor=1,
stretch="percentile",
cmap="RdYlGn",
aoi=goes_job.target_aoi,
aoi_edgecolor="blue",
)
fig, ax = plot_artifact_patches(
viirs_artifacts, ds_factor=1, stretch="percentile", cmap="Grays", ax=ax, alpha=0.3
)Ignoring fixed y limits to fulfill fixed data aspect with adjustable data limits.
