Skip to main content

pysentinel2

A local Sentinel-2 datacube that fills itself on demand. Every pixel this machine ever downloads lands in one sparse, pixel-indexed store — so nothing is ever downloaded twice: overlapping areas, extended date ranges and repeat runs all reuse the same chunks. Part of the Borevitz Lab ecosystem; the default source is Digital Earth Australia's ARD collections (ga_s2am_ard_3 / ga_s2bm_ard_3) via STAC.

Full documentation — architecture, grid geometry, storage, cleaning, indices, robustness — is in docs/, with flowcharts and figures generated from a real store.

Every stored solar day for the example window Contents of the store for a 2 × 2 km example window. Clear, cloudy and off-swath days are all stored raw and classified at read time.

How it works

{data_root}/sentinel2_cube/
├── index.db      # SQLite: coverage rects · seen scenes · past searches
└── cube.zarr/
    ├── 2024-01-03/   # one group per solar day
    │   ├── nbart_red # arrays on a fixed EPSG:6933 10 m global grid
    │   └── ...       # sparse: only written 256×256-px chunks exist on disk
    └── 2024-01-08/ ...
  • Any bbox maps deterministically to a pixel window on the fixed grid. Cube.get_ds(bbox, start, end) subtracts each day's recorded coverage rectangles from that window and downloads only the missing pixels — coverage accounting is pixel-exact, so small farms pay no chunk padding (256×256-px chunks remain the storage unit inside the Zarr arrays).
  • STAC results are cached (full item JSON) in the index, so re-reads and re-fills of known regions work without re-searching. Cloud-cover filtering happens at read time from the index — relaxing the threshold later needs no re-search.
  • Only raw bands (incl. fmask) are stored. get_ds(..., clean=True) applies cloud masking on read — there is no second "clean" copy on disk, roughly halving storage versus a raw+clean layout. See Cleaning & masking for exactly what the mask does.
  • Spectral indices — NDVI, CFI, NIRv, NDTI, CAI — are on-read derivatives too: get_ds(..., indices=('NDVI', 'NIRv')) computes them from cloud-masked reflectance and stores nothing.
  • Writes are whole-chunk and the index is transactional (SQLite/WAL): a crash mid-fill just leaves cells unmarked, and the next run resumes.

Usage

The core API is troi-agnostic — just a bbox and dates, no setup:

from datetime import date
from pysentinel2.cube import Cube

cube = Cube()
bbox = [148.36265, -33.52606, 148.38265, -33.50606]  # [W, S, E, N]

ds_raw = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31))
ds     = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31), clean=True)
ds     = cube.get_ds(bbox, date(2024, 1, 1), date(2024, 12, 31),
                     indices=('NDVI', 'CFI', 'NIRv', 'NDTI', 'CAI'))

cube.fill(bbox, date(2024, 1, 1), date(2024, 12, 31))  # → 0: already local

Pipelines that speak the shared troi.troi.Troi (the reproducibility layer — stubs, registry) use the adapters:

ds = cube.get_ds_troi(troi)            # = cube.get_ds(troi.bbox, troi.start, troi.end)

download_sentinel2(troi) and clean_sentinel2(troi) remain as thin wrappers over Cube.get_ds_troi for pipeline compatibility.

