Skip to content

Commit 9a97dc5

Browse files
authored
Merge branch 'main' into konstntokas-xxx-add_sen1_anaylsis
2 parents d3f7bb7 + d144814 commit 9a97dc5

9 files changed

Lines changed: 16273 additions & 5719 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
and is no longer used internally.
77
- Add support for Sentinel-1 GRD analysis mode.
88
- Updated year in the headers.
9+
- Added footprint-based subsetting for Sentinel-3 OLCI and SLSTR LST using STAC
10+
metadata, improving performance by avoiding full latitude/longitude grid downloads
11+
during subsetting.
912

1013

1114
## Changes in 0.2.7 (from 2026-03-27)

docs/examples/sentinel_3_analysis.ipynb

Lines changed: 7961 additions & 2818 deletions
Large diffs are not rendered by default.

examples/sentinel_3_analysis.ipynb

Lines changed: 7959 additions & 2816 deletions
Large diffs are not rendered by default.

tests/amodes/test_sentinel3.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
from tests.helpers import make_s3_olci_efr, make_s3_slstr_lst, make_s3_slstr_rbt
1616
from xarray_eopf.amode import AnalysisModeRegistry
17-
from xarray_eopf.amodes.sentinel3 import Sen3Ol1Efr, Sen3Sl1Rbt, Sen3Sl2Lst, register
17+
from xarray_eopf.amodes.sentinel3 import (
18+
Sen3Ol1Efr,
19+
Sen3Sl1Rbt,
20+
Sen3Sl2Lst,
21+
register,
22+
)
1823
from xarray_eopf.constants import FloatInt
1924

2025

@@ -91,7 +96,7 @@ def assert_convert_datatree_ok(
9196
self,
9297
original_dt: xr.DataTree,
9398
expected_var_names: list[str],
94-
expected_size: (int, int),
99+
expected_size: tuple[int, int],
95100
resolution: FloatInt | tuple[FloatInt, FloatInt] | None = None,
96101
bbox: Sequence[float | int] | None = None,
97102
):
@@ -118,6 +123,12 @@ def assert_convert_datatree_fail(self, original_dt: xr.DataTree):
118123
with pytest.raises(ValueError, match="No variables selected"):
119124
self.mode.convert_datatree(original_dt, includes="bibo")
120125

126+
def assert_convert_datatree_fail_with_include_exclude(
127+
self, original_dt: xr.DataTree
128+
):
129+
with pytest.raises(ValueError, match="No variables selected"):
130+
self.mode.convert_datatree(original_dt, includes=".+", excludes=".+")
131+
121132

122133
class OlciEfrTest(Sen3TestMixin, TestCase):
123134
mode = Sen3Ol1Efr()
@@ -163,7 +174,7 @@ def test_convert_datatree_bbox(self):
163174
"oa02_radiance",
164175
"oa03_radiance",
165176
],
166-
expected_size=(372, 421),
177+
expected_size=(372, 454),
167178
bbox=[1, 55, 3, 56],
168179
)
169180

@@ -187,6 +198,21 @@ def test_convert_datatree_raise_warning(self):
187198
def test_convert_datatree_fail(self):
188199
self.assert_convert_datatree_fail(make_s3_olci_efr(size=48))
189200

201+
def test_convert_datatree_fail_include_exclude_overlap(self):
202+
self.assert_convert_datatree_fail_with_include_exclude(
203+
make_s3_olci_efr(size=48)
204+
)
205+
206+
def test_convert_datatree_sets_other_metadata_as_attrs(self):
207+
dt = make_s3_olci_efr(size=100)
208+
dt.attrs["other_metadata"] = {"test_key": "test_val"}
209+
ds = self.mode.convert_datatree(
210+
dt,
211+
includes=["oa01_radiance"],
212+
resolution=0.1,
213+
)
214+
self.assertEqual({"test_key": "test_val"}, ds.attrs)
215+
190216

191217
class SlstrRbtTest(Sen3TestMixin, TestCase):
192218
mode = Sen3Sl1Rbt()
@@ -260,6 +286,11 @@ def test_convert_datatree_raise_warning(self):
260286
def test_convert_datatree_fail(self):
261287
self.assert_convert_datatree_fail(make_s3_slstr_rbt(size=48))
262288

289+
def test_convert_datatree_fail_include_exclude_overlap(self):
290+
self.assert_convert_datatree_fail_with_include_exclude(
291+
make_s3_slstr_rbt(size=48)
292+
)
293+
263294
def test_get_outer_bbox(self):
264295
bboxs = np.array([[-2, 10, 8, 20], [2, 12, 13, 25]])
265296
expected = [-2, 10, 13, 25]
@@ -307,9 +338,14 @@ def test_convert_datatree_bbox(self):
307338
self.assert_convert_datatree_ok(
308339
make_s3_slstr_lst(size=1000),
309340
expected_var_names=["lst"],
310-
expected_size=(112, 127),
341+
expected_size=(112, 148),
311342
bbox=[1, 55, 3, 56],
312343
)
313344

314345
def test_convert_datatree_fail(self):
315346
self.assert_convert_datatree_fail(make_s3_slstr_lst(size=48))
347+
348+
def test_convert_datatree_fail_include_exclude_overlap(self):
349+
self.assert_convert_datatree_fail_with_include_exclude(
350+
make_s3_slstr_lst(size=48)
351+
)

tests/helpers/sentinel3.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,39 @@
1111

1212

1313
def make_s3_olci_efr(size: int = 48) -> xr.DataTree:
14+
ds = make_s3_meas(size, bands=[f"oa{i:02}_radiance" for i in range(1, 22)])
1415

