Skip to content

Commit 4542051

Browse files
jlenhbouweandela
andcommitted
Fix for add_ancillary_variable (#2820) (#2825)
Co-authored-by: Bouwe Andela <[email protected]>
1 parent 7bec2d4 commit 4542051

File tree

2 files changed

+201
-53
lines changed

2 files changed

+201
-53
lines changed

esmvalcore/preprocessor/_supplementary_vars.py

Lines changed: 108 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,112 @@ def add_cell_measure(
103103
)
104104

105105

106+
def find_matching_coord_dims(
107+
coord_to_match: iris.coords.DimCoord,
108+
cube: iris.cube.Cube,
109+
) -> tuple[int] | None:
110+
"""Find a matching coordinate from the ancillary variable in the cube.
111+
112+
Parameters
113+
----------
114+
coord_to_match: iris.coords.DimCoord
115+
Coordinate from an ancillary cube to match.
116+
cube: iris.cube.Cube
117+
Iris cube with variable data.
118+
119+
Returns
120+
-------
121+
cube_dims: tuple or None
122+
Tuple containing the matched cube coordinate dimension for the
123+
coordinate from the ancillary cube in the data cube. If no match
124+
is found, None is returned.
125+
"""
126+
cube_dims = None
127+
for cube_coord in cube.coords():
128+
if (
129+
(
130+
cube_coord.var_name == coord_to_match.var_name
131+
or (
132+
coord_to_match.standard_name is not None
133+
and cube_coord.standard_name
134+
== coord_to_match.standard_name
135+
)
136+
or (
137+
coord_to_match.long_name is not None
138+
and cube_coord.long_name == coord_to_match.long_name
139+
)
140+
)
141+
and cube_coord.units == coord_to_match.units
142+
and cube_coord.shape == coord_to_match.shape
143+
):
144+
cube_dims = cube.coord_dims(cube_coord)
145+
msg = (
146+
f"Found a matching coordinate for {coord_to_match.var_name}"
147+
f" with coordinate {cube_coord.var_name}"
148+
f" in the cube of variable '{cube.var_name}'."
149+
)
150+
logger.debug(msg)
151+
break
152+
return cube_dims
153+
154+
155+
def get_data_dims(
156+
cube: Cube,
157+
ancillary: Cube | iris.coords.AncillaryVariable,
158+
) -> list[None | int]:
159+
"""Get matching data dimensions between cube and ancillary variable.
160+
161+
Parameters
162+
----------
163+
cube: iris.cube.Cube
164+
Iris cube with input data.
165+
ancillary: Cube or iris.coords.AncillaryVariable
166+
Iris cube or AncillaryVariable with ancillary data.
167+
168+
Returns
169+
-------
170+
data_dims: list
171+
List with as many entries as the ancillary variable dimensions.
172+
The i-th entry corresponds to the i-th ancillary variable
173+
dimension match with the cube's dimensions. If there is no match
174+
between the ancillary variable and the cube for a dimension, then entry
175+
defaults to None.
176+
"""
177+
# Match the coordinates of the ancillary cube to coordinates and
178+
# dimensions in the input cube before adding the ancillary variable.
179+
data_dims: list[None | int] = []
180+
if isinstance(ancillary, iris.coords.AncillaryVariable):
181+
start_dim = cube.ndim - len(ancillary.shape)
182+
data_dims = list(range(start_dim, cube.ndim))
183+
else:
184+
data_dims = [None] * ancillary.ndim
185+
for coord in ancillary.coords():
186+
try:
187+
cube_dims = cube.coord_dims(coord)
188+
except iris.exceptions.CoordinateNotFoundError:
189+
cube_dims = find_matching_coord_dims(coord, cube)
190+
if cube_dims is not None:
191+
for ancillary_dim, cube_dim in zip(
192+
ancillary.coord_dims(coord),
193+
cube_dims,
194+
strict=True,
195+
):
196+
data_dims[ancillary_dim] = cube_dim
197+
if None in data_dims:
198+
none_dims = ", ".join(
199+
str(i) for i, d in enumerate(data_dims) if d is None
200+
)
201+
msg = (
202+
f"Failed to add\n{ancillary}\nas ancillary var "
203+
f"to the cube\n{cube}\n"
204+
f"Mismatch between ancillary cube and variable cube coordinate"
205+
f" {none_dims}"
206+
)
207+
logger.error(msg)
208+
raise iris.exceptions.CoordinateNotFoundError(msg)
209+
return data_dims
210+
211+
106212
def add_ancillary_variable(
107213
cube: Cube,
108214
ancillary_cube: Cube | iris.coords.AncillaryVariable,
@@ -132,42 +238,12 @@ def add_ancillary_variable(
132238
)
133239
except AttributeError as err:
134240
msg = (
135-
f"Failed to add {ancillary_cube} to {cube} as ancillary var."
241+
f"Failed to add\n{ancillary_cube}\nas ancillary var to the cube\n{cube}\n"
136242
"ancillary_cube should be either an iris.cube.Cube or an "
137243
"iris.coords.AncillaryVariable object."
138244
)
139245
raise ValueError(msg) from err
140-
# Match the coordinates of the ancillary cube to coordinates and
141-
# dimensions in the input cube before adding the ancillary variable.
142-
data_dims: list[int | None] = []
143-
if isinstance(ancillary_cube, iris.coords.AncillaryVariable):
144-
start_dim = cube.ndim - len(ancillary_var.shape)
145-
data_dims = list(range(start_dim, cube.ndim))
146-
else:
147-
data_dims = [None] * ancillary_cube.ndim
148-
for coord in ancillary_cube.coords():
149-
try:
150-
for ancillary_dim, cube_dim in zip(
151-
ancillary_cube.coord_dims(coord),
152-
cube.coord_dims(coord),
153-
strict=False,
154-
):
155-
data_dims[ancillary_dim] = cube_dim
156-
except iris.exceptions.CoordinateNotFoundError:
157-
logger.debug(
158-
"%s from ancillary cube not found in cube coords.",
159-
coord,
160-
)
161-
if None in data_dims:
162-
none_dims = ", ".join(
163-
str(i) for i, d in enumerate(data_dims) if d is None
164-
)
165-
msg = (
166-
f"Failed to add {ancillary_cube} to {cube} as ancillary var."
167-
f"No coordinate associated with ancillary cube dimensions"
168-
f"{none_dims}"
169-
)
170-
raise ValueError(msg)
246+
data_dims = get_data_dims(cube, ancillary_cube)
171247
if ancillary_cube.has_lazy_data():
172248
cube_chunks = tuple(cube.lazy_data().chunks[d] for d in data_dims)
173249
ancillary_var.data = ancillary_cube.lazy_data().rechunk(cube_chunks)

tests/integration/preprocessor/_supplementary_vars/test_add_supplementary_variables.py

Lines changed: 93 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
add_ancillary_variable,
1515
add_cell_measure,
1616
add_supplementary_variables,
17+
find_matching_coord_dims,
18+
get_data_dims,
1719
remove_supplementary_variables,
1820
)
1921

@@ -113,6 +115,42 @@ def setUp(self):
113115
(self.lons, 3),
114116
],
115117
)
118+
self.cube = iris.cube.Cube(
119+
self.new_cube_3D_data,
120+
dim_coords_and_dims=[
121+
(self.depth, 0),
122+
(self.lats, 1),
123+
(self.lons, 2),
124+
],
125+
)
126+
self.plev = iris.coords.DimCoord(
127+
[0, 1.5, 3],
128+
standard_name="air_pressure",
129+
bounds=[[0, 1], [1, 2], [2, 3]],
130+
units="Pa",
131+
)
132+
self.lats_no_metadata = iris.coords.DimCoord(
133+
[0, 1.5, 3],
134+
standard_name="latitude",
135+
bounds=[[0, 1], [1, 2], [2, 3]],
136+
units="degrees_north",
137+
)
138+
self.ancillary_cube_plev = iris.cube.Cube(
139+
np.ones(3),
140+
dim_coords_and_dims=[(self.plev, 0)],
141+
)
142+
self.ancillary_cube_lat_plev = iris.cube.Cube(
143+
np.ones((3, 3)),
144+
dim_coords_and_dims=[(self.lats, 0), (self.plev, 1)],
145+
)
146+
self.ancillary_cube_lat_lon = iris.cube.Cube(
147+
np.ones((3, 3)),
148+
dim_coords_and_dims=[(self.lats, 0), (self.lons, 1)],
149+
)
150+
self.ancillary_cube_lat_no_metadata = iris.cube.Cube(
151+
np.ones(3),
152+
dim_coords_and_dims=[(self.lats_no_metadata, 0)],
153+
)
116154

