Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
* Added support for **common band names** from the [STAC EO extension](https://github.com/stac-extensions/eo?tab=readme-ov-file#common-band-names)
in **Sentinel-2 analysis mode**. The `variables` parameter now accepts standard
spectral names such as `blue`, `green`, `red`, `nir`, and others.
* Fix: CRS information is missing in Sentinel-2 product data variables since
CPM v2.6.2. CRS is now correctly read from the dataset’s `other_metadata`
attributes in the datatree.


## Changes in 0.2.3 (from 2025-10-23)

- **Sentinel-3 SLSTR Level-1 RBT products** are now supported in analysis mode. This
* **Sentinel-3 SLSTR Level-1 RBT products** are now supported in analysis mode. This
allows data from grids a, b, f, and i — in both nadir and oblique viewing
geometries — to be represented on a unified grid within a single dataset.
- **Sentinel-3 SLSTR datasets** are now terrain-corrected using the elevation
* **Sentinel-3 SLSTR datasets** are now terrain-corrected using the elevation
information provided within the product itself.


Expand Down
4 changes: 2 additions & 2 deletions examples/introduction_xarray_eopf_plugin.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -76540,7 +76540,7 @@
"source": [
"ds = xr.open_datatree(\n",
" item.assets[\"product\"].href,\n",
" **item.assets[\"product\"].extra_fields[\"xarray:open_datatree_kwargs\"]\n",
" **item.assets[\"product\"].extra_fields[\"xarray:open_datatree_kwargs\"],\n",
")\n",
"ds"
]
Expand Down Expand Up @@ -82893,7 +82893,7 @@
"source": [
"ds = xr.open_dataset(\n",
" item.assets[\"product\"].href,\n",
" **item.assets[\"product\"].extra_fields[\"xarray:open_datatree_kwargs\"]\n",
" **item.assets[\"product\"].extra_fields[\"xarray:open_datatree_kwargs\"],\n",
")\n",
"ds"
]
Expand Down
47,722 changes: 40,762 additions & 6,960 deletions examples/open-sen2.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions examples/open-sen3.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -17828,7 +17828,7 @@
"fig, ax = plt.subplots(1, 2, figsize=(12, 5))\n",
"ds.s7_bt_in[::2, ::2].plot(ax=ax[0], robust=True)\n",
"ds.s7_bt_io[::2, ::2].plot(ax=ax[1], robust=True)\n",
"plt.tight_layout() "
"plt.tight_layout()"
]
},
{
Expand Down Expand Up @@ -20146,7 +20146,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.13.9"
"version": "3.13.7"
}
},
"nbformat": 4,
Expand Down
4 changes: 3 additions & 1 deletion integration/test_sen1_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ def test_open_datatree_sen1_slc(self):
"/S01SIWSLC_20231119T170635_0027_A293_178F_063021_VH_IW1_249411/measurements",
dt.groups,
)
ds = dt.S01SIWSLC_20231119T170635_0027_A293_178F_063021_VH_IW1_249411.measurements
ds = (
dt.S01SIWSLC_20231119T170635_0027_A293_178F_063021_VH_IW1_249411.measurements
)
self.assertEqual({"azimuth_time": 1501, "slant_range_time": 22694}, ds.sizes)

def test_open_datatree_sen1_onc(self):
Expand Down
25 changes: 17 additions & 8 deletions integration/test_sen2_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,34 @@

class Sentinel2AnalysisTest(TestCase):
def test_open_dataset_sen2_l1c_s3(self):
self._test_open_dataset_sen2_l1c(f"s3://{s02msil1c_bucket}/{path_prefix}")
self._test_open_dataset_sen2_l1c(
f"s3://{s02msil1c_bucket}/{path_prefix}/{l1c_filename}"
)

def test_open_dataset_sen2_l2a_s3(self):
self._test_open_dataset_sen2_l2a(f"s3://{s02msil2a_bucket}/{path_prefix}")
self._test_open_dataset_sen2_l2a(
f"s3://{s02msil2a_bucket}/{path_prefix}/{l2a_filename}"
)

def test_open_dataset_sen2_l1c_https(self):
self._test_open_dataset_sen2_l1c(
f"{DEFAULT_ENDPOINT_URL}/{s02msil1c_bucket}/{path_prefix}"
f"{DEFAULT_ENDPOINT_URL}/{s02msil1c_bucket}/{path_prefix}/{l1c_filename}"
)