Package design (shared across the lab's packages — no inheritance, composition only):

  • Troi (from troi) — identity: what region, what dates.
  • Sentinel2 (pysentinel2.sentinel2) — config: STAC URL, collections, bands, CRS, cloud threshold, fmask codes.
  • Paths (pysentinel2.paths) — derived locations of the store for a given Config.
  • grid — the fixed global grid (pure, offline-testable math).
  • Index (pysentinel2.index) — the SQLite ledger.
  • Cube (pysentinel2.cube) — ties them together.

Cleaning & masking

clean=True (and any indices= request, which implies it) runs the window through pysentinel2.cube.clean_dataset. The design principle: invalid and contaminated are different things.

Pixel state fmask Meaning Treatment
Invalid 0 (nodata) Outside the scene footprint / never sensed → NaN; counts against coverage, not against cloudiness
Clear 1 Usable land observation kept
Cloud 2 Contaminated → NaN (dilated)
Shadow 3 Contaminated → NaN (dilated)
Snow 4 Surface state; corrupts vegetation statistics → NaN by default (mask_snow=False to keep); never counts toward the frame gate
Water 5 Legitimate signal (NDWI, dams, rivers) kept by default (mask_water=True to drop)

Contaminated pixels are dilated before masking, frames are gated on the two fractions independently, and every read is annotated with the statistics and thresholds that produced it. Nothing is persisted — different thresholds on the same window are just different reads of the same raw store. Full pipeline, tunables and figures: docs/cleaning.md.

Performance

Live measurements against DEA — a ~2 × 2 km AOI, 11-band ARD at 10 m (one cell = one 256 × 256-px chunk on one solar day):

Scenario Downloaded Time
Cold fill — 3 weeks (3 clear scenes) 12 cells 5.7 s
Same request again nothing 0.0 s
AOI shifted 1 km (inside cached chunks) nothing 0.0 s
Date range extended +1 month 32 cells — new days only 17.2 s
Read cached window (512² px × 3 days × 11 bands) 0.13 s
Read cached window, cloud-masked (clean=True) 0.23 s

Store footprint: 13.6 MB for 11 solar days — raw + fmask only, since the clean cube is a 0.1 s on-read transform rather than a second copy.

Absolute times vary with network and DEA load. The zero rows are the significant ones: those requests are resolved by index lookups alone, with no network access.

Multi-year fills are batched, not per-day: all 11 bands for up to 64 missing days come down in one bulk load per batch (see the fill algorithm), keeping the I/O threads saturated across day boundaries — a two-month cold fill measured 20-26 s where a per-day loop measured 33 s on a healthy DEA and 270 s on a degraded one, and the gap widens with the length of the range. An earlier fmask-first screening pass was removed after measurement: it skipped 8.8% of days' reflectance while paying an extra request round on every day.

Install

pip

pip install git+https://github.com/thestochasticman/pysentinel2.git

Dependencies (the troi core included, pulled from GitHub) are declared in pyproject.toml and installed automatically.

From source

git clone https://github.com/thestochasticman/pysentinel2.git
cd pysentinel2
pip install -e .

The wheels for rasterio/rioxarray/opencv bundle their native libraries on common platforms; in a conda environment the conda-forge equivalents are used instead if already installed.

Robustness notes

Hardening for DEA's public S3 + STAC quirks (cold-start 504s, stalled reads, corrupt tiles) is built in — see docs/robustness.md and, for the underlying investigations, diagnostics.md.

Test

# offline (pure math + synthetic store):
python pysentinel2/grid.py    # True
python pysentinel2/index.py   # True
python pysentinel2/paths.py   # True
python pysentinel2/cube.py    # True

# live (small real downloads from DEA, incl. dedup assertions):
python pysentinel2/download_sentinel2.py  # True
python pysentinel2/clean_sentinel2.py     # True

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pysentinel2-0.1.0.tar.gz (34.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pysentinel2-0.1.0-py3-none-any.whl (34.8 kB view details)

Uploaded Python 3

File details

Details for the file pysentinel2-0.1.0.tar.gz.

File metadata

  • Download URL: pysentinel2-0.1.0.tar.gz
  • Upload date:
  • Size: 34.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.15

File hashes

Hashes for pysentinel2-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c932ee9a22f6d62dcbc1f8700f10a6e73896735110a355f444effc2a253e56f5
MD5 88d003cc12aa2dbf0072c7d269742fa7
BLAKE2b-256 f04e69fc1c620058dcf3cc845fb625bfc6b70fbaa6ca0b7174d593c99fe547e2

See more details on using hashes here.

File details

Details for the file pysentinel2-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pysentinel2-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 34.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.15

File hashes

Hashes for pysentinel2-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7235f6068636d1d916da231fa7de586febfd9b79d309f23e22f729d9122f244e
MD5 131ddc0f5d984ecf9f897a105c0ae4a8
BLAKE2b-256 25a34239895cbb3bea205831214b1b18d3b2f47c8cbeeff9638763d9aea725bb

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

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