Skip to content

Commit 0125e69

Browse files
committed
Add suport for S3 SLSTR #11
1 parent d12cbc5 commit 0125e69

File tree

7 files changed

+48885
-29
lines changed

7 files changed

+48885
-29
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/).
66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
- Add support for the following Sentinel 3 OLCI products: `S03OLCEFR`, `S03OLCERR`, `S03OLCLFR`, `S03OLCLRR` [#9](https://github.com/EOPF-Sample-Service/eopf-stac/issues/9)
12+
- Add support for the following Sentinel 3 SLSTR products: `S03SLSRBT`, `S03SLSLST` [#11](https://github.com/EOPF-Sample-Service/eopf-stac/issues/11)
13+
- Add tests [#10](https://github.com/EOPF-Sample-Service/eopf-stac/issues/10)
14+
715
## [0.8.0] - 2025-04-15
816

917
### Added

src/eopf_stac/common/constants.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,35 @@
2424
SUPPORTED_PRODUCT_TYPES_S2 = ["S02MSIL1C", "S02MSIL2A"]
2525

2626
SUPPORTED_S3_OLCI_L1_PRODUCT_TYPES = ["S03OLCEFR", "S03OLCERR"]
27-
SUPPORTED_S3_OLCI_L2_PRODUCT_TYPES = ["S03OLCLFR"]
28-
SUPPORTED_PRODUCT_TYPES_S3 = SUPPORTED_S3_OLCI_L1_PRODUCT_TYPES + SUPPORTED_S3_OLCI_L2_PRODUCT_TYPES
27+
SUPPORTED_S3_OLCI_L2_PRODUCT_TYPES = ["S03OLCLFR", "S03OLCLRR"]
28+
SUPPORTED_S3_SLSTR_L1_PRODUCT_TYPES = ["S03SLSRBT"]
29+
SUPPORTED_S3_SLSTR_L2_LST_PRODUCT_TYPE = ["S03SLSLST"]
30+
# SUPPORTED_S3_SLSTR_L2_FRP_PRODUCT_TYPE = ["S03SLSFRP"] # conversion error
31+
SUPPORTED_PRODUCT_TYPES_S3 = (
32+
SUPPORTED_S3_OLCI_L1_PRODUCT_TYPES
33+
+ SUPPORTED_S3_OLCI_L2_PRODUCT_TYPES
34+
+ SUPPORTED_S3_SLSTR_L1_PRODUCT_TYPES
35+
+ SUPPORTED_S3_SLSTR_L2_LST_PRODUCT_TYPE
36+
)
2937

3038
# Other Sentinen-3 product types to support
31-
# SRAL
32-
# SLSTR
33-
# SYN
39+
SUPPORTED_S3_SRAL_L1_PRODUCT_TYPES = ["S03AHRL1B"] # sentinel-3-sra-l1b
40+
SUPPORTED_S3_SRAL_L2_PRODUCT_TYPES = ["S03AHRL2H"] # sentinel-3-sra-l2-lan-hy
41+
SUPPORTED_S3_SYN_L2_PRODUCT_TYPES = [
42+
"S03SYNAOD",
43+
"S03SYNSDR",
44+
"S03SYNV10",
45+
"S03SYNVG1",
46+
"S03SYNVGK",
47+
"S03SYNVGP",
48+
] # sentinel-3-syn-l2-aod, sentinel-3-syn-l2, sentinel-3-syn-l2-v10, sentinel-3-syn-l2-vg1, ?, sentinel-3-syn-l2-vgp
49+
50+
51+
# other SRAL listed in [1]
52+
# - S03AHRL1A (SR_1_SRA_A_), S03ALRL1A (SR_1_SRA_A_), S03ALRL1B (SR_1_SRA_BS), S03ALRL2H (SR_2_LAN_HY)
53+
# other SYN listed in [1]: S03SYNMIS
54+
55+
# [1] https://cpm.pages.eopf.copernicus.eu/eopf-cpm/main/PSFD/3-product-types-naming-rules.html
3456

3557
PRODUCT_TYPE_TO_COLLECTION: Final[dict] = {
3658
"S01SIWGRH": "sentinel-1-l1-grd",
@@ -49,6 +71,10 @@
4971
"S03OLCEFR": "sentinel-3-olci-l1-efr",
5072
"S03OLCERR": "sentinel-3-olci-l1-err",
5173
"S03OLCLFR": "sentinel-3-olci-l2-lfr",
74+
"S03OLCLRR": "sentinel-3-olci-l2-lrr",
75+
"S03SLSRBT": "sentinel-3-slstr-l1-rbt",
76+
"S03SLSFRP": "sentinel-3-slstr-l2-frp",
77+
"S03SLSLST": "sentinel-3-slstr-l2-lst",
5278
}
5379

5480
MEDIA_TYPE_ZARR = "application/vnd+zarr"

src/eopf_stac/sentinel3/constants.py

Lines changed: 166 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pystac.extensions.sat import OrbitState
77
from pystac.item_assets import ItemAssetDefinition
88
from pystac.utils import str_to_datetime
9-
from stactools.sentinel3.constants import SENTINEL_OLCI_BANDS
9+
from stactools.sentinel3.constants import SENTINEL_OLCI_BANDS, SENTINEL_SLSTR_BANDS
1010

1111
from eopf_stac.common.constants import (
1212
DATASET_ASSET_EXTRA_FIELDS,
@@ -205,6 +205,124 @@ def get_olci_bands(band_keys: list[str] | None = None) -> list[dict]:
205205
PRODUCT_METADATA_ASSET_KEY: PRODUCT_METADATA_PATH,
206206
}
207207

208+
# SENTINEL_SLSTR_BANDS
209+
# SLSTR_BANDS_TO_RESOLUTIONS
210+
211+
212+
def get_slstr_bands(band_keys: list[str] | None = None) -> list[dict]:
213+
bands = []
214+
if band_keys is None:
215+
for _, band in SENTINEL_SLSTR_BANDS.items():
216+
bands.append(band.to_dict())
217+
else:
218+
for key in band_keys:
219+
bands.append(SENTINEL_SLSTR_BANDS[key].to_dict())
220+
221+
return bands
222+
223+
224+
SLSTR_L1_ASSETS: dict[str, ItemAssetDefinition] = {
225+
"radiance_an": ItemAssetDefinition.create(
226+
title="TOA radiance - stripe A, nadir view",
227+
media_type=pystac.MediaType.ZARR,
228+
description=("Dataset of the TOA radiances for the 500m grid, stripe A, nadir view"),
229+
roles=[ROLE_DATA, ROLE_DATASET],
230+
extra_fields={"bands": get_slstr_bands(["S01", "S02", "S03", "S04", "S05", "S06"]), "gsd": 500},
231+
),
232+
"radiance_ao": ItemAssetDefinition.create(
233+
title="TOA radiance - stripe A, oblique view",
234+
media_type=pystac.MediaType.ZARR,
235+
description=("Dataset of the TOA radiances for the 500m grid, stripe A, oblique view"),
236+
roles=[ROLE_DATA, ROLE_DATASET],
237+
extra_fields={"bands": get_slstr_bands(["S01", "S02", "S03", "S04", "S05", "S06"]), "gsd": 500},
238+
),
239+
"radiance_bn": ItemAssetDefinition.create(
240+
title="TOA radiance - stripe B, nadir view",
241+
media_type=pystac.MediaType.ZARR,
242+
description=("Dataset of the TOA radiances for the 500m grid, stripe B, nadir view"),
243+
roles=[ROLE_DATA, ROLE_DATASET],
244+
extra_fields={"bands": get_slstr_bands(["S04", "S05", "S06"]), "gsd": 500},
245+
),
246+
"radiance_bo": ItemAssetDefinition.create(
247+
title="TOA radiance - stripe B, oblique view",
248+
media_type=pystac.MediaType.ZARR,
249+
description=("Dataset of the TOA radiances for the 500m grid, stripe B, oblique view"),
250+
roles=[ROLE_DATA, ROLE_DATASET],
251+
extra_fields={"bands": get_slstr_bands(["S04", "S05", "S06"]), "gsd": 500},
252+
),
253+
"BT_in": ItemAssetDefinition.create(
254+
title="TOA brightness temperature - TIR, nadir view",
255+
media_type=pystac.MediaType.ZARR,
256+
description=("Dataset of the TOA brightness temperature for channels S7-S9 and F2 in the 1km grid, nadir view"),
257+
roles=[ROLE_DATA, ROLE_DATASET],
258+
extra_fields={"bands": get_slstr_bands(["S07", "S08", "S09", "S11"]), "gsd": 1000},
259+
),
260+
"BT_io": ItemAssetDefinition.create(
261+
title="TOA brightness temperature - TIR, oblique view",
262+
media_type=pystac.MediaType.ZARR,
263+
description=("Dataset of the TOA brightness temperature for channels S7-S9 and F2, 1km grid, oblique view"),
264+
roles=[ROLE_DATA, ROLE_DATASET],
265+
extra_fields={"bands": get_slstr_bands(["S07", "S08", "S09", "S11"]), "gsd": 1000},
266+
),
267+
"BT_fn": ItemAssetDefinition.create(
268+
title="TOA brightness temperature - F1, nadir view",
269+
media_type=pystac.MediaType.ZARR,
270+
description=("Dataset of the TOA brightness temperature for the F1 channel, 1km grid, nadir view"),
271+
roles=[ROLE_DATA, ROLE_DATASET],
272+
extra_fields={"bands": get_slstr_bands(["S10"]), "gsd": 1000},
273+
),
274+
"BT_fo": ItemAssetDefinition.create(
275+
title="TOA brightness temperature - F1, oblique view",
276+
media_type=pystac.MediaType.ZARR,
277+
description=("Dataset of the TOA brightness temperature for the F1 channel, 1km grid, oblique view"),
278+
roles=[ROLE_DATA, ROLE_DATASET],
279+
extra_fields={"bands": get_slstr_bands(["S10"]), "gsd": 1000},
280+
),
281+
PRODUCT_ASSET_KEY: get_item_asset_product(),
282+
PRODUCT_METADATA_ASSET_KEY: get_item_asset_metadata(),
283+
}
284+
285+
SLSTR_L1_ASSETS_KEY_TO_PATH: dict[str:str] = {
286+
"radiance_an": "measurements/anadir",
287+
"radiance_ao": "measurements/aoblique",
288+
"radiance_bn": "measurements/bnadir",
289+
"radiance_bo": "measurements/boblique",
290+
"BT_in": "measurements/inadir",
291+
"BT_io": "measurements/ioblique",
292+
"BT_fn": "measurements/fnadir",
293+
"BT_fo": "measurements/foblique",
294+
PRODUCT_ASSET_KEY: "",
295+
PRODUCT_METADATA_ASSET_KEY: PRODUCT_METADATA_PATH,
296+
}
297+
298+
SLSTR_L2_LST_ASSETS: dict[str, ItemAssetDefinition] = {
299+
"lst": ItemAssetDefinition.create(
300+
title="Land Surface Temperature (LST)",
301+
media_type=pystac.MediaType.ZARR,
302+
description=("Gridded Land Surface Temperature generated on the wide 1 km measurement grid"),
303+
roles=[ROLE_DATA, ROLE_DATASET],
304+
extra_fields={"bands": get_slstr_bands(["S07", "S08", "S09"]), "gsd": 1000},
305+
),
306+
PRODUCT_ASSET_KEY: get_item_asset_product(),
307+
PRODUCT_METADATA_ASSET_KEY: get_item_asset_metadata(),
308+
}
309+
310+
SLSTR_L2_LST_ASSETS_KEY_TO_PATH: dict[str:str] = {
311+
"lst": "measurements",
312+
PRODUCT_ASSET_KEY: "",
313+
PRODUCT_METADATA_ASSET_KEY: PRODUCT_METADATA_PATH,
314+
}
315+
316+
SLSTR_L2_FRP_ASSETS: dict[str, ItemAssetDefinition] = {
317+
PRODUCT_ASSET_KEY: get_item_asset_product(),
318+
PRODUCT_METADATA_ASSET_KEY: get_item_asset_metadata(),
319+
}
320+
321+
SLSTR_L2_FRP_ASSETS_KEY_TO_PATH: dict[str:str] = {
322+
PRODUCT_ASSET_KEY: "",
323+
PRODUCT_METADATA_ASSET_KEY: PRODUCT_METADATA_PATH,
324+
}
325+
208326
# -- Collection metadata
209327

210328
S3_OLCI_L1_EFR = {
@@ -218,7 +336,7 @@ def get_olci_bands(band_keys: list[str] | None = None) -> list[dict]:
218336
"product_type": "S03OLCEFR",
219337
"processing_level": "L1",
220338
"instrument": "olci",
221-
"gsd": 300,
339+
"gsd": [300],
222340
"item_assets": {**OLCI_L1_ASSETS},
223341
}
224342

@@ -233,7 +351,7 @@ def get_olci_bands(band_keys: list[str] | None = None) -> list[dict]:
233351
"product_type": "S03OLCERR",
234352
"processing_level": "L1",
235353
"instrument": "olci",
236-
"gsd": 1200,
354+
"gsd": [1200],
237355
"item_assets": {**OLCI_L1_ASSETS},
238356
}
239357

@@ -246,21 +364,10 @@ def get_olci_bands(band_keys: list[str] | None = None) -> list[dict]:
246364
"product_type": "S03OLCLFR",
247365
"processing_level": "L2",
248366
"instrument": "olci",
249-
"gsd": 300,
367+
"gsd": [300],
250368
"item_assets": {**OLCI_L2_ASSETS},
251369
}
252370

253-
254-
# S03SLSRBT / SL_1_RBT
255-
S3_SLSTR_L1_RBT = {}
256-
257-
# S03SLSLST / SL_2_LST
258-
S3_SLSTR_L2_LST = {}
259-
260-
# TBD: SRAL, SYN
261-
262-
# Conversion not supported by CPM; no mapping
263-
264371
S3_OLCI_L2_LRR = {
265372
"id": "sentinel-3-olci-l2-lrr",
266373
"title": "Sentinel-3 OLCI Level-2 LRR",
@@ -270,10 +377,53 @@ def get_olci_bands(band_keys: list[str] | None = None) -> list[dict]:
270377
"product_type": "S03OLCLRR",
271378
"processing_level": "L2",
272379
"instrument": "olci",
273-
"gsd": 1200,
380+
"gsd": [1200],
274381
"item_assets": {**OLCI_L2_ASSETS},
275382
}
276383

384+
S3_SLSTR_L1_RBT = {
385+
"id": "sentinel-3-slstr-l1-rbt",
386+
"title": "Sentinel-3 SLSTR Level-1 RBT",
387+
"description": (
388+
"The Sentinel-3 SLSTR Level-1B RBT product provides radiances and brightness temperatures for each pixel "
389+
"in a regular image grid for each view and SLSTR channel. In addition, it also contains annotations data "
390+
"associated with each image pixels."
391+
),
392+
"product_type": "S03SLSRBT",
393+
"processing_level": "L1",
394+
"instrument": "slstr",
395+
"gsd": [500, 1000],
396+
"item_assets": {**SLSTR_L1_ASSETS},
397+
}
398+
399+
S3_SLSTR_L2_LST = {
400+
"id": "sentinel-3-slstr-l2-lst",
401+
"title": "Sentinel-3 SLSTR Level-2 LST",
402+
"description": "The Sentinel-3 SLSTR Level-2 LST product provides land surface temperature.",
403+
"product_type": "S03SLSLST",
404+
"processing_level": "L2",
405+
"instrument": "slstr",
406+
"gsd": [500, 1000],
407+
"item_assets": {**SLSTR_L2_LST_ASSETS},
408+
}
409+
410+
S3_SLSTR_L2_FRP = {
411+
"id": "sentinel-3-slstr-l2-frp",
412+
"title": "Sentinel-3 SLSTR Level-2 FRP",
413+
"description": (
414+
"The Sentinel-3 SLSTR Level-2 FRP product provides global (over land and water) fire radiative power."
415+
),
416+
"product_type": "S03SLSFRP",
417+
"processing_level": "L2",
418+
"instrument": "slstr",
419+
"gsd": [500, 1000],
420+
"item_assets": {**SLSTR_L2_FRP_ASSETS},
421+
}
422+
423+
# TBD: SRAL, SYN
424+
425+
# Conversion not supported by CPM; no mapping
426+
277427
S3_OLCI_L2_WFR = {
278428
"id": "sentinel-3-olci-l2-wfr",
279429
"title": "Sentinel-3 OLCI Level-2 WFR",

src/eopf_stac/sentinel3/stac.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
SENTINEL_LICENSE,
1212
SUPPORTED_S3_OLCI_L1_PRODUCT_TYPES,
1313
SUPPORTED_S3_OLCI_L2_PRODUCT_TYPES,
14+
SUPPORTED_S3_SLSTR_L1_PRODUCT_TYPES,
15+
SUPPORTED_S3_SLSTR_L2_LST_PRODUCT_TYPE,
1416
THUMBNAIL_ASSET,
1517
)
1618
from eopf_stac.common.stac import (
@@ -31,6 +33,10 @@
3133
OLCI_L2_ASSETS,
3234
OLCI_L2_ASSETS_KEY_TO_PATH,
3335
SENTINEL3_METADATA,
36+
SLSTR_L1_ASSETS,
37+
SLSTR_L1_ASSETS_KEY_TO_PATH,
38+
SLSTR_L2_LST_ASSETS,
39+
SLSTR_L2_LST_ASSETS_KEY_TO_PATH,
3440
)
3541

3642
logger = logging.getLogger(__name__)
@@ -42,7 +48,7 @@ def create_collection(collection_metadata: dict, thumbnail_href: str) -> pystac.
4248
"constellation": [mission_metadata.get("constellation")],
4349
"platform": mission_metadata.get("platforms"),
4450
"instruments": [collection_metadata.get("instrument")],
45-
"gsd": [collection_metadata.get("gsd")],
51+
"gsd": collection_metadata.get("gsd"),
4652
"processing:level": [collection_metadata.get("processing_level")],
4753
"product:type": [collection_metadata.get("product_type")],
4854
}
@@ -152,6 +158,12 @@ def create_item(metadata: dict, product_type: str, asset_href_prefix: str) -> py
152158
elif product_type in SUPPORTED_S3_OLCI_L2_PRODUCT_TYPES:
153159
asset_defintions = OLCI_L2_ASSETS
154160
asset_path_lookups = OLCI_L2_ASSETS_KEY_TO_PATH
161+
elif product_type in SUPPORTED_S3_SLSTR_L1_PRODUCT_TYPES:
162+
asset_defintions = SLSTR_L1_ASSETS
163+
asset_path_lookups = SLSTR_L1_ASSETS_KEY_TO_PATH
164+
elif product_type in SUPPORTED_S3_SLSTR_L2_LST_PRODUCT_TYPE:
165+
asset_defintions = SLSTR_L2_LST_ASSETS
166+
asset_path_lookups = SLSTR_L2_LST_ASSETS_KEY_TO_PATH
155167
else:
156168
raise ValueError(f"Unsupported Sentinel-3 product type '{product_type}'")
157169

0 commit comments

Comments
 (0)