def test_open_dataset_sen2_l2a_https(self):
self._test_open_dataset_sen2_l2a(
f"{DEFAULT_ENDPOINT_URL}/{s02msil2a_bucket}/{path_prefix}"
f"{DEFAULT_ENDPOINT_URL}/{s02msil2a_bucket}/{path_prefix}/{l2a_filename}"
)

def test_open_dataset_sen2_l2a_https_cpm_v262(self):
self._test_open_dataset_sen2_l2a(
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202511-"
"s02msil2a-eu/09/products/cpm_v262/S2C_MSIL2A_20251109T112321_N0511_"
"R037_T29TNF_20251109T130709.zarr"
)

def _test_open_dataset_sen2_l1c(self, url_prefix):
def _test_open_dataset_sen2_l1c(self, url):
# See https://stac.browser.user.eopf.eodc.eu/collections/sentinel-2-l1c/items/S2B_MSIL1C_20250415T142749_N0511_R139_T25WEV_20250415T180239
url = f"{url_prefix}/{l1c_filename}"
with timeit("open " + url) as result:
# noinspection PyTypeChecker
ds = xr.open_dataset(
Expand All @@ -58,8 +68,7 @@ def _test_open_dataset_sen2_l1c(self, url_prefix):
for var_name in ds.data_vars:
self.assertEqual((10980, 10980), ds[var_name].shape[-2:], msg=var_name)

def _test_open_dataset_sen2_l2a(self, url_prefix):
url = f"{url_prefix}/{l2a_filename}"
def _test_open_dataset_sen2_l2a(self, url):
with timeit("open " + url) as result:
# noinspection PyTypeChecker
ds = xr.open_dataset(
Expand Down
20 changes: 10 additions & 10 deletions integration/test_sen3_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
import xarray as xr

from integration.helpers import assert_dataset_is_chunked
from xarray_eopf.constants import DEFAULT_ENDPOINT_URL
from xarray_eopf.utils import timeit


allowed_open_time = 1000 # seconds
show_chunking = False

Expand All @@ -28,15 +26,17 @@
)

ol2lfr_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03olclfr-"
"global/15/products/cpm_v256/S3A_OL_2_LFR____20251015T050206_20251015T050506_"
"20251015T070316_0179_131_290_2340_PS1_O_NR_003.zarr"
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202505-s03olclfr/27/"
"products/cpm_v256/S3B_OL_2_LFR____20250527T084123_20250527T084423_20250606T"
"121000_0179_107_064_2340_ESA_O_NT_003.zarr"
)

sl1rbt_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03slsrbt-global/"
"16/products/cpm_v256/S3B_SL_1_RBT____20251016T072510_20251016T072810_"
"20251016T092049_0179_112_163_2700_ESA_O_NR_004.zarr"
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202505-s03slsrbt/30/"
"products/cpm_v256/S3B_SL_1_RBT____20250530T072251_20250530T072551_20250623T2"
"24053_0179_107_106_2340_ESA_O_NT_004.zarr"
)

sl2lst_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03slslst-eu/16/"
"products/cpm_v256/S3B_SL_2_LST____20251016T215803_20251016T220103_20251017T004323_"
Expand All @@ -57,12 +57,12 @@ def test_open_dataset_sen3_olci_l1_err(self):

def test_open_dataset_sen3_olci_l2_lfr(self):
expected_vars = ["gifapar", "iwv", "otci"]
expected_size = (4790, 5125)
expected_size = (4789, 5125)
self._test_sen3(ol2lfr_url, expected_vars, expected_size)

def test_open_dataset_sen3_slstr_l1_rbt(self):
expected_vars = ["s1_radiance_an", "s7_bt_in", "s7_bt_io"]
expected_size = (2959, 3308)
expected_size = (2944, 3313)
self._test_sen3(sl1rbt_url, expected_vars, expected_size)

def test_open_dataset_sen3_slstr_l2_lst(self):
Expand Down
16 changes: 7 additions & 9 deletions integration/test_sen3_native.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,20 @@
"products/cpm_v256/S3B_OL_1_EFR____20250819T074058_20250819T074358_"
"20250819T092155_0179_110_106_3420_ESA_O_NR_004.zarr"
)

