Skip to main content

open-sar-triad (Python client)

Discover and download open Synthetic Aperture Radar scenes from ICEYE, Umbra and Capella through one interface.

from opensartriad import Catalog

cat = Catalog()
scenes = cat.search(bbox=(5.9, 47.2, 10.5, 55.1), start="2025-01-01", family="complex")
scenes.download("data/", family="complex", dry_run=True)
  • No API key, no account, no rate limits. The backend is a static catalog on a CDN.
  • No required dependencies. The client is standard library only.
  • Imagery is never proxied. Downloads go straight to each provider's own storage.

Contents


Install

pip install open-sar-triad

The client has no required dependencies: it is standard library only, so this pulls in nothing else.

Optional extras:

pip install "open-sar-triad[pandas]"     # to_dataframe()
pip install "open-sar-triad[geo]"        # geopandas / shapely workflows
pip install "open-sar-triad[stac]"       # pystac interop
pip install "open-sar-triad[notebooks]"  # everything the example notebooks use

From source:

git clone https://github.com/pmuguda/open-sar-triad.git
pip install -e open-sar-triad/python

Quick start

from opensartriad import Catalog

cat = Catalog()

# What is in the catalog?
stats = cat.stats()
print(stats["total"])            # 14798
print(stats["by_provider"])      # {'umbra': 11892, 'capella': 2469, 'iceye': 437}
print(stats["temporal_extent"])  # ['2020-12-31', '2026-08-19']

# Find scenes over an area, in a date window
scenes = cat.search(
    bbox=(5.9, 47.2, 10.5, 55.1),   # west, south, east, north (EPSG:4326)
    start="2025-01-01",
    end="2026-01-01",
)
print(scenes)
# SceneCollection(129 scenes; umbra 128, iceye 1; 2025-01-30 to 2026-07-09)

# Look at one
s = scenes[0]
print(s.id, s.provider, s.date, s.mode, s.formats)

The first search() downloads a ~0.6 MB index and caches it on the Catalog instance. Later searches are local and instant, so reuse one Catalog.


Searching

All filters combine with AND. Omitted filters do nothing.

scenes = cat.search(
    bbox=(72.8, 18.9, 73.1, 19.3),  # (west, south, east, north)
    start="2025-06-01",             # inclusive; str, date or datetime
    end="2025-12-31",               # inclusive
    providers=["umbra", "capella"], # or a single string
    mode="spotlight",               # sensor mode, case-insensitive
    orbit="ascending",              # or "descending"
    look="right",                   # or "left"
    family="complex",               # only scenes that can satisfy this family
    formats=["SLC", "SICD"],        # or exact formats
    limit=100,                      # stop after N matches
)

SceneCollection behaves like a list:

len(scenes)
scenes[0]
scenes[:10]
for s in scenes:
    ...
[s for s in scenes if s.year == 2025]

bbox and the antimeridian. Matching is a bounding-box intersection test, so it is inclusive at the edges: a scene whose footprint box touches your box is returned even if the polygon itself does not overlap. A box crossing ±180° should be passed as two searches.


Working with a scene

Cheap fields come from the search index and cost nothing:

s.id          # provider scene id
s.provider    # 'iceye' | 'umbra' | 'capella'
s.date        # 'YYYY-MM-DD'
s.year        # int
s.mode        # sensor mode, lowercase
s.orbit       # 'ascending' | 'descending' | None
s.look        # 'left' | 'right' | None
s.formats     # ['GEC', 'CSI', 'SICD', 'SIDD', 'CPHD']
s.bbox        # [west, south, east, north]

These trigger a lazy fetch of that provider's full records the first time (then cached):

s.url("SICD")           # download URL for an exact format
s.metadata_url("SICD")  # provider metadata sidecar, or None if not published
s.properties            # polarization, resolution, incidence angle, ...
s.geometry              # GeoJSON footprint
s.assets                # full STAC asset map

Product families

Each provider names the same kind of product differently. Asking for SLC by name returns nothing from Umbra, even though all of its scenes carry complex data, because Umbra labels it SICD.

