From 1a676ea40728e563fb4e3ff8b185c1aa4516d10e Mon Sep 17 00:00:00 2001 From: dsamaey Date: Thu, 12 Nov 2026 17:30:12 +0100 Subject: [PATCH] Warn on masking between cubes in different CRS in dry run #436 --- openeo_driver/dry_run.py | 23 +++++++++++++++++++++++ tests/test_dry_run.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/openeo_driver/dry_run.py b/openeo_driver/dry_run.py index 78944998..2da4a1a8 100644 --- a/openeo_driver/dry_run.py +++ b/openeo_driver/dry_run.py @@ -72,6 +72,7 @@ from openeo_driver.util.geometry import ( BoundingBox, GeometryBufferer, + is_auto_utm_crs, geojson_to_geometry, reproject_geometry, ) import openeo_driver.stac.datacube @@ -662,7 +663,29 @@ def filter_labels( ) -> "DryRunDataCube": return self._process("filter_labels", arguments=dict(condition=condition, dimension=dimension, context=context)) + @staticmethod + def _spatial_crs(cube: "DryRunDataCube") -> Optional[str]: + """Normalized CRS of the cube's spatial dimensions, or None if unknown.""" + if cube.metadata is None: + return None + for dim in cube.metadata.spatial_dimensions: + if dim.crs is not None: + try: + crs = BoundingBox.normalize_crs(dim.crs) + except Exception: + return None + # Auto UTM is resolved later, so it can't be compared yet + return None if is_auto_utm_crs(crs) else crs + return None + def mask(self, mask: "DryRunDataCube", replacement=None) -> "DryRunDataCube": + data_crs = self._spatial_crs(self) + mask_crs = self._spatial_crs(mask) + if data_crs is not None and mask_crs is not None and data_crs != mask_crs: + _log.warning( + f"Masking between cubes with different CRS: data cube has CRS {data_crs}, mask has CRS {mask_crs}." + " The mask will be resampled with nearest neighbour, which may give unexpected results." + ) # TODO: if mask cube has no temporal or bbox extent: copy from self? # TODO: or add reference to the self trace to the mask trace and vice versa? mask_resampled = mask._process("resample_cube_spatial", arguments={"target": self, "method": "near"}) diff --git a/tests/test_dry_run.py b/tests/test_dry_run.py index e696582a..c5d16049 100644 --- a/tests/test_dry_run.py +++ b/tests/test_dry_run.py @@ -607,6 +607,34 @@ def test_mask_polygon_and_filter_bbox(dry_run_env, dry_run_tracer, bbox_first): } +def _load_collection_in_crs(collection_id: str, epsg: int) -> DataCube: + cube = DataCube(PGNode("load_collection", id=collection_id), connection=None) + return cube.resample_spatial(resolution=10, projection=epsg) + + +def test_mask_cubes_different_crs_warns(dry_run_env, dry_run_tracer, caplog): + """https://github.com/Open-EO/openeo-python-driver/issues/436""" + cube = _load_collection_in_crs("S2_FOOBAR", epsg=32631) + mask = _load_collection_in_crs("S2_FOOBAR", epsg=3035) + pg = cube.mask(mask=mask).flat_graph() + + caplog.set_level(logging.WARNING) + evaluate(pg, env=dry_run_env, do_dry_run=False) + + assert "Masking between cubes with different CRS: data cube has CRS EPSG:32631, mask has CRS EPSG:3035" in caplog.text + + +def test_mask_cubes_same_crs_ok(dry_run_env, dry_run_tracer, caplog): + cube = _load_collection_in_crs("S2_FOOBAR", epsg=32631) + mask = _load_collection_in_crs("S2_FOOBAR", epsg=32631) + pg = cube.mask(mask=mask).flat_graph() + + caplog.set_level(logging.WARNING) + evaluate(pg, env=dry_run_env, do_dry_run=False) + + assert "different CRS" not in caplog.text + + def test_aggregate_spatial_only(dry_run_env, dry_run_tracer): polygon = {"type": "Polygon", "coordinates": [[(0, 0), (3, 5), (8, 2), (0, 0)]]} pg = {