diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 46a808ab57..26a1a8827d 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -57,6 +57,9 @@ This document explains the changes made to Iris for this release 🐛 Bugs Fixed ============= +#. `@schlunma`_ fixed :meth:`iris.cube.CubeList.concatenate` so that it + preserves derived coordinates. (:issue:`2478`, :pull:`5096`) + #. `@trexfeathers`_ and `@pp-mo`_ made Iris' use of the `netCDF4`_ library thread-safe. (:pull:`5095`) diff --git a/lib/iris/_concatenate.py b/lib/iris/_concatenate.py index 5debc452ee..a383e4e01a 100644 --- a/lib/iris/_concatenate.py +++ b/lib/iris/_concatenate.py @@ -160,6 +160,39 @@ def name(self): return self.defn.name() +class _DerivedCoordAndDims( + namedtuple("DerivedCoordAndDims", ["coord", "dims", "aux_factory"]) +): + """ + Container for a derived coordinate, the associated AuxCoordFactory, and the + associated data dimension(s) spanned over a :class:`iris.cube.Cube`. + + Args: + + * coord: + A :class:`iris.coords.DimCoord` or :class:`iris.coords.AuxCoord` + coordinate instance. + + * dims: + A tuple of the data dimension(s) spanned by the coordinate. + + * aux_factory: + A :class:`iris.aux_factory.AuxCoordFactory` instance. + + """ + + __slots__ = () + + def __eq__(self, other): + """Do not take aux factories into account for equality.""" + result = NotImplemented + if isinstance(other, _DerivedCoordAndDims): + equal_coords = self.coord == other.coord + equal_dims = self.dims == other.dims + result = equal_coords and equal_dims + return result + + class _OtherMetaData(namedtuple("OtherMetaData", ["defn", "dims"])): """ Container for the metadata that defines a cell measure or ancillary @@ -280,6 +313,7 @@ def concatenate( check_aux_coords=True, check_cell_measures=True, check_ancils=True, + check_derived_coords=True, ): """ Concatenate the provided cubes over common existing dimensions. @@ -296,6 +330,30 @@ def concatenate( If True, raise an informative :class:`~iris.exceptions.ContatenateError` if registration fails. + * check_aux_coords + Checks if the points and bounds of auxiliary coordinates of the cubes + match. This check is not applied to auxiliary coordinates that span the + dimension the concatenation is occurring along. Defaults to True. + + * check_cell_measures + Checks if the data of cell measures of the cubes match. This check is + not applied to cell measures that span the dimension the concatenation + is occurring along. Defaults to True. + + * check_ancils + Checks if the data of ancillary variables of the cubes match. This + check is not applied to ancillary variables that span the dimension the + concatenation is occurring along. Defaults to True. + + * check_derived_coords + Checks if the points and bounds of derived coordinates of the cubes + match. This check is not applied to derived coordinates that span the + dimension the concatenation is occurring along. Note that differences + in scalar coordinates and dimensional coordinates used to derive the + coordinate are still checked. Checks for auxiliary coordinates used to + derive the coordinates can be ignored with `check_aux_coords`. Defaults + to True. + Returns: A :class:`iris.cube.CubeList` of concatenated :class:`iris.cube.Cube` instances. @@ -321,6 +379,7 @@ def concatenate( check_aux_coords, check_cell_measures, check_ancils, + check_derived_coords, ) if registered: axis = proto_cube.axis @@ -378,6 +437,8 @@ def __init__(self, cube): self.cm_metadata = [] self.ancillary_variables_and_dims = [] self.av_metadata = [] + self.derived_coords_and_dims = [] + self.derived_metadata = [] self.dim_mapping = [] # Determine whether there are any anonymous cube dimensions. @@ -437,6 +498,17 @@ def meta_key_func(dm): av_and_dims = _CoordAndDims(av, tuple(dims)) self.ancillary_variables_and_dims.append(av_and_dims) + def name_key_func(factory): + return factory.name() + + for factory in sorted(cube.aux_factories, key=name_key_func): + coord = factory.make_coord(cube.coord_dims) + dims = cube.coord_dims(coord) + metadata = _CoordMetaData(coord, dims) + self.derived_metadata.append(metadata) + coord_and_dims = _DerivedCoordAndDims(coord, tuple(dims), factory) + self.derived_coords_and_dims.append(coord_and_dims) + def _coordinate_differences(self, other, attr, reason="metadata"): """ Determine the names of the coordinates that differ between `self` and @@ -544,6 +616,14 @@ def match(self, other, error_on_mismatch): msgs.append( msg_template.format("Ancillary variables", *differences) ) + # Check derived coordinates. + if self.derived_metadata != other.derived_metadata: + differences = self._coordinate_differences( + other, "derived_metadata" + ) + msgs.append( + msg_template.format("Derived coordinates", *differences) + ) # Check scalar coordinates. if self.scalar_coords != other.scalar_coords: differences = self._coordinate_differences( @@ -597,6 +677,7 @@ def __init__(self, cube_signature): self.ancillary_variables_and_dims = ( cube_signature.ancillary_variables_and_dims ) + self.derived_coords_and_dims = cube_signature.derived_coords_and_dims self.dim_coords = cube_signature.dim_coords self.dim_mapping = cube_signature.dim_mapping self.dim_extents = [] @@ -779,6 +860,11 @@ def concatenate(self): # Concatenate the new ancillary variables ancillary_variables_and_dims = self._build_ancillary_variables() + # Concatenate the new aux factories + aux_factories = self._build_aux_factories( + dim_coords_and_dims, aux_coords_and_dims + ) + # Concatenate the new data payload. data = self._build_data() @@ -790,6 +876,7 @@ def concatenate(self): aux_coords_and_dims=aux_coords_and_dims, cell_measures_and_dims=cell_measures_and_dims, ancillary_variables_and_dims=ancillary_variables_and_dims, + aux_factories=aux_factories, **kwargs, ) else: @@ -807,6 +894,7 @@ def register( check_aux_coords=False, check_cell_measures=False, check_ancils=False, + check_derived_coords=False, ): """ Determine whether the given source-cube is suitable for concatenation @@ -827,6 +915,31 @@ def register( * error_on_mismatch: If True, raise an informative error if registration fails. + * check_aux_coords + Checks if the points and bounds of auxiliary coordinates of the + cubes match. This check is not applied to auxiliary coordinates + that span the dimension the concatenation is occurring along. + Defaults to False. + + * check_cell_measures + Checks if the data of cell measures of the cubes match. This check + is not applied to cell measures that span the dimension the + concatenation is occurring along. Defaults to False. + + * check_ancils + Checks if the data of ancillary variables of the cubes match. This + check is not applied to ancillary variables that span the dimension + the concatenation is occurring along. Defaults to False. + + * check_derived_coords + Checks if the points and bounds of derived coordinates of the cubes + match. This check is not applied to derived coordinates that span + the dimension the concatenation is occurring along. Note that + differences in scalar coordinates and dimensional coordinates used + to derive the coordinate are still checked. Checks for auxiliary + coordinates used to derive the coordinates can be ignored with + `check_aux_coords`. Defaults to False. + Returns: Boolean. @@ -905,6 +1018,21 @@ def register( if not coord_a == coord_b: match = False + # Check for compatible derived coordinates. + if match: + if check_derived_coords: + for coord_a, coord_b in zip( + self._cube_signature.derived_coords_and_dims, + cube_signature.derived_coords_and_dims, + ): + # Derived coords that span the candidate axis can differ + if ( + candidate_axis not in coord_a.dims + or candidate_axis not in coord_b.dims + ): + if not coord_a == coord_b: + match = False + if match: # Register the cube as a source-cube for this proto-cube. self._add_skeleton(coord_signature, cube.lazy_data()) @@ -1088,6 +1216,64 @@ def _build_ancillary_variables(self): return ancillary_variables_and_dims + def _build_aux_factories(self, dim_coords_and_dims, aux_coords_and_dims): + """ + Generate the aux factories for the new concatenated cube. + + Args: + + * dim_coords_and_dims: + A list of dimension coordinate and dimension tuple pairs from the + concatenated cube. + + * aux_coords_and_dims: + A list of auxiliary coordinates and dimension(s) tuple pairs from + the concatenated cube. + + Returns: + A list of :class:`iris.aux_factory.AuxCoordFactory`. + + """ + # Setup convenience hooks. + cube_signature = self._cube_signature + old_dim_coords = cube_signature.dim_coords + old_aux_coords = [a[0] for a in cube_signature.aux_coords_and_dims] + new_dim_coords = [d[0] for d in dim_coords_and_dims] + new_aux_coords = [a[0] for a in aux_coords_and_dims] + scalar_coords = cube_signature.scalar_coords + + aux_factories = [] + + # Generate all the factories for the new concatenated cube. + for i, (coord, dims, factory) in enumerate( + cube_signature.derived_coords_and_dims + ): + # Check whether the derived coordinate of the factory spans the + # nominated dimension of concatenation. + if self.axis in dims: + # Update the dependencies of the factory with coordinates of + # the concatenated cube. We need to check all coordinate types + # here (dim coords, aux coords, and scalar coords). + new_dependencies = {} + for old_dependency in factory.dependencies.values(): + if old_dependency in old_dim_coords: + dep_idx = old_dim_coords.index(old_dependency) + new_dependency = new_dim_coords[dep_idx] + elif old_dependency in old_aux_coords: + dep_idx = old_aux_coords.index(old_dependency) + new_dependency = new_aux_coords[dep_idx] + else: + dep_idx = scalar_coords.index(old_dependency) + new_dependency = scalar_coords[dep_idx] + new_dependencies[id(old_dependency)] = new_dependency + + # Create new factory with the updated dependencies. + factory = factory.updated(new_dependencies) + + aux_factories.append(factory) + + return aux_factories + def _build_data(self): """ Generate the data payload for the new concatenated cube. diff --git a/lib/iris/cube.py b/lib/iris/cube.py index b09f61aefb..7c6fd55c10 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -542,6 +542,7 @@ def concatenate_cube( check_aux_coords=True, check_cell_measures=True, check_ancils=True, + check_derived_coords=True, ): """ Return the concatenated contents of the :class:`CubeList` as a single @@ -554,20 +555,30 @@ def concatenate_cube( Kwargs: * check_aux_coords - Checks the auxiliary coordinates of the cubes match. This check - is not applied to auxiliary coordinates that span the dimension - the concatenation is occurring along. Defaults to True. + Checks if the points and bounds of auxiliary coordinates of the + cubes match. This check is not applied to auxiliary coordinates + that span the dimension the concatenation is occurring along. + Defaults to True. * check_cell_measures - Checks the cell measures of the cubes match. This check - is not applied to cell measures that span the dimension - the concatenation is occurring along. Defaults to True. + Checks if the data of cell measures of the cubes match. This check + is not applied to cell measures that span the dimension the + concatenation is occurring along. Defaults to True. * check_ancils - Checks the ancillary variables of the cubes match. This check - is not applied to ancillary variables that span the dimension + Checks if the data of ancillary variables of the cubes match. This + check is not applied to ancillary variables that span the dimension the concatenation is occurring along. Defaults to True. + * check_derived_coords + Checks if the points and bounds of derived coordinates of the cubes + match. This check is not applied to derived coordinates that span + the dimension the concatenation is occurring along. Note that + differences in scalar coordinates and dimensional coordinates used + to derive the coordinate are still checked. Checks for auxiliary + coordinates used to derive the coordinates can be ignored with + `check_aux_coords`. Defaults to True. + .. note:: Concatenation cannot occur along an anonymous dimension. @@ -587,6 +598,7 @@ def concatenate_cube( check_aux_coords=check_aux_coords, check_cell_measures=check_cell_measures, check_ancils=check_ancils, + check_derived_coords=check_derived_coords, ) n_res_cubes = len(res) if n_res_cubes == 1: @@ -613,6 +625,7 @@ def concatenate( check_aux_coords=True, check_cell_measures=True, check_ancils=True, + check_derived_coords=True, ): """ Concatenate the cubes over their common dimensions. @@ -620,20 +633,30 @@ def concatenate( Kwargs: * check_aux_coords - Checks the auxiliary coordinates of the cubes match. This check - is not applied to auxiliary coordinates that span the dimension - the concatenation is occurring along. Defaults to True. + Checks if the points and bounds of auxiliary coordinates of the + cubes match. This check is not applied to auxiliary coordinates + that span the dimension the concatenation is occurring along. + Defaults to True. * check_cell_measures - Checks the cell measures of the cubes match. This check - is not applied to cell measures that span the dimension - the concatenation is occurring along. Defaults to True. + Checks if the data of cell measures of the cubes match. This check + is not applied to cell measures that span the dimension the + concatenation is occurring along. Defaults to True. * check_ancils - Checks the ancillary variables of the cubes match. This check - is not applied to ancillary variables that span the dimension + Checks if the data of ancillary variables of the cubes match. This + check is not applied to ancillary variables that span the dimension the concatenation is occurring along. Defaults to True. + * check_derived_coords + Checks if the points and bounds of derived coordinates of the cubes + match. This check is not applied to derived coordinates that span + the dimension the concatenation is occurring along. Note that + differences in scalar coordinates and dimensional coordinates used + to derive the coordinate are still checked. Checks for auxiliary + coordinates used to derive the coordinates can be ignored with + `check_aux_coords`. Defaults to True. + Returns: A new :class:`iris.cube.CubeList` of concatenated :class:`iris.cube.Cube` instances. @@ -718,6 +741,7 @@ def concatenate( check_aux_coords=check_aux_coords, check_cell_measures=check_cell_measures, check_ancils=check_ancils, + check_derived_coords=check_derived_coords, ) def realise_data(self): diff --git a/lib/iris/tests/integration/concatenate/test_concatenate.py b/lib/iris/tests/integration/concatenate/test_concatenate.py index 091ecd4378..1f39b2589d 100644 --- a/lib/iris/tests/integration/concatenate/test_concatenate.py +++ b/lib/iris/tests/integration/concatenate/test_concatenate.py @@ -16,13 +16,43 @@ import cf_units import numpy as np -from iris._concatenate import concatenate +from iris._concatenate import _DerivedCoordAndDims, concatenate +import iris.aux_factory import iris.coords import iris.cube import iris.tests.stock as stock from iris.util import unify_time_units +class Test_DerivedCoordAndDims: + def test_equal(self): + assert _DerivedCoordAndDims( + "coord", "dims", "aux_factory" + ) == _DerivedCoordAndDims("coord", "dims", "aux_factory") + + def test_non_equal_coord(self): + assert _DerivedCoordAndDims( + "coord_0", "dims", "aux_factory" + ) != _DerivedCoordAndDims("coord_1", "dims", "aux_factory") + + def test_non_equal_dims(self): + assert _DerivedCoordAndDims( + "coord", "dims_0", "aux_factory" + ) != _DerivedCoordAndDims("coord", "dims_1", "aux_factory") + + def test_non_equal_aux_factory(self): + # Note: aux factories are not taken into account for equality! + assert _DerivedCoordAndDims( + "coord", "dims", "aux_factory_0" + ) == _DerivedCoordAndDims("coord", "dims", "aux_factory_1") + + def test_non_equal_types(self): + assert ( + _DerivedCoordAndDims("coord", "dims", "aux_factory") + != "I am not a _DerivedCoordAndDims" + ) + + class Test_concatenate__epoch(tests.IrisTest): def simple_1d_time_cubes(self, reftimes, coords_points): cubes = [] @@ -187,6 +217,127 @@ def test_ignore_diff_ancillary_variables(self): self.assertEqual(result[0].shape, (4, 2)) +class Test_cubes_with_derived_coord(tests.IrisTest): + def create_cube(self): + data = np.arange(4).reshape(2, 2) + aux_factories = [] + + # DimCoords + sigma = iris.coords.DimCoord([0.0, 10.0], var_name="sigma", units="1") + t_unit = cf_units.Unit( + "hours since 1970-01-01 00:00:00", calendar="standard" + ) + time = iris.coords.DimCoord([0, 6], standard_name="time", units=t_unit) + + # AtmosphereSigmaFactory (does not span concatenated dim) + ptop = iris.coords.AuxCoord(100.0, var_name="ptop", units="Pa") + surface_p = iris.coords.AuxCoord([1.0, 2.0], var_name="ps", units="Pa") + aux_factories.append( + iris.aux_factory.AtmosphereSigmaFactory(ptop, sigma, surface_p) + ) + + # HybridHeightFactory (span concatenated dim) + delta = iris.coords.AuxCoord(10.0, var_name="delta", units="m") + orog = iris.coords.AuxCoord(data, var_name="orog", units="m") + aux_factories.append( + iris.aux_factory.HybridHeightFactory(delta, sigma, orog) + ) + + dim_coords_and_dims = [(time, 0), (sigma, 1)] + aux_coords_and_dims = [ + (ptop, ()), + (delta, ()), + (surface_p, 1), + (orog, (0, 1)), + ] + + cube = iris.cube.Cube( + data, + standard_name="air_temperature", + units="K", + dim_coords_and_dims=dim_coords_and_dims, + aux_coords_and_dims=aux_coords_and_dims, + aux_factories=aux_factories, + ) + return cube + + def test_equal_derived_coords(self): + cube_a = self.create_cube() + cube_b = cube_a.copy() + cube_b.coord("time").points = [12, 18] + + result = concatenate([cube_a, cube_b]) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].shape, (4, 2)) + + np.testing.assert_allclose( + result[0].coord("air_pressure").points, [100.0, -880.0] + ) + np.testing.assert_allclose( + result[0].coord("altitude").points, + [[10.0, 20.0], [10.0, 40.0], [10.0, 20.0], [10.0, 40.0]], + ) + + def test_equal_derived_coords_with_bounds(self): + cube_a = self.create_cube() + cube_a.coord("sigma").bounds = [[0.0, 5.0], [5.0, 20.0]] + cube_b = cube_a.copy() + cube_b.coord("time").points = [12, 18] + + result = concatenate([cube_a, cube_b]) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].shape, (4, 2)) + + np.testing.assert_allclose( + result[0].coord("air_pressure").bounds, + [[100.0, -395.0], [-390.0, -1860.0]], + ) + + def test_diff_altitude(self): + """Gives one cube since altitude spans concatenation dim.""" + cube_a = self.create_cube() + cube_b = cube_a.copy() + cube_b.coord("time").points = [12, 18] + cube_b.coord("orog").points = [[0, 0], [0, 0]] + + result = concatenate([cube_a, cube_b]) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].shape, (4, 2)) + + np.testing.assert_allclose( + result[0].coord("altitude").points, + [[10.0, 20.0], [10.0, 40.0], [10.0, 10.0], [10.0, 10.0]], + ) + + def test_diff_air_pressure(self): + """Gives two cubes since altitude does not span concatenation dim.""" + cube_a = self.create_cube() + cube_b = cube_a.copy() + cube_b.coord("time").points = [12, 18] + cube_b.coord("ps").points = [10.0, 20.0] + + result = concatenate([cube_a, cube_b], check_aux_coords=False) + self.assertEqual(len(result), 2) + + def test_ignore_diff_air_pressure(self): + cube_a = self.create_cube() + cube_b = cube_a.copy() + cube_b.coord("time").points = [12, 18] + cube_b.coord("ps").points = [10.0, 20.0] + + result = concatenate( + [cube_a, cube_b], + check_aux_coords=False, + check_derived_coords=False, + ) + self.assertEqual(len(result), 1) + self.assertEqual(result[0].shape, (4, 2)) + + np.testing.assert_allclose( + result[0].coord("air_pressure").points, [100.0, -880.0] + ) + + class Test_anonymous_dims(tests.IrisTest): def setUp(self): data = np.arange(12).reshape(2, 3, 2) diff --git a/lib/iris/tests/test_concatenate.py b/lib/iris/tests/test_concatenate.py index 968b71d292..e4c22f49b0 100644 --- a/lib/iris/tests/test_concatenate.py +++ b/lib/iris/tests/test_concatenate.py @@ -15,13 +15,22 @@ import numpy as np import numpy.ma as ma +from iris.aux_factory import HybridHeightFactory from iris.coords import AncillaryVariable, AuxCoord, CellMeasure, DimCoord import iris.cube import iris.tests.stock as stock def _make_cube( - x, y, data, aux=None, cell_measure=None, ancil=None, offset=0, scalar=None + x, + y, + data, + aux=None, + cell_measure=None, + ancil=None, + derived=None, + offset=0, + scalar=None, ): """ A convenience test function that creates a custom 2D cube. @@ -47,6 +56,18 @@ def _make_cube( A CSV string specifying which points only auxiliary coordinates to create. Accepts either of 'x', 'y', 'xy'. + * cell_measure: + A CSV string specifying which points only cell measures + coordinates to create. Accepts either of 'x', 'y', 'xy'. + + * ancil: + A CSV string specifying which points only ancillary variables + coordinates to create. Accepts either of 'x', 'y', 'xy'. + + * derived: + A CSV string specifying which points only derived coordinates + coordinates to create. Accepts either of 'x', 'y', 'xy'. + * offset: Offset value to be added to the 'xy' auxiliary coordinate points. @@ -120,6 +141,30 @@ def _make_cube( ) cube.add_ancillary_variable(av, (0, 1)) + if derived is not None: + derived = derived.split(",") + delta = AuxCoord(0.0, var_name="delta", units="m") + sigma = AuxCoord(1.0, var_name="sigma", units="1") + cube.add_aux_coord(delta, ()) + cube.add_aux_coord(sigma, ()) + if "y" in derived: + orog = AuxCoord(y_range * 10, long_name="orog", units="m") + cube.add_aux_coord(orog, 0) + elif "x" in derived: + orog = AuxCoord(x_range * 10, long_name="orog", units="m") + cube.add_aux_coord(orog, 1) + elif "xy" in derived: + payload = np.arange(y_size * x_size, dtype=np.float32).reshape( + y_size, x_size + ) + orog = AuxCoord( + payload * 100 + offset, long_name="orog", units="m" + ) + cube.add_aux_coord(orog, (0, 1)) + else: + raise NotImplementedError() + cube.add_aux_factory(HybridHeightFactory(delta, sigma, orog)) + if scalar is not None: data = np.array([scalar], dtype=np.float32) coord = AuxCoord(data, long_name="height", units="m") @@ -362,6 +407,14 @@ def test_ancil_missing(self): result = concatenate(cubes) self.assertEqual(len(result), 2) + def test_derived_coord_missing(self): + cubes = [] + y = (0, 2) + cubes.append(_make_cube((0, 2), y, 1, derived="x")) + cubes.append(_make_cube((2, 4), y, 2)) + result = concatenate(cubes) + self.assertEqual(len(result), 2) + class Test2D(tests.IrisTest): def test_masked_and_unmasked(self): @@ -736,6 +789,17 @@ def test_concat_2y2d_ancil_x_y_xy(self): self.assertEqual(result[0].shape, (6, 2)) self.assertEqual(result[0], com) + def test_concat_2y2d_derived_x_y_xy(self): + cubes = [] + x = (0, 2) + cubes.append(_make_cube(x, (0, 4), 1, derived="x,y,xy")) + cubes.append(_make_cube(x, (4, 6), 1, derived="x,y,xy")) + result = concatenate(cubes) + com = _make_cube(x, (0, 6), 1, derived="x,y,xy") + self.assertEqual(len(result), 1) + self.assertEqual(result[0].shape, (6, 2)) + self.assertEqual(result[0], com) + class TestMulti2D(tests.IrisTest): def test_concat_4x2d_aux_xy(self): diff --git a/lib/iris/tests/unit/concatenate/test_concatenate.py b/lib/iris/tests/unit/concatenate/test_concatenate.py index 96d13d7d15..a4243dfbbc 100644 --- a/lib/iris/tests/unit/concatenate/test_concatenate.py +++ b/lib/iris/tests/unit/concatenate/test_concatenate.py @@ -15,6 +15,7 @@ from iris._concatenate import concatenate from iris._lazy_data import as_lazy_data +from iris.aux_factory import HybridHeightFactory import iris.coords import iris.cube from iris.exceptions import ConcatenateError @@ -90,16 +91,26 @@ def setUp(self): iris.coords.AuxCoord([0, 1, 2], long_name="foo", units="1"), data_dims=(1,), ) + # Cell Measures cube.add_cell_measure( iris.coords.CellMeasure([0, 1, 2], long_name="bar", units="1"), data_dims=(1,), ) + # Ancillary Variables cube.add_ancillary_variable( iris.coords.AncillaryVariable( [0, 1, 2], long_name="baz", units="1" ), data_dims=(1,), ) + # Derived Coords + delta = iris.coords.AuxCoord(0.0, var_name="delta", units="m") + sigma = iris.coords.AuxCoord(1.0, var_name="sigma", units="1") + orog = iris.coords.AuxCoord(2.0, var_name="orog", units="m") + cube.add_aux_coord(delta, ()) + cube.add_aux_coord(sigma, ()) + cube.add_aux_coord(orog, ()) + cube.add_aux_factory(HybridHeightFactory(delta, sigma, orog)) self.cube = cube def test_definition_difference_message(self): @@ -190,6 +201,22 @@ def test_ancillary_variable_metadata_difference_message(self): with self.assertRaisesRegex(ConcatenateError, exc_regexp): _ = concatenate([cube_1, cube_2], True) + def test_derived_coord_difference_message(self): + cube_1 = self.cube + cube_2 = cube_1.copy() + cube_2.remove_aux_factory(cube_2.aux_factories[0]) + exc_regexp = "Derived coordinates differ: .* != .*" + with self.assertRaisesRegex(ConcatenateError, exc_regexp): + _ = concatenate([cube_1, cube_2], True) + + def test_derived_coord_metadata_difference_message(self): + cube_1 = self.cube + cube_2 = cube_1.copy() + cube_2.aux_factories[0].units = "km" + exc_regexp = "Derived coordinates metadata differ: .* != .*" + with self.assertRaisesRegex(ConcatenateError, exc_regexp): + _ = concatenate([cube_1, cube_2], True) + def test_ndim_difference_message(self): cube_1 = self.cube cube_2 = iris.cube.Cube(