Families solve this. Ask for what the product is, and each scene resolves to its own provider's equivalent:

Family Resolves in order
detected GEO → GRD → GEC → SIDD
complex SLC → SICD
phase CPHD
visual CSI → VID
s.resolve("complex")    # 'SICD' for Umbra, 'SLC' for ICEYE/Capella
s.resolve("detected")   # 'GEC' for Umbra, 'GRD' for ICEYE, 'GEO' for Capella
s.resolve("phase")      # 'CPHD', or None if that scene has none

Each scene contributes one file per family, so you get the best available product rather than several near-duplicates.


Downloading

SAR products are large. A single GRD can exceed 1 GB. Always dry_run first.

# See what would happen; downloads nothing
scenes.download("data/", family="complex", dry_run=True)
# 6 file(s): 3 data + 3 metadata, from 3 scene(s)
#   [data    ] umbra/2025-07-15-06-10-12_UMBRA-08_SICD.nitf
#   [metadata] umbra/2025-07-15-06-10-12_UMBRA-08.stac.v2.json

# Then commit
paths = scenes.download("data/", family="complex")

Files land in data/<provider>/, with each provider's metadata sidecar saved next to every data file, so your archive arrives documented rather than as a pile of anonymous rasters.

scenes.download(
    dest="data/",
    family="complex",       # or formats=["SLC", "SICD"]
    metadata=True,          # fetch sidecars (default)
    skip_existing=True,     # resume-friendly (default)
    dry_run=False,
    quiet=False,
)

Inspect the plan without downloading:

jobs = scenes.download_urls(family="detected")
len(jobs)
jobs[0]  # {'scene': ..., 'provider': 'umbra', 'format': 'GEC', 'url': ..., 'kind': 'data'}

Downloads are resumable: existing non-empty files are skipped, partial files are written to .part and only renamed on success, and one failed asset does not abort the batch.


Exporting

scenes.to_geojson()             # GeoJSON FeatureCollection (dict)
scenes.save_geojson("aoi.geojson")
scenes.to_stac()                # STAC ItemCollection, for pystac / stackstac
scenes.to_dataframe()           # pandas DataFrame (needs pandas)
df = scenes.to_dataframe()
df.groupby("provider").size()
df.groupby(df["date"].str[:7]).size()   # scenes per month

Plotting

pip install "open-sar-triad[plot]"

Four plots, all drawn against a country basemap:

scenes.plot_coverage()     # footprints on a world map, coloured by provider
scenes.plot_timeline()     # acquisitions over time, stacked by provider
scenes.plot_providers()    # scene counts per provider
scene.plot_footprint()     # one scene, with surrounding geography

Every one returns a matplotlib Axes and accepts ax=, so they compose into your own figures:

import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 1, figsize=(13, 10))
scenes.plot_coverage(ax=axes[0])
scenes.plot_timeline(ax=axes[1], freq="year")
fig.savefig("report.png", dpi=150, bbox_inches="tight")

Coverage options

scenes.plot_coverage(
    footprints=True,            # true acquisition polygons instead of bounding boxes
    bbox=(-10, 35, 30, 60),     # zoom to an area
    basemap=False,              # skip the country outlines
    alpha=0.5,
    title="Scenes over Europe",
)

plot_coverage draws bounding boxes by default, because those come free with the search index. footprints=True is more accurate but downloads the full per-provider records first.

About the basemap

Country outlines come from the same world-atlas data the web map uses, decoded in about forty lines rather than pulling in cartopy (which needs PROJ and GEOS compiled) or contextily (which needs rasterio). It is fetched once and cached under ~/.cache/opensartriad/.

Several mirrors are tried in turn, and if all of them are blocked the plot still renders, without the basemap and with a warning. That matters on networks where public CDNs are unreachable.

from opensartriad.plotting import load_world, draw_basemap

load_world()                      # rings, cached after the first call
draw_basemap(ax, required=True)   # raise instead of warning if unavailable

STAC interoperability

