From 147010486257616e16fe62316b14def12961ff0c Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 22 Oct 2025 11:10:50 +0100 Subject: [PATCH 01/10] added rolling window (tests) --- lib/iris/tests/unit/cube/test_Cube.py | 54 ++++++++++++++++++++------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index a354d9f472..8048579ad0 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -925,7 +925,10 @@ def _setup(self): self.mock_agg.aggregate = mock.Mock(return_value=np.empty([4])) self.mock_agg.post_process = mock.Mock(side_effect=lambda x, y, z: x) - def test_string_coord(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_string_coord(self, dataless): + if dataless: + self.cube.data = None # Rolling window on a cube that contains a string coordinate. res_cube = self.cube.rolling_window("val", self.mock_agg, 3) val_coord = DimCoord( @@ -978,38 +981,60 @@ def test_lazy(self): ) _shared_utils.assert_masked_array_equal(expected_result, res_cube.data) - def test_ancillary_variables_and_cell_measures_kept(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_ancillary_variables_and_cell_measures_kept(self, dataless): + if dataless: + self.cube.data = None res_cube = self.multi_dim_cube.rolling_window("val", self.mock_agg, 3) assert res_cube.ancillary_variables() == [self.ancillary_variable] assert res_cube.cell_measures() == [self.cell_measure] - def test_ancillary_variables_and_cell_measures_removed(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_ancillary_variables_and_cell_measures_removed(self, dataless): + if dataless: + self.cube.data = None res_cube = self.multi_dim_cube.rolling_window("extra", self.mock_agg, 3) assert res_cube.ancillary_variables() == [] assert res_cube.cell_measures() == [] - def test_weights_arr(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_weights_arr(self, dataless): + if dataless: + self.cube.data = None weights = np.array([0, 0, 1, 0, 2]) res_cube = self.cube.rolling_window("val", SUM, 5, weights=weights) - _shared_utils.assert_array_equal(res_cube.data, [10, 13]) + if not dataless: + _shared_utils.assert_array_equal(res_cube.data, [10, 13]) assert res_cube.units == "kg" - def test_weights_cube(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_weights_cube(self, dataless): + if dataless: + self.cube.data = None weights = Cube([0, 0, 1, 0, 2], units="m2") res_cube = self.cube.rolling_window("val", SUM, 5, weights=weights) - _shared_utils.assert_array_equal(res_cube.data, [10, 13]) + if not dataless: + _shared_utils.assert_array_equal(res_cube.data, [10, 13]) assert res_cube.units == "kg m2" - def test_weights_str(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_weights_str(self, dataless): + if dataless: + self.cube.data = None weights = "val" res_cube = self.cube.rolling_window("val", SUM, 6, weights=weights) - _shared_utils.assert_array_equal(res_cube.data, [55]) + if not dataless: + _shared_utils.assert_array_equal(res_cube.data, [55]) assert res_cube.units == "kg s" - def test_weights_dim_coord(self): + @pytest.mark.parametrize("dataless", [True, False]) + def test_weights_dim_coord(self, dataless): + if dataless: + self.cube.data = None weights = self.cube.coord("val") res_cube = self.cube.rolling_window("val", SUM, 6, weights=weights) - _shared_utils.assert_array_equal(res_cube.data, [55]) + if not dataless: + _shared_utils.assert_array_equal(res_cube.data, [55]) assert res_cube.units == "kg s" @@ -1095,9 +1120,12 @@ def test_all_permutations(self): @_shared_utils.skip_data class Test_slices_over: - @pytest.fixture(autouse=True) - def _setup(self): + @pytest.fixture(autouse=True, params=[False, True], ids=["with data", "dataless"]) + def _setup(self, request): + dataless = request.param self.cube = stock.realistic_4d()[:, :7, :10, :10] + if dataless: + self.cube.data = None # Define expected iterators for 1D and 2D test cases. self.exp_iter_1d = range(len(self.cube.coord("model_level_number").points)) self.exp_iter_2d = np.ndindex(6, 7, 1, 1) From 367fcad14e8566d8120f5f31a9a2294fc692465d Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Wed, 22 Oct 2025 10:35:03 +0100 Subject: [PATCH 02/10] added rolling window (code) --- lib/iris/cube.py | 56 +++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index f80854df30..47dc1db43c 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -4926,8 +4926,7 @@ def rolling_window( """ # noqa: D214, D406, D407, D410, D411 # Update weights kwargs (if necessary) to handle different types of # weights - if self.is_dataless(): - raise iris.exceptions.DatalessError("rolling_window") + dataless = self.is_dataless() weights_info = None if kwargs.get("weights") is not None: weights_info = _Weights(kwargs["weights"], self) @@ -4966,13 +4965,13 @@ def rolling_window( key = [slice(None, None)] * self.ndim key[dimension] = slice(None, self.shape[dimension] - window + 1) new_cube = new_cube[tuple(key)] - - # take a view of the original data using the rolling_window function - # this will add an extra dimension to the data at dimension + 1 which - # represents the rolled window (i.e. will have a length of window) - rolling_window_data = iris.util.rolling_window( - self.core_data(), window=window, axis=dimension - ) + if not dataless: + # take a view of the original data using the rolling_window function + # this will add an extra dimension to the data at dimension + 1 which + # represents the rolled window (i.e. will have a length of window) + rolling_window_data = iris.util.rolling_window( + self.core_data(), window=window, axis=dimension + ) # now update all of the coordinates to reflect the aggregation for coord_ in self.coords(dimensions=dimension): @@ -5021,27 +5020,30 @@ def rolling_window( ) # and perform the data transformation, generating weights first if # needed - if isinstance( - aggregator, iris.analysis.WeightedAggregator - ) and aggregator.uses_weighting(**kwargs): - if "weights" in kwargs: - weights = kwargs["weights"] - if weights.ndim > 1 or weights.shape[0] != window: - raise ValueError( - "Weights for rolling window aggregation " - "must be a 1d array with the same length " - "as the window." + if not dataless: + if isinstance( + aggregator, iris.analysis.WeightedAggregator + ) and aggregator.uses_weighting(**kwargs): + if "weights" in kwargs: + weights = kwargs["weights"] + if weights.ndim > 1 or weights.shape[0] != window: + raise ValueError( + "Weights for rolling window aggregation " + "must be a 1d array with the same length " + "as the window." + ) + kwargs = dict(kwargs) + kwargs["weights"] = iris.util.broadcast_to_shape( + weights, rolling_window_data.shape, (dimension + 1,) ) - kwargs = dict(kwargs) - kwargs["weights"] = iris.util.broadcast_to_shape( - weights, rolling_window_data.shape, (dimension + 1,) - ) - if aggregator.lazy_func is not None and self.has_lazy_data(): - agg_method = aggregator.lazy_aggregate + if aggregator.lazy_func is not None and self.has_lazy_data(): + agg_method = aggregator.lazy_aggregate + else: + agg_method = aggregator.aggregate + data_result = agg_method(rolling_window_data, axis=dimension + 1, **kwargs) else: - agg_method = aggregator.aggregate - data_result = agg_method(rolling_window_data, axis=dimension + 1, **kwargs) + data_result = None result = aggregator.post_process(new_cube, data_result, [coord], **kwargs) return result From 7edc97f6f0b0ad7b8100415cf210791550a889d2 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 23 Oct 2025 10:38:06 +0100 Subject: [PATCH 03/10] added intersection (code) --- lib/iris/cube.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index a77e15643c..08db0f64ce 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3276,8 +3276,8 @@ def intersection(self, *args, **kwargs) -> Cube: which intersects with the requested coordinate intervals. """ - if self.is_dataless(): - raise iris.exceptions.DatalessError("intersection") + # if self.is_dataless(): + # raise iris.exceptions.DatalessError("intersection") result = self ignore_bounds = kwargs.pop("ignore_bounds", False) threshold = kwargs.pop("threshold", 0) @@ -3349,9 +3349,20 @@ def make_chunk(key): if len(chunks) == 1: result = chunks[0] else: - chunk_data = [chunk.core_data() for chunk in chunks] - data = _lazy.concatenate(chunk_data, axis=dim) - result = iris.cube.Cube(data) + if self.is_dataless(): + dim_shape = 0 + old_shape = list(self.shape) + for chunk in chunks: + # sum the shape of the relevant dimension of each chunk together + dim_shape += len(chunk.coord(coord).points) + old_shape[dim] = dim_shape + new_shape = tuple(old_shape) + data = None + else: + chunk_data = [chunk.core_data() for chunk in chunks] + data = _lazy.concatenate(chunk_data, axis=dim) + new_shape = None + result = iris.cube.Cube(data=data, shape=new_shape) result.metadata = deepcopy(self.metadata) # Record a mapping from old coordinate IDs to new coordinates, From 2d45bbf747c7aec3e8d0b1ebc6ac72dd8e8d838f Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 23 Oct 2025 12:00:45 +0100 Subject: [PATCH 04/10] added intersection (tests) --- lib/iris/tests/unit/cube/test_Cube.py | 651 ++++++++++++++++---------- 1 file changed, 414 insertions(+), 237 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 90e2658531..fe53dca748 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1369,11 +1369,16 @@ def test_nodimension(self): assert next(res) == self.cube -def create_cube(lon_min, lon_max, bounds=False): +def create_cube(lon_min, lon_max, bounds=False, dataless=False): n_lons = max(lon_min, lon_max) - min(lon_max, lon_min) data = np.arange(4 * 3 * n_lons, dtype="f4").reshape(4, 3, -1) - data = as_lazy_data(data) - cube = Cube(data, standard_name="x_wind", units="ms-1") + if dataless: + shape = data.shape + data = None + else: + shape = None + data = as_lazy_data(data) + cube = Cube(data=data, standard_name="x_wind", units="ms-1", shape=shape) cube.add_dim_coord( iris.coords.DimCoord([0, 20, 40, 80], long_name="level_height", units="m"), 0, @@ -1433,71 +1438,74 @@ def create_cube(lon_min, lon_max, bounds=False): # Ensure all the other coordinates and factories are correctly preserved. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__Metadata: - def test_metadata(self, request): - cube = create_cube(0, 360) + def test_metadata(self, request, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(170, 190)) _shared_utils.assert_CML(request, result, approx_data=True) - def test_metadata_wrapped(self, request): - cube = create_cube(-180, 180) + def test_metadata_wrapped(self, request, dataless): + cube = create_cube(-180, 180, dataless=dataless) result = cube.intersection(longitude=(170, 190)) _shared_utils.assert_CML(request, result, approx_data=True) # Explicitly check the handling of `circular` on the result. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__Circular: - def test_regional(self): - cube = create_cube(0, 360) + def test_regional(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(170, 190)) assert not result.coord("longitude").circular - def test_regional_wrapped(self): - cube = create_cube(-180, 180) + def test_regional_wrapped(self, dataless): + cube = create_cube(-180, 180, dataless=dataless) result = cube.intersection(longitude=(170, 190)) assert not result.coord("longitude").circular - def test_global(self): - cube = create_cube(-180, 180) + def test_global(self, dataless): + cube = create_cube(-180, 180, dataless=dataless) result = cube.intersection(longitude=(-180, 180)) assert result.coord("longitude").circular - def test_global_wrapped(self): - cube = create_cube(-180, 180) + def test_global_wrapped(self, dataless): + cube = create_cube(-180, 180, dataless=dataless) result = cube.intersection(longitude=(10, 370)) assert result.coord("longitude").circular # Check the various error conditions. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__Invalid: - def test_reversed_min_max(self): - cube = create_cube(0, 360) + def test_reversed_min_max(self, dataless): + cube = create_cube(0, 360, dataless=dataless) with pytest.raises(ValueError): cube.intersection(longitude=(30, 10)) - def test_dest_too_large(self): - cube = create_cube(0, 360) + def test_dest_too_large(self, dataless): + cube = create_cube(0, 360, dataless=dataless) with pytest.raises(ValueError): cube.intersection(longitude=(30, 500)) - def test_src_too_large(self): - cube = create_cube(0, 400) + def test_src_too_large(self, dataless): + cube = create_cube(0, 400, dataless=dataless) with pytest.raises(ValueError): cube.intersection(longitude=(10, 30)) - def test_missing_coord(self): - cube = create_cube(0, 360) + def test_missing_coord(self, dataless): + cube = create_cube(0, 360, dataless=dataless) with pytest.raises(iris.exceptions.CoordinateNotFoundError): cube.intersection(parrots=(10, 30)) - def test_multi_dim_coord(self): - cube = create_cube(0, 360) + def test_multi_dim_coord(self, dataless): + cube = create_cube(0, 360, dataless=dataless) with pytest.raises(iris.exceptions.CoordinateMultiDimError): cube.intersection(surface_altitude=(10, 30)) - def test_null_region(self): + def test_null_region(self, dataless): # 10 <= v < 10 - cube = create_cube(0, 360) + cube = create_cube(0, 360, dataless=dataless) with pytest.raises(IndexError): cube.intersection(longitude=(10, 10, False, False)) @@ -1546,9 +1554,10 @@ def test_lazy_data_wrapped(self): assert result.data[0, 0, -1] == 10 +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection_Points: - def test_ignore_bounds(self): - cube = create_cube(0, 30, bounds=True) + def test_ignore_bounds(self, dataless): + cube = create_cube(0, 30, bounds=True, dataless=dataless) result = cube.intersection(longitude=(9.5, 12.5), ignore_bounds=True) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(10, 13) @@ -1563,73 +1572,98 @@ def test_ignore_bounds(self): # Check what happens with a regional, points-only circular intersection # coordinate. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__RegionalSrcModulus: - def test_request_subset(self): - cube = create_cube(40, 60) + def test_request_subset(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(45, 50)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(45, 51) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(5, 11)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(5, 11)) - def test_request_left(self): - cube = create_cube(40, 60) + def test_request_left(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(35, 45)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(40, 46) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 6)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 6)) - def test_request_right(self): - cube = create_cube(40, 60) + def test_request_right(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(55, 65)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(55, 60) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(15, 20)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(15, 20)) - def test_request_superset(self): - cube = create_cube(40, 60) + def test_request_superset(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(35, 65)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(40, 60) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 20)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 20)) - def test_request_subset_modulus(self): - cube = create_cube(40, 60) + def test_request_subset_modulus(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(45 + 360, 50 + 360)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(45 + 360, 51 + 360) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(5, 11)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(5, 11)) - def test_request_left_modulus(self): - cube = create_cube(40, 60) + def test_request_left_modulus(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(35 + 360, 45 + 360)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(40 + 360, 46 + 360) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 6)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 6)) - def test_request_right_modulus(self): - cube = create_cube(40, 60) + def test_request_right_modulus(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(55 + 360, 65 + 360)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(55 + 360, 60 + 360) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(15, 20)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(15, 20)) - def test_request_superset_modulus(self): - cube = create_cube(40, 60) + def test_request_superset_modulus(self, dataless): + cube = create_cube(40, 60, dataless=dataless) result = cube.intersection(longitude=(35 + 360, 65 + 360)) _shared_utils.assert_array_equal( result.coord("longitude").points, np.arange(40 + 360, 60 + 360) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 20)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.arange(0, 20)) - def test_tolerance_f4(self): - cube = create_cube(0, 5) + def test_tolerance_f4(self, dataless): + cube = create_cube(0, 5, dataless=dataless) cube.coord("longitude").points = np.array( [0.0, 3.74999905, 7.49999809, 11.24999714, 14.99999619], dtype="f4" ) @@ -1637,10 +1671,13 @@ def test_tolerance_f4(self): _shared_utils.assert_array_almost_equal( result.coord("longitude").points, np.array([0.0, 3.74999905]) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.array([0, 1])) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.array([0, 1])) - def test_tolerance_f8(self): - cube = create_cube(0, 5) + def test_tolerance_f8(self, dataless): + cube = create_cube(0, 5, dataless=dataless) cube.coord("longitude").points = np.array( [0.0, 3.74999905, 7.49999809, 11.24999714, 14.99999619], dtype="f8" ) @@ -1648,173 +1685,231 @@ def test_tolerance_f8(self): _shared_utils.assert_array_almost_equal( result.coord("longitude").points, np.array([0.0, 3.74999905]) ) - _shared_utils.assert_array_equal(result.data[0, 0], np.array([0, 1])) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data[0, 0], np.array([0, 1])) # Check what happens with a global, points-only circular intersection # coordinate. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__GlobalSrcModulus: - def test_global_wrapped_extreme_increasing_base_period(self): + def test_global_wrapped_extreme_increasing_base_period(self, dataless): # Ensure that we can correctly handle points defined at (base + period) - cube = create_cube(-180.0, 180.0) + cube = create_cube(-180.0, 180.0, dataless=dataless) lons = cube.coord("longitude") # Redefine longitude so that points at (base + period) lons.points = np.linspace(-180.0, 180, lons.points.size) result = cube.intersection(longitude=(lons.points.min(), lons.points.max())) - _shared_utils.assert_array_equal(result.data, cube.data) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, cube.data) - def test_global_wrapped_extreme_decreasing_base_period(self): + def test_global_wrapped_extreme_decreasing_base_period(self, dataless): # Ensure that we can correctly handle points defined at (base + period) - cube = create_cube(180.0, -180.0) + cube = create_cube(180.0, -180.0, dataless=dataless) lons = cube.coord("longitude") # Redefine longitude so that points at (base + period) lons.points = np.linspace(180.0, -180.0, lons.points.size) result = cube.intersection(longitude=(lons.points.min(), lons.points.max())) - _shared_utils.assert_array_equal(result.data, cube.data) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, cube.data) - def test_global(self): - cube = create_cube(0, 360) + def test_global(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(0, 360)) assert result.coord("longitude").points[0] == 0 assert result.coord("longitude").points[-1] == 359 - assert result.data[0, 0, 0] == 0 - assert result.data[0, 0, -1] == 359 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 0 + assert result.data[0, 0, -1] == 359 - def test_global_wrapped(self): - cube = create_cube(0, 360) + def test_global_wrapped(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(-180, 180)) assert result.coord("longitude").points[0] == -180 assert result.coord("longitude").points[-1] == 179 - assert result.data[0, 0, 0] == 180 - assert result.data[0, 0, -1] == 179 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 180 + assert result.data[0, 0, -1] == 179 - def test_aux_coord(self): - cube = create_cube(0, 360) + def test_aux_coord(self, dataless): + cube = create_cube(0, 360, dataless=dataless) cube.replace_coord(iris.coords.AuxCoord.from_coord(cube.coord("longitude"))) result = cube.intersection(longitude=(0, 360)) assert result.coord("longitude").points[0] == 0 assert result.coord("longitude").points[-1] == 359 - assert result.data[0, 0, 0] == 0 - assert result.data[0, 0, -1] == 359 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 0 + assert result.data[0, 0, -1] == 359 - def test_aux_coord_wrapped(self): - cube = create_cube(0, 360) + def test_aux_coord_wrapped(self, dataless): + cube = create_cube(0, 360, dataless=dataless) cube.replace_coord(iris.coords.AuxCoord.from_coord(cube.coord("longitude"))) result = cube.intersection(longitude=(-180, 180)) assert result.coord("longitude").points[0] == 0 assert result.coord("longitude").points[-1] == -1 - assert result.data[0, 0, 0] == 0 - assert result.data[0, 0, -1] == 359 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 0 + assert result.data[0, 0, -1] == 359 - def test_aux_coord_non_contiguous_wrapped(self): - cube = create_cube(0, 360) + def test_aux_coord_non_contiguous_wrapped(self, dataless): + cube = create_cube(0, 360, dataless=dataless) coord = iris.coords.AuxCoord.from_coord(cube.coord("longitude")) coord.points = (coord.points * 1.5) % 360 cube.replace_coord(coord) result = cube.intersection(longitude=(-90, 90)) assert result.coord("longitude").points[0] == 0 assert result.coord("longitude").points[-1] == 90 - assert result.data[0, 0, 0] == 0 - assert result.data[0, 0, -1] == 300 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 0 + assert result.data[0, 0, -1] == 300 - def test_decrementing(self): - cube = create_cube(360, 0) + def test_decrementing(self, dataless): + cube = create_cube(360, 0, dataless=dataless) result = cube.intersection(longitude=(40, 60)) assert result.coord("longitude").points[0] == 60 assert result.coord("longitude").points[-1] == 40 - assert result.data[0, 0, 0] == 300 - assert result.data[0, 0, -1] == 320 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 300 + assert result.data[0, 0, -1] == 320 - def test_decrementing_wrapped(self): - cube = create_cube(360, 0) + def test_decrementing_wrapped(self, dataless): + cube = create_cube(360, 0, dataless=dataless) result = cube.intersection(longitude=(-10, 10)) assert result.coord("longitude").points[0] == 10 assert result.coord("longitude").points[-1] == -10 - assert result.data[0, 0, 0] == 350 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 350 + assert result.data[0, 0, -1] == 10 - def test_no_wrap_after_modulus(self): - cube = create_cube(0, 360) + def test_no_wrap_after_modulus(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(170 + 360, 190 + 360)) assert result.coord("longitude").points[0] == 170 + 360 assert result.coord("longitude").points[-1] == 190 + 360 - assert result.data[0, 0, 0] == 170 - assert result.data[0, 0, -1] == 190 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 170 + assert result.data[0, 0, -1] == 190 - def test_wrap_after_modulus(self): - cube = create_cube(-180, 180) + def test_wrap_after_modulus(self, dataless): + cube = create_cube(-180, 180, dataless=dataless) result = cube.intersection(longitude=(170 + 360, 190 + 360)) assert result.coord("longitude").points[0] == 170 + 360 assert result.coord("longitude").points[-1] == 190 + 360 - assert result.data[0, 0, 0] == 350 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 350 + assert result.data[0, 0, -1] == 10 - def test_select_by_coord(self): - cube = create_cube(0, 360) + def test_select_by_coord(self, dataless): + cube = create_cube(0, 360, dataless=dataless) coord = iris.coords.DimCoord(0, "longitude", units="degrees") result = cube.intersection(iris.coords.CoordExtent(coord, 10, 30)) assert result.coord("longitude").points[0] == 10 assert result.coord("longitude").points[-1] == 30 - assert result.data[0, 0, 0] == 10 - assert result.data[0, 0, -1] == 30 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 10 + assert result.data[0, 0, -1] == 30 - def test_inclusive_exclusive(self): - cube = create_cube(0, 360) + def test_inclusive_exclusive(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(170, 190, True, False)) assert result.coord("longitude").points[0] == 170 assert result.coord("longitude").points[-1] == 189 - assert result.data[0, 0, 0] == 170 - assert result.data[0, 0, -1] == 189 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 170 + assert result.data[0, 0, -1] == 189 - def test_exclusive_inclusive(self): - cube = create_cube(0, 360) + def test_exclusive_inclusive(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(170, 190, False)) assert result.coord("longitude").points[0] == 171 assert result.coord("longitude").points[-1] == 190 - assert result.data[0, 0, 0] == 171 - assert result.data[0, 0, -1] == 190 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 171 + assert result.data[0, 0, -1] == 190 - def test_exclusive_exclusive(self): - cube = create_cube(0, 360) + def test_exclusive_exclusive(self, dataless): + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(170, 190, False, False)) assert result.coord("longitude").points[0] == 171 assert result.coord("longitude").points[-1] == 189 - assert result.data[0, 0, 0] == 171 - assert result.data[0, 0, -1] == 189 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 171 + assert result.data[0, 0, -1] == 189 - def test_single_point(self): + def test_single_point(self, dataless): # 10 <= v <= 10 - cube = create_cube(0, 360) + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(10, 10)) assert result.coord("longitude").points[0] == 10 assert result.coord("longitude").points[-1] == 10 - assert result.data[0, 0, 0] == 10 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 10 + assert result.data[0, 0, -1] == 10 - def test_two_points(self): + def test_two_points(self, dataless): # -1.5 <= v <= 0.5 - cube = create_cube(0, 360) + cube = create_cube(0, 360, dataless=dataless) result = cube.intersection(longitude=(-1.5, 0.5)) assert result.coord("longitude").points[0] == -1 assert result.coord("longitude").points[-1] == 0 - assert result.data[0, 0, 0] == 359 - assert result.data[0, 0, -1] == 0 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 359 + assert result.data[0, 0, -1] == 0 - def test_wrap_radians(self): - cube = create_cube(0, 360) + def test_wrap_radians(self, dataless): + cube = create_cube(0, 360, dataless=dataless) cube.coord("longitude").convert_units("radians") result = cube.intersection(longitude=(-1, 0.5)) _shared_utils.assert_array_all_close( result.coord("longitude").points, np.arange(-57, 29) * np.pi / 180 ) - assert result.data[0, 0, 0] == 303 - assert result.data[0, 0, -1] == 28 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 303 + assert result.data[0, 0, -1] == 28 - def test_tolerance_bug(self): + def test_tolerance_bug(self, dataless): # Floating point changes introduced by wrapping mean # the resulting coordinate values are not equal to their # equivalents. This led to a bug that this test checks. - cube = create_cube(0, 400) + cube = create_cube(0, 400, dataless=dataless) cube.coord("longitude").points = np.linspace(-179.55, 179.55, 400) result = cube.intersection(longitude=(125, 145)) _shared_utils.assert_array_almost_equal( @@ -1822,8 +1917,8 @@ def test_tolerance_bug(self): cube.coord("longitude").points[339:361], ) - def test_tolerance_bug_wrapped(self): - cube = create_cube(0, 400) + def test_tolerance_bug_wrapped(self, dataless): + cube = create_cube(0, 400, dataless=dataless) cube.coord("longitude").points = np.linspace(-179.55, 179.55, 400) result = cube.intersection(longitude=(-190, -170)) # Expected result is the last 11 and first 11 points. @@ -1838,23 +1933,30 @@ def test_tolerance_bug_wrapped(self): # Check what happens with a global, points-and-bounds circular # intersection coordinate. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__ModulusBounds: - def test_global_wrapped_extreme_increasing_base_period(self): + def test_global_wrapped_extreme_increasing_base_period(self, dataless): # Ensure that we can correctly handle bounds defined at (base + period) - cube = create_cube(-180.0, 180.0, bounds=True) + cube = create_cube(-180.0, 180.0, bounds=True, dataless=dataless) lons = cube.coord("longitude") result = cube.intersection(longitude=(lons.bounds.min(), lons.bounds.max())) - _shared_utils.assert_array_equal(result.data, cube.data) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, cube.data) - def test_global_wrapped_extreme_decreasing_base_period(self): + def test_global_wrapped_extreme_decreasing_base_period(self, dataless): # Ensure that we can correctly handle bounds defined at (base + period) - cube = create_cube(180.0, -180.0, bounds=True) + cube = create_cube(180.0, -180.0, bounds=True, dataless=dataless) lons = cube.coord("longitude") result = cube.intersection(longitude=(lons.bounds.min(), lons.bounds.max())) - _shared_utils.assert_array_equal(result.data, cube.data) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, cube.data) - def test_misaligned_points_inside(self): - cube = create_cube(0, 360, bounds=True) + def test_misaligned_points_inside(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(169.75, 190.25)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [169.5, 170.5] @@ -1862,11 +1964,14 @@ def test_misaligned_points_inside(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [189.5, 190.5] ) - assert result.data[0, 0, 0] == 170 - assert result.data[0, 0, -1] == 190 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 170 + assert result.data[0, 0, -1] == 190 - def test_misaligned_points_outside(self): - cube = create_cube(0, 360, bounds=True) + def test_misaligned_points_outside(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(170.25, 189.75)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [169.5, 170.5] @@ -1874,11 +1979,14 @@ def test_misaligned_points_outside(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [189.5, 190.5] ) - assert result.data[0, 0, 0] == 170 - assert result.data[0, 0, -1] == 190 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 170 + assert result.data[0, 0, -1] == 190 - def test_misaligned_bounds(self): - cube = create_cube(-180, 180, bounds=True) + def test_misaligned_bounds(self, dataless): + cube = create_cube(-180, 180, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0, 360)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [-0.5, 0.5] @@ -1886,11 +1994,14 @@ def test_misaligned_bounds(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [358.5, 359.5] ) - assert result.data[0, 0, 0] == 180 - assert result.data[0, 0, -1] == 179 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 180 + assert result.data[0, 0, -1] == 179 - def test_misaligned_bounds_decreasing(self): - cube = create_cube(180, -180, bounds=True) + def test_misaligned_bounds_decreasing(self, dataless): + cube = create_cube(180, -180, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0, 360)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [359.5, 358.5] @@ -1899,11 +2010,14 @@ def test_misaligned_bounds_decreasing(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [0.5, -0.5] ) - assert result.data[0, 0, 0] == 181 - assert result.data[0, 0, -1] == 180 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 181 + assert result.data[0, 0, -1] == 180 - def test_aligned_inclusive(self): - cube = create_cube(0, 360, bounds=True) + def test_aligned_inclusive(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(170.5, 189.5)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [169.5, 170.5] @@ -1911,11 +2025,14 @@ def test_aligned_inclusive(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [189.5, 190.5] ) - assert result.data[0, 0, 0] == 170 - assert result.data[0, 0, -1] == 190 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 170 + assert result.data[0, 0, -1] == 190 - def test_aligned_exclusive(self): - cube = create_cube(0, 360, bounds=True) + def test_aligned_exclusive(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(170.5, 189.5, False, False)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [170.5, 171.5] @@ -1923,21 +2040,27 @@ def test_aligned_exclusive(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [188.5, 189.5] ) - assert result.data[0, 0, 0] == 171 - assert result.data[0, 0, -1] == 189 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 171 + assert result.data[0, 0, -1] == 189 - def test_aligned_bounds_at_modulus(self): - cube = create_cube(-179.5, 180.5, bounds=True) + def test_aligned_bounds_at_modulus(self, dataless): + cube = create_cube(-179.5, 180.5, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0, 360)) _shared_utils.assert_array_equal(result.coord("longitude").bounds[0], [0, 1]) _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [359, 360] ) - assert result.data[0, 0, 0] == 180 - assert result.data[0, 0, -1] == 179 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 180 + assert result.data[0, 0, -1] == 179 - def test_negative_aligned_bounds_at_modulus(self): - cube = create_cube(0.5, 360.5, bounds=True) + def test_negative_aligned_bounds_at_modulus(self, dataless): + cube = create_cube(0.5, 360.5, bounds=True, dataless=dataless) result = cube.intersection(longitude=(-180, 180)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [-180, -179] @@ -1945,11 +2068,14 @@ def test_negative_aligned_bounds_at_modulus(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [179, 180] ) - assert result.data[0, 0, 0] == 180 - assert result.data[0, 0, -1] == 179 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 180 + assert result.data[0, 0, -1] == 179 - def test_negative_misaligned_points_inside(self): - cube = create_cube(0, 360, bounds=True) + def test_negative_misaligned_points_inside(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(-10.25, 10.25)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [-10.5, -9.5] @@ -1957,11 +2083,14 @@ def test_negative_misaligned_points_inside(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [9.5, 10.5] ) - assert result.data[0, 0, 0] == 350 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 350 + assert result.data[0, 0, -1] == 10 - def test_negative_misaligned_points_outside(self): - cube = create_cube(0, 360, bounds=True) + def test_negative_misaligned_points_outside(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(-9.75, 9.75)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [-10.5, -9.5] @@ -1969,11 +2098,14 @@ def test_negative_misaligned_points_outside(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [9.5, 10.5] ) - assert result.data[0, 0, 0] == 350 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 350 + assert result.data[0, 0, -1] == 10 - def test_negative_aligned_inclusive(self): - cube = create_cube(0, 360, bounds=True) + def test_negative_aligned_inclusive(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(-10.5, 10.5)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [-11.5, -10.5] @@ -1981,11 +2113,14 @@ def test_negative_aligned_inclusive(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [10.5, 11.5] ) - assert result.data[0, 0, 0] == 349 - assert result.data[0, 0, -1] == 11 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 349 + assert result.data[0, 0, -1] == 11 - def test_negative_aligned_exclusive(self): - cube = create_cube(0, 360, bounds=True) + def test_negative_aligned_exclusive(self, dataless): + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(-10.5, 10.5, False, False)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [-10.5, -9.5] @@ -1993,11 +2128,14 @@ def test_negative_aligned_exclusive(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [9.5, 10.5] ) - assert result.data[0, 0, 0] == 350 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 350 + assert result.data[0, 0, -1] == 10 - def test_decrementing(self): - cube = create_cube(360, 0, bounds=True) + def test_decrementing(self, dataless): + cube = create_cube(360, 0, bounds=True, dataless=dataless) result = cube.intersection(longitude=(40, 60)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [60.5, 59.5] @@ -2005,11 +2143,14 @@ def test_decrementing(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [40.5, 39.5] ) - assert result.data[0, 0, 0] == 300 - assert result.data[0, 0, -1] == 320 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 300 + assert result.data[0, 0, -1] == 320 - def test_decrementing_wrapped(self): - cube = create_cube(360, 0, bounds=True) + def test_decrementing_wrapped(self, dataless): + cube = create_cube(360, 0, bounds=True, dataless=dataless) result = cube.intersection(longitude=(-10, 10)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [10.5, 9.5] @@ -2017,13 +2158,16 @@ def test_decrementing_wrapped(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [-9.5, -10.5] ) - assert result.data[0, 0, 0] == 350 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 350 + assert result.data[0, 0, -1] == 10 - def test_numerical_tolerance(self): + def test_numerical_tolerance(self, dataless): # test the tolerance on the coordinate value is not causing a # modulus wrapping - cube = create_cube(28.5, 68.5, bounds=True) + cube = create_cube(28.5, 68.5, bounds=True, dataless=dataless) result = cube.intersection(longitude=(27.74, 68.61)) result_lons = result.coord("longitude") _shared_utils.assert_array_almost_equal(result_lons.points[0], 28.5) @@ -2036,10 +2180,10 @@ def test_numerical_tolerance(self): result_lons.bounds[-1], np.array([67.0, 68.0], dtype=dtype) ) - def test_numerical_tolerance_wrapped(self): + def test_numerical_tolerance_wrapped(self, dataless): # test the tolerance on the coordinate value causes modulus wrapping # where appropriate - cube = create_cube(0.5, 3600.5, bounds=True) + cube = create_cube(0.5, 3600.5, bounds=True, dataless=dataless) lons = cube.coord("longitude") lons.points = lons.points / 10 lons.bounds = lons.bounds / 10 @@ -2055,9 +2199,9 @@ def test_numerical_tolerance_wrapped(self): result_lons.bounds[-1], np.array([60.0, 60.1], dtype=dtype) ) - def test_ignore_bounds_wrapped(self): + def test_ignore_bounds_wrapped(self, dataless): # Test `ignore_bounds` fully ignores bounds when wrapping - cube = create_cube(0, 360, bounds=True) + cube = create_cube(0, 360, bounds=True, dataless=dataless) result = cube.intersection(longitude=(10.25, 370.25), ignore_bounds=True) # Expect points 11..370 not bounds [9.5, 10.5] .. [368.5, 369.5] _shared_utils.assert_array_equal( @@ -2066,12 +2210,15 @@ def test_ignore_bounds_wrapped(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [369.5, 370.5] ) - assert result.data[0, 0, 0] == 11 - assert result.data[0, 0, -1] == 10 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 11 + assert result.data[0, 0, -1] == 10 - def test_within_cell(self): + def test_within_cell(self, dataless): # Test cell is included when it entirely contains the requested range - cube = create_cube(0, 10, bounds=True) + cube = create_cube(0, 10, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0.7, 0.8)) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [0.5, 1.5] @@ -2079,11 +2226,14 @@ def test_within_cell(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [0.5, 1.5] ) - assert result.data[0, 0, 0] == 1 - assert result.data[0, 0, -1] == 1 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 1 + assert result.data[0, 0, -1] == 1 - def test_threshold_half(self): - cube = create_cube(0, 10, bounds=True) + def test_threshold_half(self, dataless): + cube = create_cube(0, 10, bounds=True, dataless=dataless) result = cube.intersection(longitude=(1, 6.999), threshold=0.5) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [0.5, 1.5] @@ -2091,11 +2241,14 @@ def test_threshold_half(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [5.5, 6.5] ) - assert result.data[0, 0, 0] == 1 - assert result.data[0, 0, -1] == 6 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 1 + assert result.data[0, 0, -1] == 6 - def test_threshold_full(self): - cube = create_cube(0, 10, bounds=True) + def test_threshold_full(self, dataless): + cube = create_cube(0, 10, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0.5, 7.499), threshold=1) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [0.5, 1.5] @@ -2103,13 +2256,16 @@ def test_threshold_full(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [5.5, 6.5] ) - assert result.data[0, 0, 0] == 1 - assert result.data[0, 0, -1] == 6 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 1 + assert result.data[0, 0, -1] == 6 - def test_threshold_wrapped(self): + def test_threshold_wrapped(self, dataless): # Test that a cell is wrapped to `maximum` if required to exceed # the threshold - cube = create_cube(-180, 180, bounds=True) + cube = create_cube(-180, 180, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0.4, 360.4), threshold=0.2) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [0.5, 1.5] @@ -2117,13 +2273,16 @@ def test_threshold_wrapped(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [359.5, 360.5] ) - assert result.data[0, 0, 0] == 181 - assert result.data[0, 0, -1] == 180 + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 181 + assert result.data[0, 0, -1] == 180 - def test_threshold_wrapped_gap(self): + def test_threshold_wrapped_gap(self, dataless): # Test that a cell is wrapped to `maximum` if required to exceed # the threshold (even with a gap in the range) - cube = create_cube(-180, 180, bounds=True) + cube = create_cube(-180, 180, bounds=True, dataless=dataless) result = cube.intersection(longitude=(0.4, 360.35), threshold=0.2) _shared_utils.assert_array_equal( result.coord("longitude").bounds[0], [0.5, 1.5] @@ -2131,13 +2290,21 @@ def test_threshold_wrapped_gap(self): _shared_utils.assert_array_equal( result.coord("longitude").bounds[-1], [359.5, 360.5] ) - assert result.data[0, 0, 0] == 181 - assert result.data[0, 0, -1] == 180 - - -def unrolled_cube(): - data = np.arange(5, dtype="f4") - cube = Cube(data) + if dataless: + assert result.data is None + else: + assert result.data[0, 0, 0] == 181 + assert result.data[0, 0, -1] == 180 + + +def unrolled_cube(dataless=False): + if dataless: + data = None + shape = (5,) + else: + data = np.arange(5, dtype="f4") + shape = None + cube = Cube(data=data, shape=shape) cube.add_aux_coord( iris.coords.AuxCoord([5.0, 10.0, 8.0, 5.0, 3.0], "longitude", units="degrees"), 0, @@ -2150,28 +2317,38 @@ def unrolled_cube(): # Check what happens with a "unrolled" scatter-point data with a circular # intersection coordinate. +@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__ScatterModulus: - def test_subset(self): - cube = unrolled_cube() + def test_subset(self, dataless): + cube = unrolled_cube(dataless) result = cube.intersection(longitude=(5, 8)) _shared_utils.assert_array_equal(result.coord("longitude").points, [5, 8, 5]) - _shared_utils.assert_array_equal(result.data, [0, 2, 3]) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, [0, 2, 3]) - def test_subset_wrapped(self): - cube = unrolled_cube() + def test_subset_wrapped(self, dataless): + cube = unrolled_cube(dataless) result = cube.intersection(longitude=(5 + 360, 8 + 360)) _shared_utils.assert_array_equal( result.coord("longitude").points, [365, 368, 365] ) - _shared_utils.assert_array_equal(result.data, [0, 2, 3]) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, [0, 2, 3]) - def test_superset(self): - cube = unrolled_cube() + def test_superset(self, dataless): + cube = unrolled_cube(dataless) result = cube.intersection(longitude=(0, 15)) _shared_utils.assert_array_equal( result.coord("longitude").points, [5, 10, 8, 5, 3] ) - _shared_utils.assert_array_equal(result.data, np.arange(5)) + if dataless: + assert result.data is None + else: + _shared_utils.assert_array_equal(result.data, np.arange(5)) # Test the API of the cube interpolation method. From fe8a3d820fc870b85a2dd7cb38e6bd487fa01da5 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 23 Oct 2025 21:51:34 +0100 Subject: [PATCH 05/10] removed intersection (CML tests) --- lib/iris/tests/unit/cube/test_Cube.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index fe53dca748..353bbf30b4 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1438,15 +1438,14 @@ def create_cube(lon_min, lon_max, bounds=False, dataless=False): # Ensure all the other coordinates and factories are correctly preserved. -@pytest.mark.parametrize("dataless", [True, False], ids=["dataless", "with data"]) class Test_intersection__Metadata: - def test_metadata(self, request, dataless): - cube = create_cube(0, 360, dataless=dataless) + def test_metadata(self, request): + cube = create_cube(0, 360) result = cube.intersection(longitude=(170, 190)) _shared_utils.assert_CML(request, result, approx_data=True) - def test_metadata_wrapped(self, request, dataless): - cube = create_cube(-180, 180, dataless=dataless) + def test_metadata_wrapped(self, request): + cube = create_cube(-180, 180) result = cube.intersection(longitude=(170, 190)) _shared_utils.assert_CML(request, result, approx_data=True) From 80597ff15367aedc18ff15d59102f1bf7a2b1583 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 23 Oct 2025 22:23:17 +0100 Subject: [PATCH 06/10] added whatsnew --- docs/src/whatsnew/latest.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 9b9dcff946..29cee14dd6 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -49,9 +49,8 @@ This document explains the changes made to Iris for this release #. `@ESadek-MO`_ added functionality to allow :func:`~iris.cube.Cube.extract`, :func:`~iris.cube.Cube.collapsed`, :func:`~iris.cube.Cube.aggregated_by`, - :func:`~iris.cube.Cube.convert_units`, :func:`~iris.cube.Cube.subset` and - :func:`~iris.cube.Cube.slices` to work with dataless cubes. - (:issue:`6725`, :pull:`6724`) + and other single cube operations to work with dataless cubes. + (:issue:`6725`, :pull:`6724`, :pull:`6757`) 🐛 Bugs Fixed From ebf31ba720b386817725e92b201016061e48e982 Mon Sep 17 00:00:00 2001 From: Elias <110238618+ESadek-MO@users.noreply.github.com> Date: Tue, 28 Oct 2025 11:09:16 +0000 Subject: [PATCH 07/10] Update lib/iris/cube.py Co-authored-by: Chris Bunney <48915820+ukmo-ccbunney@users.noreply.github.com> --- lib/iris/cube.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 08db0f64ce..612e0b80f8 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3350,12 +3350,9 @@ def make_chunk(key): result = chunks[0] else: if self.is_dataless(): - dim_shape = 0 old_shape = list(self.shape) - for chunk in chunks: - # sum the shape of the relevant dimension of each chunk together - dim_shape += len(chunk.coord(coord).points) - old_shape[dim] = dim_shape + newlen = sum(chunk.coord(coord).shape[0] for chunk in chunks) + old_shape[dim] = newlen new_shape = tuple(old_shape) data = None else: From cb6ea51ab372e2648af7d452ed633efd0e49647b Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 28 Oct 2025 11:11:49 +0000 Subject: [PATCH 08/10] review comments --- lib/iris/cube.py | 2 -- lib/iris/tests/unit/cube/test_Cube.py | 17 +++++------------ 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 08db0f64ce..e7e78efc6e 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -3276,8 +3276,6 @@ def intersection(self, *args, **kwargs) -> Cube: which intersects with the requested coordinate intervals. """ - # if self.is_dataless(): - # raise iris.exceptions.DatalessError("intersection") result = self ignore_bounds = kwargs.pop("ignore_bounds", False) threshold = kwargs.pop("threshold", 0) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 353bbf30b4..b2557336c0 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1101,24 +1101,17 @@ def test_ancillary_variables_and_cell_measures_removed(self, dataless): assert res_cube.ancillary_variables() == [] assert res_cube.cell_measures() == [] - @pytest.mark.parametrize("dataless", [True, False]) - def test_weights_arr(self, dataless): - if dataless: - self.cube.data = None + def test_weights_arr(self): weights = np.array([0, 0, 1, 0, 2]) res_cube = self.cube.rolling_window("val", SUM, 5, weights=weights) - if not dataless: - _shared_utils.assert_array_equal(res_cube.data, [10, 13]) + _shared_utils.assert_array_equal(res_cube.data, [10, 13]) assert res_cube.units == "kg" - @pytest.mark.parametrize("dataless", [True, False]) - def test_weights_cube(self, dataless): - if dataless: - self.cube.data = None + def test_weights_cube(self): + self.cube.data = None weights = Cube([0, 0, 1, 0, 2], units="m2") res_cube = self.cube.rolling_window("val", SUM, 5, weights=weights) - if not dataless: - _shared_utils.assert_array_equal(res_cube.data, [10, 13]) + _shared_utils.assert_array_equal(res_cube.data, [10, 13]) assert res_cube.units == "kg m2" @pytest.mark.parametrize("dataless", [True, False]) From f75e4d8459360a093fc6133551657094b322d952 Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Tue, 28 Oct 2025 11:16:18 +0000 Subject: [PATCH 09/10] split whatsnew entry, in expectation this won't make 3.14 --- docs/src/whatsnew/latest.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 29cee14dd6..c601f6741a 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -49,8 +49,12 @@ This document explains the changes made to Iris for this release #. `@ESadek-MO`_ added functionality to allow :func:`~iris.cube.Cube.extract`, :func:`~iris.cube.Cube.collapsed`, :func:`~iris.cube.Cube.aggregated_by`, - and other single cube operations to work with dataless cubes. - (:issue:`6725`, :pull:`6724`, :pull:`6757`) + :func:`~iris.cube.Cube.convert_units`, :func:`~iris.cube.Cube.subset` and + :func:`~iris.cube.Cube.slices` to work with dataless cubes. + (:issue:`6725`, :pull:`6724`) + +#. `@ESadek-MO`_ added functionality to allow :func:`~iris.cube.Cube.rolling_window` and + :func:`~iris.cube.Cube.intersection` to work with dataless cubes. (:pull:`6757`) 🐛 Bugs Fixed From 0c481296395a6877a2d6c1cf1f08ec0a17f8075c Mon Sep 17 00:00:00 2001 From: Elias Sadek Date: Thu, 30 Oct 2025 10:02:24 +0000 Subject: [PATCH 10/10] fixed failing test --- lib/iris/tests/unit/cube/test_Cube.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index b2557336c0..ef59bd4463 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1108,7 +1108,6 @@ def test_weights_arr(self): assert res_cube.units == "kg" def test_weights_cube(self): - self.cube.data = None weights = Cube([0, 0, 1, 0, 2], units="m2") res_cube = self.cube.rolling_window("val", SUM, 5, weights=weights) _shared_utils.assert_array_equal(res_cube.data, [10, 13])