EO pipeline
CryoHealth-geo ↗ is the Python service that watches glacial lakes from space. Once a day it measures each lake's water area from Sentinel-2 imagery, then scores its outburst hazard. It only computes: whether a score becomes an alert is decided by CryoHealth-api.
Service architecture
Scene to hazard score
Entry points
- Daily scheduler
- APScheduler runs inside the FastAPI process every 24 hours: the observation pass first, then the hazard pass, so scoring always sees that run's newest observation.
- POST /run and POST /run-hazard
- Trigger either pass on demand with the same code the scheduler runs. GET /health reports whether the scheduler is running.
- Backfill CLI
- uv run python -m pipeline.backfill --start YYYY-MM-DD fills history over a date range. It is a terminal command on purpose: imagery quota and run time grow with the range.
Observation pass
The daily run keeps the newest usable scene per lake. A backfill keeps every usable scene in its date range.
- For each of the six monitored lakes, search Sentinel-2 L2A for up to 12 recent scenes over the lake's area of interest (a box reaching about 1 km out from the lake's point).
- Read the green (B03), near-infrared (B08) and scene classification (SCL) bands, already aligned on one 10 m grid.
- Mask cloud, cloud shadow and cirrus pixels (SCL 3, 8, 9, 10). Snow and ice (SCL 11) stay unmasked because glacial lakes border them.
- If more than 50% of the area is masked, skip the scene and try the next one.
- Compute NDWI = (Green − NIR) / (Green + NIR). Pixels above 0 count as water; area is water pixels × 100 m².
- Upsert a row into observations with the area, cloud fraction, scene ID and run ID. One row per lake per day per source; a re-run never overwrites it.
- Flag the lake stale if its newest observation is more than 60 days old, and clear the flag when a fresher one lands.
Scene sources
Both sit behind one SceneSource interface that always returns pixel-aligned bands, so the rest of the pipeline never knows which one it is using.
- Copernicus Data Space (production)
Used when CDSE_CLIENT_ID is set. STAC search finds candidate scenes; the Sentinel Hub Process API, authenticated with OAuth2 client credentials, returns the bands already cropped and reprojected on the server.
- Microsoft Planetary Computer (fallback)
Anonymous, used when no CDSE credentials are present. The service reprojects the box into each scene's UTM zone and upsamples the 20 m SCL band so every band lines up.
Hazard pass
- Read each lake's static fields (dam type, glacier contact, GLOF history) and its last 548 days of observations, then close the database connection before any slow network work.
- Compute mean terrain slope from the Copernicus GLO-30 DEM. Terrain does not change, so the result is cached per process.
- Count population within 5 km of the lake (a square buffer) from the WorldPop Pakistan 2025 raster, downloaded once (about 140 MB) and cached.
- Compute the hazard index and tier with pure functions in pipeline/hazard.py.
- POST the score, tier and full components to CryoHealth-api's /alerts/hazard-scores with the service API key. The API stores the score and decides whether a tier change raises an alert.
In both passes a failure at one lake is recorded against that lake and the run moves on to the next.
Hazard index
Methodology version 1.0. Six components are each turned into a risk from 0 to 1 and combined as a weighted sum. A missing signal counts as zero risk rather than shifting its weight onto the others. Every score is stored with its raw inputs, risks, weights and thresholds, so it can be recomputed from the database alone.
| Component | Weight | Risk |
|---|---|---|
| Area growth (30 and 90 days) | 0.30 | Growth clamped to 0–50% and mapped to 0–1; the two windows are averaged. Shrinking is not scored. |
| Dam type | 0.20 | moraine 1.0 · ice 0.8 · unknown 0.5 · bedrock 0.2 |
| Historical GLOF | 0.15 | 1.0 if the lake has a recorded GLOF, else 0 |
| Seasonal anomaly | 0.15 | Latest area against the same month in other years, clamped to 0–30% |
| Glacier contact | 0.10 | 1.0 if still in contact with its glacier, else 0.3 |
| Slope | 0.10 | Mean slope clamped to 0–40° and mapped to 0–1 |
Tiers
- critical ≥ 0.65
- high ≥ 0.45
- watch ≥ 0.25
- normal < 0.25
Population exposure is stored with the score to help prioritise lakes. It never changes the tier.
Configuration
CDSE_CLIENT_ID, CDSE_CLIENT_SECRET- Copernicus OAuth client; selects the production scene source
CRYOHEALTH_API_URL- Base URL of CryoHealth-api
CRYOHEALTH_API_KEY- Must equal CryoHealth-api's GEO_SERVICE_API_KEY
DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME- Shared PostgreSQL + PostGIS; defaults match local docker-compose (port 5433)
CRYOHEALTH_CACHE_DIR- Where the WorldPop raster is cached
Every merge to main that passes CI builds a Docker image, pushes it to GitHub Container Registry, and redeploys the service on the backend host.
Known limitations
- Weights and tier thresholds are a documented starting point. None of the six lakes has had a GLOF in the observed period, so they are not yet calibrated.
- Only growth counts as risk. A lake draining fast, which can mean an outburst is underway, is not detected by this index.
- Exposure uses a straight-line 5 km square, not a modelled flood path, and does not change the tier.
- Each lake is measured inside a fixed box around its point, not its digitized outline.
- Very small lakes give noisy areas from day to day; the growth clamp limits how far one bad reading can move the score.
- POST /run and /run-hazard have no authentication, so the service must stay on a private network.
Source documents
- CryoHealth-geo/pipeline ↗ — the service code: one module per step described on this page.
- CryoHealth-geo/docs/HAZARD_METHODOLOGY.md ↗ — the full hazard methodology, kept in step with pipeline/hazard.py.
- cryohealth/docs/architecture ↗ — source for both diagrams above.
Where this service fits among the others is shown in System architecture; the observations, lakes and hazard_scores tables are in Schema design.