diff --git a/src/CSET/operators/read.py b/src/CSET/operators/read.py index b9f6e9a74..a43fef8d5 100644 --- a/src/CSET/operators/read.py +++ b/src/CSET/operators/read.py @@ -23,6 +23,7 @@ import iris import iris.coords import iris.cube +import iris.util import numpy as np @@ -144,6 +145,13 @@ def read_cubes( cubes = cubes.merge() cubes = cubes.concatenate() + # Ensure dimension coordinates are bounded. + for cube in cubes: + for dim_coord in cube.coords(dim_coords=True): + # Iris can't guess the bounds of a scalar coordinate. + if not dim_coord.has_bounds() and dim_coord.shape[0] > 1: + dim_coord.guess_bounds() + logging.debug("Loaded cubes: %s", cubes) if len(cubes) == 0: warnings.warn( @@ -185,6 +193,7 @@ def callback(cube: iris.cube.Cube, field, filename: str): else: _deterministic_callback(cube, field, filename) _lfric_normalise_callback(cube, field, filename) + _lfric_time_coord_fix_callback(cube, field, filename) return callback @@ -252,6 +261,21 @@ def _lfric_normalise_callback(cube: iris.cube.Cube, field, filename): cube.attributes["um_stash_source"] = str(sorted(ast.literal_eval(stash_list))) +def _lfric_time_coord_fix_callback(cube: iris.cube.Cube, field, filename): + """Ensure the time coordinate is a DimCoord rather than an AuxCoord. + + The coordinate is converted and replaced if not. SLAMed LFRic data has this + issue, though the coordinate satisfies all the properties for a DimCoord. + Scalar time values are left as AuxCoords. + """ + if cube.coords("time"): + time_coord = cube.coord("time") + if not isinstance(time_coord, iris.coords.DimCoord) and cube.coord_dims( + time_coord + ): + iris.util.promote_aux_coord_to_dim_coord(cube, time_coord) + + def _check_input_files(input_path: Path | str, filename_pattern: str) -> Iterable[Path]: """Get an iterable of files to load, and check that they all exist. diff --git a/tests/operators/test_read.py b/tests/operators/test_read.py index 8ad284451..06cf14a48 100644 --- a/tests/operators/test_read.py +++ b/tests/operators/test_read.py @@ -14,6 +14,8 @@ """Reading operator tests.""" +import iris +import iris.coords import iris.cube import pytest @@ -192,3 +194,31 @@ def test_lfric_normalise_callback_sort_stash(cube): actual = cube.attributes["um_stash_source"] expected = "['m01s00i025', 'm01s03i025']" assert actual == expected + + +def test_lfric_time_coord_fix_callback(): + """Correctly convert time from AuxCoord to DimCoord.""" + time_coord = iris.coords.AuxCoord([0, 1, 2], standard_name="time") + cube = iris.cube.Cube([0, 0, 0], aux_coords_and_dims=[(time_coord, 0)]) + read._lfric_time_coord_fix_callback(cube, None, None) + assert isinstance(cube.coord("time"), iris.coords.DimCoord) + assert cube.coord_dims("time") == (0,) + + +def test_lfric_time_coord_fix_callback_scalar_time(): + """Correctly convert time from AuxCoord to DimCoord for scalar time.""" + length_coord = iris.coords.DimCoord([0, 1, 2], var_name="length") + time_coord = iris.coords.AuxCoord([0], standard_name="time") + cube = iris.cube.Cube([0, 0, 0], aux_coords_and_dims=[(length_coord, 0)]) + cube.add_aux_coord(time_coord) + read._lfric_time_coord_fix_callback(cube, None, None) + assert isinstance(cube.coord("time"), iris.coords.AuxCoord) + assert cube.coord_dims("time") == () + + +def test_lfric_time_coord_fix_callback_no_time(): + """Don't do anything if no time coordinate present.""" + length_coord = iris.coords.DimCoord([0, 1, 2], var_name="length") + cube = iris.cube.Cube([0, 0, 0], aux_coords_and_dims=[(length_coord, 0)]) + read._lfric_time_coord_fix_callback(cube, None, None) + assert len(cube.coords("time")) == 0