diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index ea4b52ce2a..d0aefd02c1 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -47,6 +47,13 @@ This document explains the changes made to Iris for this release :class:`~iris.coords.AuxCoord`, which avoids some specific known usage problems. (:issue:`4860`, :pull:`5020`) +#. `@Esadek-MO`_ and `@trexfeathers`_ added dim coord + prioritisation to ``_get_lon_lat_coords()`` in :mod:`iris.analysis.cartography`. + This allows :func:`iris.analysis.cartography.area_weights` and + :func:`~iris.analysis.cartography.project` to handle cubes which contain + both dim and aux coords of the same type e.g. ``longitude`` and ``grid_longitude``. + (:issue:`3916`, :pull:`5029`). + 🐛 Bugs Fixed ============= diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index f38e48354d..8dbad9c4e9 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -169,20 +169,25 @@ def rotate_pole(lons, lats, pole_lon, pole_lat): def _get_lon_lat_coords(cube): - lat_coords = [ - coord for coord in cube.coords() if "latitude" in coord.name() - ] - lon_coords = [ - coord for coord in cube.coords() if "longitude" in coord.name() - ] + def search_for_coord(coord_iterable, coord_name): + return [ + coord for coord in coord_iterable if coord_name in coord.name() + ] + + lat_coords = search_for_coord( + cube.dim_coords, "latitude" + ) or search_for_coord(cube.coords(), "latitude") + lon_coords = search_for_coord( + cube.dim_coords, "longitude" + ) or search_for_coord(cube.coords(), "longitude") if len(lat_coords) > 1 or len(lon_coords) > 1: raise ValueError( - "Calling `_get_lon_lat_coords` with multiple lat or lon coords" + "Calling `_get_lon_lat_coords` with multiple same-type (i.e. dim/aux) lat or lon coords" " is currently disallowed" ) lat_coord = lat_coords[0] lon_coord = lon_coords[0] - return (lon_coord, lat_coord) + return lon_coord, lat_coord def _xy_range(cube, mode=None): @@ -578,6 +583,11 @@ def project(cube, target_proj, nx=None, ny=None): An instance of :class:`iris.cube.Cube` and a list describing the extent of the projection. + .. note:: + + If there are both dim and aux latitude-longitude coordinates, only + the dim coordinates will be used. + .. note:: This function assumes global data and will if necessary extrapolate diff --git a/lib/iris/tests/unit/analysis/cartography/test__get_lon_lat_coords.py b/lib/iris/tests/unit/analysis/cartography/test__get_lon_lat_coords.py new file mode 100644 index 0000000000..612e5d8ecf --- /dev/null +++ b/lib/iris/tests/unit/analysis/cartography/test__get_lon_lat_coords.py @@ -0,0 +1,114 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Test function :func:`iris.analysis.cartography._get_lon_lat_coords""" + +import pytest + +from iris.analysis.cartography import _get_lon_lat_coords as g_lon_lat +from iris.coords import AuxCoord +from iris.tests.stock import lat_lon_cube + + +@pytest.fixture +def dim_only_cube(): + return lat_lon_cube() + + +def test_dim_only(dim_only_cube): + t_lat, t_lon = dim_only_cube.dim_coords + + lon, lat = g_lon_lat(dim_only_cube) + + assert lon == t_lon + assert lat == t_lat + + +@pytest.fixture +def dim_aux_cube(dim_only_cube): + lat_dim, lon_dim = dim_only_cube.dim_coords + + lat_aux = AuxCoord.from_coord(lat_dim) + lat_aux.standard_name = "grid_latitude" + lon_aux = AuxCoord.from_coord(lon_dim) + lon_aux.standard_name = "grid_longitude" + + dim_aux_cube = dim_only_cube + dim_aux_cube.add_aux_coord(lat_aux, 0) + dim_aux_cube.add_aux_coord(lon_aux, 1) + + return dim_aux_cube + + +def test_dim_aux(dim_aux_cube): + t_lat_dim, t_lon_dim = dim_aux_cube.dim_coords + + lon, lat = g_lon_lat(dim_aux_cube) + + assert lon == t_lon_dim + assert lat == t_lat_dim + + +@pytest.fixture +def aux_only_cube(dim_aux_cube): + lon_dim, lat_dim = dim_aux_cube.dim_coords + + aux_only_cube = dim_aux_cube + aux_only_cube.remove_coord(lon_dim) + aux_only_cube.remove_coord(lat_dim) + + return dim_aux_cube + + +def test_aux_only(aux_only_cube): + aux_lat, aux_lon = aux_only_cube.aux_coords + + lon, lat = g_lon_lat(aux_only_cube) + + assert lon == aux_lon + assert lat == aux_lat + + +@pytest.fixture +def double_dim_cube(dim_only_cube): + double_dim_cube = dim_only_cube + double_dim_cube.coord("latitude").standard_name = "grid_longitude" + + return double_dim_cube + + +def test_double_dim(double_dim_cube): + t_error_message = "with multiple.*is currently disallowed" + + with pytest.raises(ValueError, match=t_error_message): + g_lon_lat(double_dim_cube) + + +@pytest.fixture +def double_aux_cube(aux_only_cube): + double_aux_cube = aux_only_cube + double_aux_cube.coord("grid_latitude").standard_name = "longitude" + + return double_aux_cube + + +def test_double_aux(double_aux_cube): + t_error_message = "with multiple.*is currently disallowed" + + with pytest.raises(ValueError, match=t_error_message): + g_lon_lat(double_aux_cube) + + +@pytest.fixture +def missing_lat_cube(dim_only_cube): + missing_lat_cube = dim_only_cube + missing_lat_cube.remove_coord("latitude") + + return missing_lat_cube + + +def test_missing_coord(missing_lat_cube): + with pytest.raises(IndexError): + g_lon_lat(missing_lat_cube)