Skip to content

Commit a7e0689

Browse files
schlunmapp-mo
authored andcommitted
Handle derived coordinates correctly in concatenate (SciTools#5096)
* First working prototype of concatenate that handels derived coordinates correctly * Added checks for derived coord metadata during concatenation * Added tests * Fixed defaults * Added what's new entry * Optimized test coverage
1 parent 02f2b66 commit a7e0689

File tree

6 files changed

+473
-18
lines changed

6 files changed

+473
-18
lines changed

docs/src/whatsnew/latest.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ This document explains the changes made to Iris for this release
6262
🐛 Bugs Fixed
6363
=============
6464

65+
#. `@schlunma`_ fixed :meth:`iris.cube.CubeList.concatenate` so that it
66+
preserves derived coordinates. (:issue:`2478`, :pull:`5096`)
67+
6568
#. `@trexfeathers`_ and `@pp-mo`_ made Iris' use of the `netCDF4`_ library
6669
thread-safe. (:pull:`5095`)
6770

lib/iris/_concatenate.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,39 @@ def name(self):
160160
return self.defn.name()
161161

162162

163+
class _DerivedCoordAndDims(
164+
namedtuple("DerivedCoordAndDims", ["coord", "dims", "aux_factory"])
165+
):
166+
"""
167+
Container for a derived coordinate, the associated AuxCoordFactory, and the
168+
associated data dimension(s) spanned over a :class:`iris.cube.Cube`.
169+
170+
Args:
171+
172+
* coord:
173+
A :class:`iris.coords.DimCoord` or :class:`iris.coords.AuxCoord`
174+
coordinate instance.
175+
176+
* dims:
177+
A tuple of the data dimension(s) spanned by the coordinate.
178+
179+
* aux_factory:
180+
A :class:`iris.aux_factory.AuxCoordFactory` instance.
181+
182+
"""
183+
184+
__slots__ = ()
185+
186+
def __eq__(self, other):
187+
"""Do not take aux factories into account for equality."""
188+
result = NotImplemented
189+
if isinstance(other, _DerivedCoordAndDims):
190+
equal_coords = self.coord == other.coord
191+
equal_dims = self.dims == other.dims
192+
result = equal_coords and equal_dims
193+
return result
194+
195+
163196
class _OtherMetaData(namedtuple("OtherMetaData", ["defn", "dims"])):
164197
"""
165198
Container for the metadata that defines a cell measure or ancillary
@@ -280,6 +313,7 @@ def concatenate(
280313
check_aux_coords=True,
281314
check_cell_measures=True,
282315
check_ancils=True,
316+
check_derived_coords=True,
283317
):
284318
"""
285319
Concatenate the provided cubes over common existing dimensions.
@@ -296,6 +330,30 @@ def concatenate(
296330
If True, raise an informative
297331
:class:`~iris.exceptions.ContatenateError` if registration fails.
298332
333+
* check_aux_coords
334+
Checks if the points and bounds of auxiliary coordinates of the cubes
335+
match. This check is not applied to auxiliary coordinates that span the
336+
dimension the concatenation is occurring along. Defaults to True.
337+
338+
* check_cell_measures
339+
Checks if the data of cell measures of the cubes match. This check is
340+
not applied to cell measures that span the dimension the concatenation
341+
is occurring along. Defaults to True.
342+
343+
* check_ancils
344+
Checks if the data of ancillary variables of the cubes match. This
345+
check is not applied to ancillary variables that span the dimension the
346+
concatenation is occurring along. Defaults to True.
347+
348+
* check_derived_coords
349+
Checks if the points and bounds of derived coordinates of the cubes
350+
match. This check is not applied to derived coordinates that span the
351+
dimension the concatenation is occurring along. Note that differences
352+
in scalar coordinates and dimensional coordinates used to derive the
353+
coordinate are still checked. Checks for auxiliary coordinates used to
354+
derive the coordinates can be ignored with `check_aux_coords`. Defaults
355+
to True.
356+
299357
Returns:
300358
A :class:`iris.cube.CubeList` of concatenated :class:`iris.cube.Cube`
301359
instances.
@@ -321,6 +379,7 @@ def concatenate(
321379
check_aux_coords,
322380
check_cell_measures,
323381
check_ancils,
382+
check_derived_coords,
324383
)
325384
if registered:
326385
axis = proto_cube.axis
@@ -378,6 +437,8 @@ def __init__(self, cube):
378437
self.cm_metadata = []
379438
self.ancillary_variables_and_dims = []
380439
self.av_metadata = []
440+
self.derived_coords_and_dims = []
441+
self.derived_metadata = []
381442
self.dim_mapping = []
382443

383444
# Determine whether there are any anonymous cube dimensions.
@@ -437,6 +498,17 @@ def meta_key_func(dm):
437498
av_and_dims = _CoordAndDims(av, tuple(dims))
438499
self.ancillary_variables_and_dims.append(av_and_dims)
439500

501+
def name_key_func(factory):
502+
return factory.name()
503+
504+
for factory in sorted(cube.aux_factories, key=name_key_func):
505+
coord = factory.make_coord(cube.coord_dims)
506+
dims = cube.coord_dims(coord)
507+
metadata = _CoordMetaData(coord, dims)
508+
self.derived_metadata.append(metadata)
509+
coord_and_dims = _DerivedCoordAndDims(coord, tuple(dims), factory)
510+
self.derived_coords_and_dims.append(coord_and_dims)
511+
440512
def _coordinate_differences(self, other, attr, reason="metadata"):
441513
"""
442514
Determine the names of the coordinates that differ between `self` and
@@ -544,6 +616,14 @@ def match(self, other, error_on_mismatch):
544616
msgs.append(
545617
msg_template.format("Ancillary variables", *differences)
546618
)
619+
# Check derived coordinates.
620+
if self.derived_metadata != other.derived_metadata:
621+
differences = self._coordinate_differences(
622+
other, "derived_metadata"
623+
)
624+
msgs.append(
625+
msg_template.format("Derived coordinates", *differences)
626+
)
547627
# Check scalar coordinates.
548628
if self.scalar_coords != other.scalar_coords:
549629
differences = self._coordinate_differences(
@@ -597,6 +677,7 @@ def __init__(self, cube_signature):
597677
self.ancillary_variables_and_dims = (
598678
cube_signature.ancillary_variables_and_dims
599679
)
680+
self.derived_coords_and_dims = cube_signature.derived_coords_and_dims
600681
self.dim_coords = cube_signature.dim_coords
601682
self.dim_mapping = cube_signature.dim_mapping
602683
self.dim_extents = []
@@ -779,6 +860,11 @@ def concatenate(self):
779860
# Concatenate the new ancillary variables
780861
ancillary_variables_and_dims = self._build_ancillary_variables()
781862

863+
# Concatenate the new aux factories
864+
aux_factories = self._build_aux_factories(
865+
dim_coords_and_dims, aux_coords_and_dims
866+
)
867+
782868
# Concatenate the new data payload.
783869
data = self._build_data()
784870

@@ -790,6 +876,7 @@ def concatenate(self):
790876
aux_coords_and_dims=aux_coords_and_dims,
791877
cell_measures_and_dims=cell_measures_and_dims,
792878
ancillary_variables_and_dims=ancillary_variables_and_dims,
879+
aux_factories=aux_factories,
793880
**kwargs,
794881
)
795882
else:
@@ -807,6 +894,7 @@ def register(
807894
check_aux_coords=False,
808895
check_cell_measures=False,
809896
check_ancils=False,
897+
check_derived_coords=False,
810898
):
811899
"""
812900
Determine whether the given source-cube is suitable for concatenation
@@ -827,6 +915,31 @@ def register(
827915
* error_on_mismatch:
828916
If True, raise an informative error if registration fails.
829917
918+
* check_aux_coords
919+
Checks if the points and bounds of auxiliary coordinates of the
920+
cubes match. This check is not applied to auxiliary coordinates
921+
that span the dimension the concatenation is occurring along.
922+
Defaults to False.
923+
924+
* check_cell_measures
925+
Checks if the data of cell measures of the cubes match. This check
926+
is not applied to cell measures that span the dimension the
927+
concatenation is occurring along. Defaults to False.
928+
929+
* check_ancils
930+
Checks if the data of ancillary variables of the cubes match. This
931+
check is not applied to ancillary variables that span the dimension
932+
the concatenation is occurring along. Defaults to False.
933+
934+
* check_derived_coords
935+
Checks if the points and bounds of derived coordinates of the cubes
936+
match. This check is not applied to derived coordinates that span
937+
the dimension the concatenation is occurring along. Note that
938+
differences in scalar coordinates and dimensional coordinates used
939+
to derive the coordinate are still checked. Checks for auxiliary
940+
coordinates used to derive the coordinates can be ignored with
941+
`check_aux_coords`. Defaults to False.
942+
830943
Returns:
831944
Boolean.
832945
@@ -905,6 +1018,21 @@ def register(
9051018
if not coord_a == coord_b:
9061019
match = False
9071020

1021+
# Check for compatible derived coordinates.
1022+
if match:
1023+
if check_derived_coords:
1024+
for coord_a, coord_b in zip(
1025+
self._cube_signature.derived_coords_and_dims,
1026+
cube_signature.derived_coords_and_dims,
1027+
):
1028+
# Derived coords that span the candidate axis can differ
1029+
if (
1030+
candidate_axis not in coord_a.dims
1031+
or candidate_axis not in coord_b.dims
1032+
):
1033+
if not coord_a == coord_b:
1034+
match = False
1035+
9081036
if match:
9091037
# Register the cube as a source-cube for this proto-cube.
9101038
self._add_skeleton(coord_signature, cube.lazy_data())
@@ -1088,6 +1216,64 @@ def _build_ancillary_variables(self):
10881216

10891217
return ancillary_variables_and_dims
10901218

1219+
def _build_aux_factories(self, dim_coords_and_dims, aux_coords_and_dims):
1220+
"""
1221+
Generate the aux factories for the new concatenated cube.
1222+
1223+
Args:
1224+
1225+
* dim_coords_and_dims:
1226+
A list of dimension coordinate and dimension tuple pairs from the
1227+
concatenated cube.
1228+
1229+
* aux_coords_and_dims:
1230+
A list of auxiliary coordinates and dimension(s) tuple pairs from
1231+
the concatenated cube.
1232+
1233+
Returns:
1234+
A list of :class:`iris.aux_factory.AuxCoordFactory`.
1235+
1236+
"""
1237+
# Setup convenience hooks.
1238+
cube_signature = self._cube_signature
1239+
old_dim_coords = cube_signature.dim_coords
1240+
old_aux_coords = [a[0] for a in cube_signature.aux_coords_and_dims]
1241+
new_dim_coords = [d[0] for d in dim_coords_and_dims]
1242+
new_aux_coords = [a[0] for a in aux_coords_and_dims]
1243+
scalar_coords = cube_signature.scalar_coords
1244+
1245+
aux_factories = []
1246+
1247+
# Generate all the factories for the new concatenated cube.
1248+
for i, (coord, dims, factory) in enumerate(
1249+
cube_signature.derived_coords_and_dims
1250+
):
1251+
# Check whether the derived coordinate of the factory spans the
1252+
# nominated dimension of concatenation.
1253+
if self.axis in dims:
1254+
# Update the dependencies of the factory with coordinates of
1255+
# the concatenated cube. We need to check all coordinate types
1256+
# here (dim coords, aux coords, and scalar coords).
1257+
new_dependencies = {}
1258+
for old_dependency in factory.dependencies.values():
1259+
if old_dependency in old_dim_coords:
1260+
dep_idx = old_dim_coords.index(old_dependency)
1261+
new_dependency = new_dim_coords[dep_idx]
1262+
elif old_dependency in old_aux_coords:
1263+
dep_idx = old_aux_coords.index(old_dependency)
1264+
new_dependency = new_aux_coords[dep_idx]
1265+
else:
1266+
dep_idx = scalar_coords.index(old_dependency)
1267+
new_dependency = scalar_coords[dep_idx]
1268+
new_dependencies[id(old_dependency)] = new_dependency
1269+
1270+
# Create new factory with the updated dependencies.
1271+
factory = factory.updated(new_dependencies)
1272+
1273+
aux_factories.append(factory)
1274+
1275+
return aux_factories
1276+
10911277
def _build_data(self):
10921278
"""
10931279
Generate the data payload for the new concatenated cube.

0 commit comments

Comments
 (0)