Skip to content
Open
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
5 changes: 4 additions & 1 deletion docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ This document explains the changes made to Iris for this release
🐛 Bugs Fixed
=============

#. N/A
#. :user:`gaoflow` fixed :func:`iris.cube.CubeList.concatenate` so that cubes
with string coordinates of differing width (e.g. dtypes ``<U1`` and ``<U5``)
can be concatenated, with the result promoted to the wider dtype.
(:issue:`6676`)


💣 Incompatible Changes
Expand Down
17 changes: 17 additions & 0 deletions lib/iris/_concatenate.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@
_INCREASING = 1


def _dtype_for_concat_compare(dtype):
"""Normalise a dtype for comparing coordinates that are about to be concatenated.

String coordinates of the same kind but differing width (e.g. ``"<U1"`` and
``"<U5"``) should not prevent concatenation: numpy promotes them to a common
width when the points are joined. Collapse such dtypes to their kind so that
they compare equal, while leaving every other dtype unchanged. See #6676.
"""
if dtype is not None and dtype.kind in ("U", "S"):
return dtype.kind
return dtype


class _CoordAndDims(namedtuple("CoordAndDims", ["coord", "dims"])):
"""Container for a coordinate and the associated data dimension(s).

Expand Down Expand Up @@ -131,6 +144,10 @@ def __eq__(self, other):
sprops, oprops = self._asdict(), other._asdict()
# Ignore "kwargs" meta-data for the first comparison.
sprops["kwargs"] = oprops["kwargs"] = None
# String coordinates of differing width must not block concatenation.
for props in (sprops, oprops):
props["points_dtype"] = _dtype_for_concat_compare(props["points_dtype"])
props["bounds_dtype"] = _dtype_for_concat_compare(props["bounds_dtype"])
result = sprops == oprops
if result:
skwargs, okwargs = self.kwargs.copy(), other.kwargs.copy()
Expand Down
35 changes: 35 additions & 0 deletions lib/iris/tests/unit/concatenate/test_concatenate.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,41 @@ def test_desc_bounds_all_singleton(self):
assert result1 == result2


class TestStringAuxCoordWidths:
# Cubes with string auxiliary coordinates of differing width (dtype) should
# still concatenate, with the result promoted to the wider dtype (#6676).
def _make_cube(self, dim_points, string_points):
data = np.arange(len(dim_points))
cube = iris.cube.Cube(data, long_name="test")
cube.add_dim_coord(iris.coords.DimCoord(dim_points, long_name="dim"), 0)
cube.add_aux_coord(
iris.coords.AuxCoord(np.array(string_points), long_name="example"), 0
)
return cube

def test_different_widths(self):
cube_a = self._make_cube([0, 1], ["1", "2"])
cube_b = self._make_cube([10, 11, 12], ["1", "123", "12345"])
assert cube_a.coord("example").dtype != cube_b.coord("example").dtype

(result,) = concatenate([cube_a, cube_b])

coord = result.coord("example")
assert coord.dtype == np.dtype("<U5")
np.testing.assert_array_equal(coord.points, ["1", "2", "1", "123", "12345"])

def test_different_dtype_kind_still_rejected(self):
# A genuine dtype-kind difference (string vs integer) must still block
# concatenation.
cube_a = self._make_cube([0, 1], ["1", "2"])
cube_b = iris.cube.Cube(np.array([2, 3]), long_name="test")
cube_b.add_dim_coord(iris.coords.DimCoord([2, 3], long_name="dim"), 0)
cube_b.add_aux_coord(iris.coords.AuxCoord([1, 2], long_name="example"), 0)

with pytest.raises(ConcatenateError):
_ = concatenate([cube_a, cube_b], True)


class TestConcatenate__dask:
@pytest.fixture
def sample_lazy_cubes(self):
Expand Down
Loading