ol1err_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03olcerr-global/"
"19/products/cpm_v256/S3A_OL_1_ERR____20251019T145533_20251019T153950_"
"20251019T165332_2657_131_353______PS1_O_NR_004.zarr"
)

ol2lfr_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03olclfr-"
"global/15/products/cpm_v256/S3A_OL_2_LFR____20251015T050206_20251015T050506_"
"20251015T070316_0179_131_290_2340_PS1_O_NR_003.zarr"
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202511-s03olclfr-eu/11/"
"products/cpm_v262/S3B_OL_2_LFR____20251111T092324_20251111T092624_20251111"
"T113927_0179_113_150_2160_ESA_O_NR_003.zarr"
)
sl1rbt_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03slsrbt-global/"
"16/products/cpm_v256/S3B_SL_1_RBT____20251016T072510_20251016T072810_"
"20251016T092049_0179_112_163_2700_ESA_O_NR_004.zarr"
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202511-s03slsrbt-eu/03/"
"products/cpm_v262/S3A_SL_1_RBT____20251103T083134_20251103T083434_20251103T104711"
"_0179_132_178_2340_PS1_O_NR_004.zarr"
)
sl2lst_url = (
"https://objects.eodc.eu/e05ab01a9d56408d82ac32d69a5aae2a:202510-s03slslst-eu/16/"
Expand Down Expand Up @@ -87,7 +85,7 @@ def test_open_dataset_sen3_ol1err_subgroup(self):

def test_open_datatree_sen3_ol2lfr(self):
self._test_open_datatree_sen3(
ol2lfr_url, 9, "measurements", {"columns": 4865, "rows": 4091}, 5
ol2lfr_url, 8, "measurements", {"columns": 4865, "rows": 4091}, 5
)

def test_open_dataset_sen3_ol2lfr(self):
Expand Down
30 changes: 25 additions & 5 deletions tests/amodes/test_sentinel2.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ def test_process_metadata(self: TestCase):

def test_assign_grid_mapping(self: TestCase):
def make_band():
return xr.DataArray(
np.zeros((10, 10)), dims=("y", "x"), attrs={"proj:epsg": 32632}
)
return xr.DataArray(np.zeros((10, 10)), dims=("y", "x"))

dataset = self.mode.assign_grid_mapping(
xr.Dataset(
Expand All @@ -57,6 +55,7 @@ def make_band():
b02=make_band(),
b03=make_band(),
),
attrs={"horizontal_CRS_code": "ESPG:32632"},
)
)
self.assertIn("spatial_ref", dataset)
Expand All @@ -68,9 +67,30 @@ def make_band():
self.assertEqual("spatial_ref", dataset.b03.attrs.get("grid_mapping"))

def test_assign_grid_mapping_fail(self: TestCase):
def make_band():
return xr.DataArray(np.zeros((10, 10)), dims=("y", "x"))

dataset = self.mode.assign_grid_mapping(
xr.Dataset(
dict(
b01=make_band(),
b02=make_band(),
b03=make_band(),
),
attrs={"horizontal_CRS_code": "ESPG:-1"},
)
)
self.assertNotIn("spatial_ref", dataset)
self.assertEqual(None, dataset.b01.attrs.get("grid_mapping"))
self.assertEqual(None, dataset.b02.attrs.get("grid_mapping"))
self.assertEqual(None, dataset.b03.attrs.get("grid_mapping"))

# test with wrong parameters in data variable attributes
def make_band():
return xr.DataArray(
np.zeros((10, 10)), dims=("y", "x"), attrs={"proj:epsg": -1}
np.zeros((10, 10)),
dims=("y", "x"),
attrs={"proj:epsg": -1},
)

dataset = self.mode.assign_grid_mapping(
Expand All @@ -79,7 +99,7 @@ def make_band():
b01=make_band(),
b02=make_band(),
b03=make_band(),
),
)
)
)
self.assertNotIn("spatial_ref", dataset)
Expand Down
2 changes: 1 addition & 1 deletion tests/amodes/test_sentinel3.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import xarray as xr
import zarr