117155
@pytest.mark.parametrize("lazy", [True, False])
118156
@pytest.mark.parametrize("var_name", ["areacella", "areacello"])
@@ -258,29 +296,63 @@ def test_remove_supplementary_vars(self):
258296

259297
def test_add_ancillary_vars_errors(self):
260298
"""Test errors when adding ancillary variable."""
261-
cube = iris.cube.Cube(
262-
self.new_cube_3D_data,
263-
dim_coords_and_dims=[
264-
(self.depth, 0),
265-
(self.lats, 1),
266-
(self.lons, 2),
267-
],
268-
)
269299
# Ancillary var not an iris.cube.Cube or iris.coords.AncillaryVariable
270300
msg = "ancillary_cube should be either an iris"
271301
with pytest.raises(ValueError, match=msg):
272-
add_ancillary_variable(cube, np.ones(self.new_cube_3D_data.shape))
302+
add_ancillary_variable(
303+
self.cube,
304+
np.ones(self.new_cube_3D_data.shape),
305+
)
273306
# Ancillary var as iris.cube.Cube without matching dimensions
274-
plev_dim = iris.coords.DimCoord(
275-
[0, 1.5, 3],
276-
standard_name="air_pressure",
277-
bounds=[[0, 1], [1, 2], [2, 3]],
278-
units="Pa",
279-
)
280-
ancillary_cube = iris.cube.Cube(
281-
np.ones(3),
282-
dim_coords_and_dims=[(plev_dim, 0)],
307+
with pytest.raises(iris.exceptions.CoordinateNotFoundError):
308+
add_ancillary_variable(
309+
self.cube,
310+
self.ancillary_cube_plev,
311+
)
312+
313+
def test_get_data_dims_no_match(self):
314+
"""Test get_data_dims matching function w/ no match."""
315+
with pytest.raises(iris.exceptions.CoordinateNotFoundError):
316+
get_data_dims(
317+
self.cube,
318+
self.ancillary_cube_plev,
319+
)
320+
321+
def test_get_data_dims_one_match(self):
322+
"""Test get_data_dims matching function w/ only one coordinate match."""
323+
with pytest.raises(iris.exceptions.CoordinateNotFoundError):
324+
_ = get_data_dims(
325+
self.cube,
326+
self.ancillary_cube_lat_plev,
327+
)
328+
329+
def test_get_data_dims_match(self):
330+
"""Test get_data_dims matching function w/ both coordinates match."""
331+
assert get_data_dims(
332+
self.cube,
333+
self.ancillary_cube_lat_lon,
334+
) == [1, 2]
335+
336+
def test_get_data_dims_match_no_metadata(self):
337+
"""Test get_data_dims matching function w/ coordinate w/o metadata."""
338+
assert get_data_dims(
339+
self.cube,
340+
self.ancillary_cube_lat_no_metadata,
341+
) == [1]
342+
343+
def test_find_matching_coord_dims_no_match(self):
344+
"""Test find_matching_coord_dims function w/ no match."""
345+
assert (
346+
find_matching_coord_dims(
347+
self.plev,
348+
self.cube,
349+
)
350+
is None
283351
)
284-
msg = "No coordinate associated with ancillary"
285-
with pytest.raises(ValueError, match=msg):
286-
add_ancillary_variable(cube, ancillary_cube)
352+
353+
def test_find_matching_coord_dims_match(self):
354+
"""Test find_matching_coord_dims function w/ match."""
355+
assert find_matching_coord_dims(
356+
self.lats,
357+
self.cube,
358+
) == (1,)

0 commit comments

Comments
 (0)