The backend is a STAC catalog, so standard tools work without this client:

import pystac

cat = pystac.Catalog.from_file(
    "https://www.pmuguda.com/open-sar-triad/api/v1/catalog.json"
)
for child in cat.get_children():
    print(child.id, child.title)

Or take search results straight into a STAC workflow:

items = scenes.to_stac()["features"]

Raw endpoints, if you want to skip the client entirely:

Endpoint What
/api/v1/catalog.json STAC root catalog
/api/v1/collections/{provider}.json STAC Collection
/api/v1/items/{provider}.json STAC ItemCollection
/api/v1/index.json compact search index
/api/v1/stats.json counts and extents
/api/v1/scenes/{provider}.geojson raw FeatureCollection

Base URL: https://www.pmuguda.com/open-sar-triad/api/v1


How it works

There is no server. Every endpoint is a file, rebuilt weekly by CI and served from a CDN. That has real consequences worth knowing:

  • Search is local. search() filters a cached index in memory, so it is instant and unmetered. There is nothing to rate limit.
  • Detail is lazy. Download URLs live in the larger per-provider files, which are fetched only when you first ask for a URL, then cached.
  • Reuse one Catalog. A new instance means a new cold cache.
  • Data refreshes weekly. stats()["generated"] tells you when.
cat = Catalog()                              # default hosted API
cat = Catalog("http://localhost:8000/api/v1")  # a local build, for testing

API reference

Catalog(base_url=DEFAULT_BASE_URL, timeout=60)

Method Returns Notes
search(...) SceneCollection see Searching
get(scene_id) Scene | None exact id lookup
all() SceneCollection every scene
stats() dict counts, extents, families
stac_catalog() dict STAC root
stac_collection(provider) dict STAC Collection
stac_items(provider) dict full ItemCollection (large)
license() dict attribution notice

Scene

Attributes id, provider, date, year, mode, orbit, look, formats, bbox (free), and assets, properties, geometry (lazy). Methods url(fmt), metadata_url(fmt), resolve(family).

SceneCollection

A Sequence of Scene. Methods download(...), download_urls(...), to_geojson(), save_geojson(path), to_stac(), to_dataframe().

Exceptions

OpenSarTriadError for any network or API failure.


Licence and attribution

The client code is MIT.

Scene metadata is CC-BY 4.0, published by ICEYE, Umbra and Capella under their open data programs. When you publish derived work, credit the originating provider.

print(Catalog().license()["attribution"])

Imagery is not redistributed by this project. Every download URL points at the provider's own storage and remains subject to that provider's terms.

This is an independent third-party catalog, not endorsed by or affiliated with ICEYE, Umbra or Capella. Scene records are normalized from the STAC catalogs maintained by Jack-Hayes/commerical-sar-stac (MIT).

Metadata

Release files for open-sar-triad 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for open-sar-triad 1.1.0
File Size Uploaded Provenance
open_sar_triad-1.1.0.tar.gz 18.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for open-sar-triad 1.1.0
File Interpreter ABI Platform
open_sar_triad-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 37.4 kB

Release files / open_sar_triad-1.1.0.tar.gz

Download URL open_sar_triad-1.1.0.tar.gz
Size 18.1 kB
Tags Source
SHA-256 checksum
How to use checksums
060d13b3ff916d1c5e4d04faefdccc450e1af0ef6e1d8b83a659613636df1af5
BLAKE2b-256 checksum
How to use checksums
bc5217960b9be383cea19be0d2f0e3e7c92504aa49a41368f656e08a77e012db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Release files / open_sar_triad-1.1.0-py3-none-any.whl

Download URL open_sar_triad-1.1.0-py3-none-any.whl
Size 19.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
01852cead07766e39a72cfcf1589da089679dd3abcd50a1b49587a59bb81d7c9
BLAKE2b-256 checksum
How to use checksums
4ae62a2181541ebb9037d483681692867f7e9e64d137ec53157a81880b423512
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

Security for open-sar-triad 1.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for open_sar_triad-1.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page