from tests.helpers import make_s3_olci_efr, make_s3_slstr_rbt, make_s3_slstr_lst
from tests.helpers import make_s3_olci_efr, make_s3_slstr_lst, make_s3_slstr_rbt
from xarray_eopf.amode import AnalysisModeRegistry
from xarray_eopf.amodes.sentinel3 import Sen3Ol1Efr, Sen3Sl1Rbt, Sen3Sl2Lst, register
from xarray_eopf.constants import FloatInt
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# https://opensource.org/license/apache-2-0.

from .sentinel2 import make_s2_msi, make_s2_msi_l1c, make_s2_msi_l2a
from .sentinel3 import make_s3_olci_efr, make_s3_slstr_rbt, make_s3_slstr_lst
from .sentinel3 import make_s3_olci_efr, make_s3_slstr_lst, make_s3_slstr_rbt

__all__ = [
"make_s2_msi",
Expand Down
24 changes: 10 additions & 14 deletions tests/helpers/sentinel2.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,31 @@


def make_s2_msi(r10m_size: int = 48) -> xr.DataTree:
return create_datatree(
dt = create_datatree(
{
"r10m": make_s2_msi_r10m(r10m_size),
"r20m": make_s2_msi_l1c_r20m(r10m_size),
"r60m": make_s2_msi_l1c_r60m(r10m_size),
}
)
dt.attrs["other_metadata"] = {"horizontal_CRS_code": "EPSG:32632"}
return dt


def make_s2_msi_l1c(r10m_size: int = 48) -> xr.DataTree:
return create_datatree(
dt = create_datatree(
{
"measurements/reflectance/r10m": make_s2_msi_l1c_r10m(r10m_size),
"measurements/reflectance/r20m": make_s2_msi_l1c_r20m(r10m_size),
"measurements/reflectance/r60m": make_s2_msi_l1c_r60m(r10m_size),
},
attrs={
"other_metadata": {
"horizontal_CRS_code": "EPSG:32632",
}
},
}
)
dt.attrs["other_metadata"] = {"horizontal_CRS_code": "EPSG:32632"}
return dt


def make_s2_msi_l2a(r10m_size: int = 48) -> xr.DataTree:
return create_datatree(
dt = create_datatree(
{
"conditions/mask/l2a_classification/r20m": make_s2_msi_l2a_scl_r20m(
r10m_size
Expand All @@ -48,12 +47,9 @@ def make_s2_msi_l2a(r10m_size: int = 48) -> xr.DataTree:
"measurements/reflectance/r60m": make_s2_msi_l2a_r60m(r10m_size),
"quality/probability/r20m": make_s2_msi_l2a_probs_r20m(r10m_size),
},
attrs={
"other_metadata": {
"horizontal_CRS_code": "EPSG:32632",
}
},
)
dt.attrs["other_metadata"] = {"horizontal_CRS_code": "EPSG:32632"}
return dt


def make_s2_msi_l1c_r10m(r10m_size: int) -> xr.Dataset:
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/sentinel3.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# Permissions are hereby granted under the terms of the Apache 2.0 License:
# https://opensource.org/license/apache-2-0.

from typing import Any
from collections.abc import Sequence
from typing import Any

import dask.array as da
import numpy as np
Expand Down
3 changes: 3 additions & 0 deletions tests/test_amode.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ def convert_datatree(
) -> xr.Dataset:
return datatree.dataset

def process_metadata(self, datatree: xr.DataTree) -> dict:
return {}


class AnalysisModeTest(TestCase):
def setUp(self):
Expand Down
1 change: 0 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from unittest import TestCase

import numpy as np
import pytest
import xarray as xr

Expand Down
11 changes: 11 additions & 0 deletions xarray_eopf/amode.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,17 @@ def convert_datatree(
A transformed data tree.
"""

@abstractmethod
def process_metadata(self, datatree: xr.DataTree) -> dict:
"""Extracts metadata from DataTree's attributes

Args:
datatree: The DataTree containing metadata in its attributes.

Returns:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually Return, not Returns.

Suggested change
Returns:
Return:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But Google Style docstrings says Returns, see https://www.geeksforgeeks.org/python/python-docstrings/, or do I miss something here?

Dictionary containing the metadata.
"""


class AnalysisModeRegistry:
"""A simple registry for `AnalysisMode` instances."""
Expand Down
Loading