From 18b92fa46ebe34737032bc050082c4ebca05e8fd Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Mon, 1 Jun 2026 14:14:37 +0200 Subject: [PATCH 1/3] Fix Cube.rolling_window with lazy auxiliary coordinates rolling_window built each window's coordinate bounds with new_bounds[:, (0, -1)]. A tuple index is treated by Dask as a multidimensional index, so a lazy coordinate along the windowed dimension raised a TypeError. Index with the list [0, -1] instead, which numpy and Dask both treat as selecting the first and last column, giving an identical (and still lazy) result. Fixes #6480. --- changelog/7123.bugfix.rst | 3 +++ lib/iris/cube.py | 7 +++++-- lib/iris/tests/unit/cube/test_Cube.py | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 changelog/7123.bugfix.rst diff --git a/changelog/7123.bugfix.rst b/changelog/7123.bugfix.rst new file mode 100644 index 0000000000..d416a902e3 --- /dev/null +++ b/changelog/7123.bugfix.rst @@ -0,0 +1,3 @@ +:user:`gaoflow` fixed :meth:`iris.cube.Cube.rolling_window` so that it no +longer raises a ``TypeError`` when the cube has a lazy auxiliary coordinate +along the windowed dimension. (:issue:`6480`) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 44be3a63d7..f5d12d8e78 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -5243,12 +5243,15 @@ def rolling_window( # window and the bounds are the first and last points in the # window as with numeric coordinates. new_points = np.apply_along_axis(lambda x: "|".join(x), -1, new_bounds) - new_bounds = new_bounds[:, (0, -1)] + # Index with a list rather than a tuple: a tuple is interpreted + # as a multidimensional index by Dask, so lazy coordinates would + # otherwise fail here (see #6480). + new_bounds = new_bounds[:, [0, -1]] else: # Take the first and last element of the rolled window (i.e. # the bounds) and the new points are the midpoints of these # bounds. - new_bounds = new_bounds[:, (0, -1)] + new_bounds = new_bounds[:, [0, -1]] new_points = np.mean(new_bounds, axis=-1) # wipe the coords points and set the bounds diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index d91c7e81c0..09aebd29cf 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1086,6 +1086,22 @@ def test_lazy(self): ) _shared_utils.assert_masked_array_equal(expected_result, res_cube.data) + def test_lazy_aux_coord(self): + # A lazy aux-coord paralleling the rolled dimension must not cause a + # failure, and should give the same result as a real one, staying lazy + # (see #6480). + self.cube.add_aux_coord(AuxCoord(da.arange(6), long_name="lazy_extra"), 0) + res_cube = self.cube.rolling_window("val", iris.analysis.MEAN, 3) + result_coord = res_cube.coord("lazy_extra") + assert result_coord.has_lazy_points() + assert result_coord.has_lazy_bounds() + expected = AuxCoord( + np.array([1.0, 2.0, 3.0, 4.0]), + bounds=np.array([[0, 2], [1, 3], [2, 4], [3, 5]]), + long_name="lazy_extra", + ) + assert result_coord == expected + def test_ancillary_variables_and_cell_measures_kept(self, dataless): if dataless: self.cube.data = None From bc6142cdd60fa50cd9d39cffcb8c744619bf10b8 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 26 Jun 2026 13:42:55 +0200 Subject: [PATCH 2/3] Address rolling window review comments --- lib/iris/cube.py | 4 +--- lib/iris/tests/unit/cube/test_Cube.py | 9 ++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index f5d12d8e78..7f45d1f9a7 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -5243,9 +5243,7 @@ def rolling_window( # window and the bounds are the first and last points in the # window as with numeric coordinates. new_points = np.apply_along_axis(lambda x: "|".join(x), -1, new_bounds) - # Index with a list rather than a tuple: a tuple is interpreted - # as a multidimensional index by Dask, so lazy coordinates would - # otherwise fail here (see #6480). + # Use list indexing so Dask selects columns, not dimensions. new_bounds = new_bounds[:, [0, -1]] else: # Take the first and last element of the rolled window (i.e. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 09aebd29cf..37409ad1fa 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1087,17 +1087,16 @@ def test_lazy(self): _shared_utils.assert_masked_array_equal(expected_result, res_cube.data) def test_lazy_aux_coord(self): - # A lazy aux-coord paralleling the rolled dimension must not cause a - # failure, and should give the same result as a real one, staying lazy # (see #6480). + window = 2 self.cube.add_aux_coord(AuxCoord(da.arange(6), long_name="lazy_extra"), 0) - res_cube = self.cube.rolling_window("val", iris.analysis.MEAN, 3) + res_cube = self.cube.rolling_window("val", iris.analysis.MEAN, window, mdtol=0) result_coord = res_cube.coord("lazy_extra") assert result_coord.has_lazy_points() assert result_coord.has_lazy_bounds() expected = AuxCoord( - np.array([1.0, 2.0, 3.0, 4.0]), - bounds=np.array([[0, 2], [1, 3], [2, 4], [3, 5]]), + np.array([0.5, 1.5, 2.5, 3.5, 4.5]), + bounds=np.array([[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]), long_name="lazy_extra", ) assert result_coord == expected From f3d42f0165bef4477f474cc8e1c304fd4ae95dae Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sat, 27 Jun 2026 21:38:18 +0200 Subject: [PATCH 3/3] Drop redundant inline comments per review - lib/iris/cube.py: remove 'Use list indexing...' comment; the list indexing itself is self-explanatory, per ESadek-MO's review. - tests/.../test_Cube.py: remove the '# (see #6480).' note in test_lazy_aux_coord; the test name already conveys the intent, per ESadek-MO's review. This pull request was prepared with the assistance of AI, under my direction and review. --- lib/iris/cube.py | 1 - lib/iris/tests/unit/cube/test_Cube.py | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 7f45d1f9a7..4ddba3781f 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -5243,7 +5243,6 @@ def rolling_window( # window and the bounds are the first and last points in the # window as with numeric coordinates. new_points = np.apply_along_axis(lambda x: "|".join(x), -1, new_bounds) - # Use list indexing so Dask selects columns, not dimensions. new_bounds = new_bounds[:, [0, -1]] else: # Take the first and last element of the rolled window (i.e. diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 37409ad1fa..103aedd558 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -1087,7 +1087,6 @@ def test_lazy(self): _shared_utils.assert_masked_array_equal(expected_result, res_cube.data) def test_lazy_aux_coord(self): - # (see #6480). window = 2 self.cube.add_aux_coord(AuxCoord(da.arange(6), long_name="lazy_extra"), 0) res_cube = self.cube.rolling_window("val", iris.analysis.MEAN, window, mdtol=0)