Skip to content

Commit 2586e00

Browse files
Implement dataless concatenation (#6860)
* made concat work datalessly * Apply suggestions from code review Co-authored-by: Chris Bunney <[email protected]> * added whatsnew * Added missing comma --------- Co-authored-by: Chris Bunney <[email protected]>
1 parent bb0ef8c commit 2586e00

File tree

3 files changed

+81
-23
lines changed

3 files changed

+81
-23
lines changed

docs/src/whatsnew/latest.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ This document explains the changes made to Iris for this release
4444

4545
(:issue:`5819`, :pull:`6854`)
4646

47-
#. `@ESadek-MO`_ added functionality to allow :func:`~iris.cube.Cube.rolling_window` and
48-
:func:`~iris.cube.Cube.intersection` to work with dataless cubes. (:pull:`6757`)
49-
47+
#. `@ESadek-MO`_ added functionality to allow :func:`~iris.cube.Cube.concatenate`,
48+
:func:`~iris.cube.Cube.rolling_window` and :func:`~iris.cube.Cube.intersection`
49+
to work with dataless cubes. (:pull:`6860`, :pull:`6757`)
5050

5151
🐛 Bugs Fixed
5252
=============

lib/iris/_concatenate.py

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,15 @@ def name(self):
239239
return self.defn.name()
240240

241241

242-
class _SkeletonCube(namedtuple("SkeletonCube", ["signature", "data"])):
242+
class _SkeletonCube(
243+
namedtuple(
244+
"SkeletonCube",
245+
["signature", "data", "shape"],
246+
defaults=[
247+
None,
248+
],
249+
)
250+
):
243251
"""Basis of a source-cube.
244252
245253
Basis of a source-cube, containing the associated coordinate metadata,
@@ -575,8 +583,6 @@ def concatenate(
575583
"""
576584
cube_signatures = []
577585
for cube in cubes:
578-
if cube.is_dataless():
579-
raise iris.exceptions.DatalessError("concatenate")
580586
cube_signatures.append(_CubeSignature(cube))
581587

582588
proto_cubes: list[_ProtoCube] = []
@@ -869,8 +875,13 @@ def match(self, other, error_on_mismatch):
869875
msgs.append(
870876
msg_template.format("Data dimensions", "", self.ndim, other.ndim)
871877
)
872-
# Check data type.
873-
if self.data_type != other.data_type:
878+
if (
879+
self.data_type is not None
880+
and other.data_type is not None
881+
and self.data_type != other.data_type
882+
):
883+
# N.B. allow "None" to match any other dtype: this means that dataless
884+
# cubes can merge with 'dataful' ones.
874885
msgs.append(
875886
msg_template.format("Data types", "", self.data_type, other.data_type)
876887
)
@@ -1026,7 +1037,9 @@ def __init__(self, cube_signature):
10261037

10271038
# The list of source-cubes relevant to this proto-cube.
10281039
self._skeletons = []
1029-
self._add_skeleton(self._coord_signature, self._cube.lazy_data())
1040+
self._add_skeleton(
1041+
self._coord_signature, self._cube.lazy_data(), shape=self._cube.shape
1042+
)
10301043

10311044
# The nominated axis of concatenation.
10321045
self._axis = None
@@ -1090,6 +1103,10 @@ def concatenate(self):
10901103

10911104
# Concatenate the new data payload.
10921105
data = self._build_data()
1106+
if data is None:
1107+
shape = [coord.shape[0] for coord, _dim in dim_coords_and_dims]
1108+
else:
1109+
shape = None
10931110

10941111
# Build the new cube.
10951112
all_aux_coords_and_dims = aux_coords_and_dims + [
@@ -1098,6 +1115,7 @@ def concatenate(self):
10981115
kwargs = cube_signature.defn._asdict()
10991116
cube = iris.cube.Cube(
11001117
data,
1118+
shape=shape,
11011119
dim_coords_and_dims=dim_coords_and_dims,
11021120
aux_coords_and_dims=all_aux_coords_and_dims,
11031121
cell_measures_and_dims=cell_measures_and_dims,
@@ -1268,7 +1286,11 @@ def check_coord_match(coord_type: str) -> tuple[bool, str]:
12681286

12691287
if match:
12701288
# Register the cube as a source-cube for this proto-cube.
1271-
self._add_skeleton(coord_signature, cube_signature.src_cube.lazy_data())
1289+
self._add_skeleton(
1290+
coord_signature,
1291+
cube_signature.src_cube.lazy_data(),
1292+
shape=cube_signature.src_cube.shape,
1293+
)
12721294
# Declare the nominated axis of concatenation.
12731295
self._axis = candidate_axis
12741296
# If the protocube dimension order is constant (indicating it was
@@ -1284,7 +1306,7 @@ def check_coord_match(coord_type: str) -> tuple[bool, str]:
12841306

12851307
return match, mismatch_error_msg
12861308

1287-
def _add_skeleton(self, coord_signature, data):
1309+
def _add_skeleton(self, coord_signature, data, shape=None):
12881310
"""Create and add the source-cube skeleton to the :class:`_ProtoCube`.
12891311
12901312
Parameters
@@ -1298,7 +1320,7 @@ def _add_skeleton(self, coord_signature, data):
12981320
source-cube.
12991321
13001322
"""
1301-
skeleton = _SkeletonCube(coord_signature, data)
1323+
skeleton = _SkeletonCube(coord_signature, data, shape)
13021324
self._skeletons.append(skeleton)
13031325

13041326
def _build_aux_coordinates(self):
@@ -1534,9 +1556,22 @@ def _build_data(self):
15341556
15351557
"""
15361558
skeletons = self._skeletons
1537-
data = [skeleton.data for skeleton in skeletons]
15381559

1539-
data = concatenate_arrays(data, self.axis)
1560+
if all(skeleton.data is None for skeleton in skeletons):
1561+
data = None
1562+
else:
1563+
data = []
1564+
for skeleton in skeletons:
1565+
if skeleton.data is None:
1566+
skeleton_data = da.ma.masked_array(
1567+
data=da.zeros(skeleton.shape, dtype=np.int8),
1568+
mask=da.ones(skeleton.shape),
1569+
)
1570+
else:
1571+
skeleton_data = skeleton.data
1572+
data.append(skeleton_data)
1573+
1574+
data = concatenate_arrays(data, self.axis)
15401575

15411576
return data
15421577

lib/iris/tests/unit/cube/test_CubeList.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,44 @@ def test_fail(self):
4444

4545

4646
class Test_concatenate_cube:
47-
@pytest.fixture(autouse=True)
48-
def _setup(self):
47+
@pytest.fixture(autouse=True, params=[True, False], ids=["dataless", "with_data"])
48+
def _setup(self, request):
4949
self.units = Unit("days since 1970-01-01 00:00:00", calendar="standard")
50-
self.cube1 = Cube([1, 2, 3], "air_temperature", units="K")
50+
if request.param:
51+
self.cube1 = Cube(shape=(3,), standard_name="air_temperature", units="K")
52+
else:
53+
self.cube1 = Cube(
54+
data=[1, 2, 3], standard_name="air_temperature", units="K"
55+
)
5156
self.cube1.add_dim_coord(DimCoord([0, 1, 2], "time", units=self.units), 0)
5257

53-
def test_pass(self):
54-
self.cube2 = Cube([1, 2, 3], "air_temperature", units="K")
55-
self.cube2.add_dim_coord(DimCoord([3, 4, 5], "time", units=self.units), 0)
56-
result = CubeList([self.cube1, self.cube2]).concatenate_cube()
58+
@pytest.mark.parametrize(
59+
"dataless_c2", [True, False], ids=["and-dataless", "and-with_data"]
60+
)
61+
def test_pass(self, dataless_c2):
62+
if dataless_c2:
63+
data = None
64+
shape = (3,)
65+
else:
66+
data = [1, 2, 3]
67+
shape = None
68+
cube2 = Cube(data=data, shape=shape, standard_name="air_temperature", units="K")
69+
cube2.add_dim_coord(DimCoord([3, 4, 5], "time", units=self.units), 0)
70+
result = CubeList([self.cube1, cube2]).concatenate_cube()
5771
assert isinstance(result, Cube)
5872

59-
def test_fail(self):
73+
@pytest.mark.parametrize(
74+
"dataless_c2", [True, False], ids=["and-dataless", "and-with_data"]
75+
)
76+
def test_fail(self, dataless_c2):
6077
units = Unit("days since 1970-01-02 00:00:00", calendar="standard")
61-
cube2 = Cube([1, 2, 3], "air_temperature", units="K")
78+
if dataless_c2:
79+
data = None
80+
shape = (3,)
81+
else:
82+
data = [1, 2, 3]
83+
shape = None
84+
cube2 = Cube(data=data, shape=shape, standard_name="air_temperature", units="K")
6285
cube2.add_dim_coord(DimCoord([0, 1, 2], "time", units=units), 0)
6386
with pytest.raises(iris.exceptions.ConcatenateError):
6487
CubeList([self.cube1, cube2]).concatenate_cube()

0 commit comments

Comments
 (0)