15-
return create_datatree(
16-
{
17-
"measurements": make_s3_meas(
18-
size, bands=[f"oa{i:02}_radiance" for i in range(1, 22)]
19-
),
16+
footprint = derive_footprint(ds)
17+
18+
dt = create_datatree(
19+
{"measurements": ds},
20+
attrs={
21+
"stac_discovery": {
22+
"geometry": {"type": "Polygon", "coordinates": [footprint]},
23+
"properties": {"sat:orbit_state": "descending"},
24+
}
2025
},
2126
)
27+
return dt
2228

2329

2430
def make_s3_slstr_lst(size: int = 48) -> xr.DataTree:
31+
ds = make_s3_meas(size, bands=["lst"])
32+
footprint = derive_footprint(ds)
2533
return create_datatree(
2634
{
2735
"conditions/auxiliary": make_s3_meas(size, bands=["elevation"]),
2836
"conditions/meteorology": make_s3_meas((size, size // 10), bands=["s2m"]),
2937
"conditions/geometry": make_s3_meas(
3038
(size, size // 10), bands=["sat_azimuth_tn", "sat_zenith_tn"]
3139
),
32-
"measurements": make_s3_meas(size, bands=["lst"]),
40+
"measurements": ds,
41+
},
42+
attrs={
43+
"stac_discovery": {
44+
"geometry": {"type": "Polygon", "coordinates": [footprint]},
45+
"properties": {"sat:orbit_state": "ascending"},
46+
}
3347
},
3448
)
3549

@@ -116,12 +130,30 @@ def make_coords(w: int, h: int, oblique_view=False) -> dict[str, xr.DataArray]:
116130
return {
117131
"latitude": xr.DataArray(lat_final, dims=("rows", "columns")),
118132
"longitude": xr.DataArray(lon_final, dims=("rows", "columns")),
119-
"time_stamps": xr.DataArray(
120-
np.arange(h).astype("datetime64[ns]"), dims=("rows")
121-
),
133+
"time_stamps": xr.DataArray(np.arange(h).astype("datetime64[ns]"), dims="rows"),
122134
}
123135

124136

137+
def derive_footprint(ds: xr.Dataset) -> list[list[float]]:
138+
lon = ds["longitude"]
139+
lat = ds["latitude"]
140+
corners = [
141+
(0, 0),
142+
(0, ds.sizes["columns"] - 1),
143+
(ds.sizes["rows"] - 1, ds.sizes["columns"] - 1),
144+
(ds.sizes["rows"] - 1, 0),
145+
]
146+
footprint = [
147+
[
148+
float(lon.isel(rows=i, columns=j).values),
149+
float(lat.isel(rows=i, columns=j).values),
150+
]
151+
for i, j in corners
152+
]
153+
footprint.append(footprint[0])
154+
return footprint
155+
156+
125157
def create_datatree(
126158
datasets: dict[str, xr.Dataset], attrs: dict[str, Any] | None = None
127159
) -> xr.DataTree:

tests/test_utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
# https://opensource.org/license/apache-2-0.
44
from typing import Literal
55
from unittest import TestCase
6+
from unittest.mock import patch
67

8+
import numpy as np
79
import pytest
810
import xarray as xr
911

1012
from tests.helpers import make_s2_msi
1113
from xarray_eopf.utils import (
1214
NameFilter,
15+
find_relative_bbox,
1316
assert_arg_has_length,
1417
assert_arg_is_instance,
1518
assert_arg_is_one_of,
19+
build_footprint_uv_mapping,
1620
get_data_tree_item,
1721
timeit,
1822
)
@@ -119,3 +123,49 @@ def test_filter(self):
119123
self.assertEqual(
120124
["ernie", "emmie"], list(f.filter(["bibo", "ernie", "bert", "emmie"]))
121125
)
126+
127+
128+
class BuildFootprintUvMappingTest(TestCase):
129+
def test_accepts_closed_ring_points(self):
130+
open_ring = np.array(
131+
[[10.0, 50.0], [12.0, 50.0], [12.0, 52.0], [10.0, 52.0]],
132+
dtype=float,
133+
)
134+
closed_ring = np.vstack([open_ring, open_ring[0]])
135+
136+
open_xy, open_uv = build_footprint_uv_mapping(open_ring)
137+
closed_xy, closed_uv = build_footprint_uv_mapping(closed_ring)
138+
139+
self.assertTrue(np.allclose(open_xy, closed_xy))
140+
self.assertTrue(np.allclose(open_uv, closed_uv))
141+
142+
def test_find_relative_bbox_uses_southern_utm_epsg(self):
143+
stac_meta = {
144+
"geometry": {
145+
"coordinates": [
146+
[
147+
[10.0, -11.0],
148+
[11.0, -11.0],
149+
[11.0, -10.0],
150+
[10.0, -10.0],
151+
[10.0, -11.0],
152+
]
153+
]
154+
},
155+
"properties": {"sat:orbit_state": "descending"},
156+
}
157+
bbox = [10.2, -10.8, 10.8, -10.2]
158+
159+
with patch("xarray_eopf.utils.pyproj.Transformer.from_crs") as from_crs:
160+
transformer = from_crs.return_value
161+
transformer.transform.return_value = (
162+
np.array([0.0, 1.0, 1.0, 0.0, 0.0]),
163+
np.array([0.0, 0.0, 1.0, 1.0, 0.0]),
164+
)
165+
transformer.transform_bounds.return_value = (0.2, 0.2, 0.8, 0.8)
166+
167+
rel_bbox = find_relative_bbox(stac_meta, bbox)
168+
169+
_, utm_epsg = from_crs.call_args.args[:2]
170+
self.assertEqual("EPSG:32732", utm_epsg)
171+
self.assertEqual(4, len(rel_bbox))

0 commit comments

Comments
 (0)