Enriching matchup metadata#
After a catalogue has been populated by
find_and_catalogue(), each matchup
STAC Item carries a small set of core properties (time of overpass, bounding
box, sensor pair, …). The enrichment system lets you compute additional
properties — area, time difference, solar elevation, land fraction, and
anything you define yourself — and write them back to the item JSON so they
persist in the catalogue and can be used to filter matchups.
Overview#
An enricher is any callable with the signature:
def my_enricher(matchup: Matchup) -> Dict[str, Any]: ...
It receives a Matchup and returns a plain
dict of property names to values.
enrich() iterates all matchup items, reconstructs
the Matchup domain object for each, calls every enricher, and merges the
resulting dicts back into item.properties. If the item has a
self_href the updated JSON is written to disk immediately so the properties
survive the process and can be ingested into the central catalogue.
Quick start#
from eomatch.mu_stac import MatchupCatalogue
from eomatch.enrich import enrich
from eomatch.enrich.time_diff import time_diff
from eomatch.enrich.geometric import geometric
from eomatch.enrich.cloud_cover import cloud_cover
In [1]: In [1]: catalogue = MatchupCatalogue.open("/data/my_catalogue")
In [2]: In [2]: enrich(catalogue, enrichers=[time_diff, geometric, cloud_cover])
In [3]: Out[2]: 42
enrich returns the number of matchup items it enriched.
Or from the command line:
eomatch-enrich \
--catalogue /data/my_catalogue \
--enricher eomatch.enrich.time_diff.time_diff \
--enricher eomatch.enrich.geometric.geometric \
--enricher eomatch.enrich.solar_elevation.solar_elevation \
--enricher eomatch.enrich.land_fraction.land_fraction \
--enricher eomatch.enrich.cloud_cover.cloud_cover
Add --overwrite to replace properties that were written in an earlier run.
Add -v / --verbose for debug-level logging.
Built-in enrichers#
All built-in enrichers live as submodules of eomatch.enrich.
solar_elevation and land_fraction require the optional enrich
extra:
pip install 'eomatch[enrich]'
Module |
Property added |
Notes |
|---|---|---|
|
|
Signed seconds: |
|
|
Area on the WGS-84 ellipsoid via |
|
|
Solar elevation at the collocation centroid midpoint. Positive =
daytime, negative = night-time. Requires |
|
|
Fraction of the collocation region over land (0.0 = ocean, 1.0 =
land), computed from Natural Earth 110 m polygons. Requires
|
|
|
Highest |
|
|
Gap in seconds to the nearest valid in-situ sample before/after the
satellite crossover ( |
|
|
Percentage of the collocation region flagged as cloud by the satellite
product’s own classification mask (Sentinel-2 L1C/L2A; for Planet
SuperDove, any pixel not flagged clear). Unlike |
|
|
Fraction (0–1) of the collocation region with valid, non-NaN data. Use it to drop matchups where the image is empty over the target. Reads one band (or the RGB group) of the product. |
Enrichers that read the product#
roi_cloud_cover and roi_valid_fraction (and insitu_time_diff) need
the product itself, so they download it on demand if it is not already local.
Run download_products() first to
control when that happens. To avoid downloading products that can never be
useful, skip matchups using cheap properties that are already known:
min_collocation_area_km2skips matchups whose collocation region is smaller than a threshold.gatetakes the same{property: {operator: value}}filter asget_events(), e.g. only look at matchups whose scene cloud cover is already low.
import functools
from eomatch.enrich.roi_valid_fraction import roi_valid_fraction
In [4]: In [1]: gate = {"cloud_cover": {"lt": 50}}
In [5]: In [2]: enricher = functools.partial(roi_valid_fraction, min_collocation_area_km2=0.1, gate=gate)
In [6]: In [3]: enrich(catalogue, enrichers=[enricher])
In [7]: Out[3]: 17
A skipped matchup simply gets no property. roi_cloud_cover also accepts a
roi polygon to use (intersected with the collocation region) instead of the
whole collocation region.
Keyword arguments can be bound from the command line by appending
:key=value to the enricher path. Values are parsed as Python literals, and
dict or list values are allowed; quote the whole argument:
eomatch-enrich \
--catalogue /data/my_catalogue \
--enricher 'eomatch.enrich.roi_valid_fraction.roi_valid_fraction:min_collocation_area_km2=0.1,gate={"cloud_cover": {"lt": 50}}'
Enriching a subset#
Enriching a large catalogue re-runs every enricher on every matchup, which is
expensive for the enrichers above. Pass matchup_ids to enrich only some
matchups, for example the ones a run has just added:
In [8]: In [1]: enrich(catalogue, enrichers=[cloud_cover], matchup_ids=["mu-a", "mu-b"])
In [9]: Out[1]: 2
Filtering by enriched properties#
Once properties have been written to the catalogue, pass a properties
filter to get_events() to
restrict results to matchups that meet your criteria:
# Keep only matchups with an overpass time difference under 15 minutes
events = catalogue.get_events(
properties={"time_diff_s": {"lt": 900}}
)
# Daytime ocean matchups with a large overlap area
events = catalogue.get_events(
properties={
"solar_elevation_deg": {"gt": 0},
"land_fraction": {"lt": 0.05},
"collocation_area_km2": {"gt": 5000},
}
)
Supported filter operators:
Operator |
Meaning |
|---|---|
|
property < threshold |
|
property ≤ threshold |
|
property > threshold |
|
property ≥ threshold |
|
property == value (also the default when the condition is a plain value) |
|
property != value |
|
property is a member of a list |
Writing a custom enricher#
Any callable that accepts a Matchup and returns
a dict qualifies as an enricher. To make it available on the CLI, place it
in an importable module and pass the dotted path:
# my_package/enrichers.py
from typing import Any, Dict
# Platforms excluded from this analysis, e.g. a known calibration issue
# for a specific date range -- not something eomatch could know about.
_EXCLUDED_PLATFORMS = {"S2B"}
def custom_qc_flag(matchup) -> Dict[str, Any]:
"""Flag matchups involving a platform excluded from this analysis."""
excluded = any(p.platform in _EXCLUDED_PLATFORMS for p in matchup.products)
return {"qc_excluded_platform": excluded}
eomatch-enrich \
--catalogue /data/my_catalogue \
--enricher my_package.enrichers.custom_qc_flag
Enrichers that raise an exception are logged as warnings and skipped for that
item — other enrichers in the same run are unaffected. Pass raise_error=True
to enrich() to raise instead, which is useful when
developing a new enricher.
The overwrite flag#
By default, enrich() skips keys that already exist
in item.properties. Pass overwrite=True (Python) or --overwrite
(CLI) to replace existing values:
# Re-run after updating the land_fraction enricher
enrich(catalogue, enrichers=[land_fraction], overwrite=True)
Ingesting enriched properties into the central catalogue#
Enriched properties are written to the local item.properties dict and
persisted to the on-disk JSON immediately. The next time you run
eomatch-ingest the updated Items — including all new properties — are
pushed to the central pgSTAC catalogue, where they become queryable via the
CQL2 filter extension:
# Local enrichment
eomatch-enrich \
--catalogue /data/my_catalogue \
--enricher eomatch.enrich.time_diff.time_diff \
--enricher eomatch.enrich.land_fraction.land_fraction
# Push to central catalogue
eomatch-ingest --catalogue /data/my_catalogue