From 777051848c175df8013c593e85c65978d7d09875 Mon Sep 17 00:00:00 2001 From: Henry Wright <84939917+HGWright@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:19:30 +0000 Subject: [PATCH] Allowing exemption to axis guessing on coords (#5551) * allowing excemption to axis guessing on coords * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * updating pr * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove from metadata * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * remove merge clash * adding review comments * more review changes * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * parametrise and add tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix last test * addressing review comments * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix test failure * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * add whatsnew and conftest files * fix sentence * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix flake8 * fix last test * update whatsnew --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- docs/src/whatsnew/latest.rst | 6 ++- lib/iris/coords.py | 38 ++++++++++++-- lib/iris/tests/unit/conftest.py | 14 ++++++ lib/iris/tests/unit/coords/test_Coord.py | 34 +++++++++++++ .../tests/unit/util/test_guess_coord_axis.py | 50 +++++++++++++++++++ lib/iris/util.py | 9 +++- 6 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 lib/iris/tests/unit/conftest.py create mode 100644 lib/iris/tests/unit/util/test_guess_coord_axis.py diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 6e7087c687..0b57a75cd7 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -29,7 +29,7 @@ This document explains the changes made to Iris for this release ✨ Features =========== - + #. `@trexfeathers`_ and `@HGWright`_ (reviewer) sub-categorised all Iris' :class:`UserWarning`\s for richer filtering. The full index of sub-categories can be seen here: :mod:`iris.exceptions` . (:pull:`5498`) @@ -44,6 +44,10 @@ This document explains the changes made to Iris for this release Winter - December to February) will be assigned to the preceding year (e.g. the year of December) instead of the following year (the default behaviour). (:pull:`5573`) + + #. `@HGWright`_ added :attr:`~iris.coords.Coord.ignore_axis` to allow manual + intervention preventing :func:`~iris.util.guess_coord_axis` from acting on a + coordinate. (:pull:`5551`) 🐛 Bugs Fixed diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 30de08d496..8af7ee0c8a 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -36,6 +36,9 @@ import iris.time import iris.util +#: The default value for ignore_axis which controls guess_coord_axis' behaviour +DEFAULT_IGNORE_AXIS = False + class _DimensionalMetadata(CFVariableMixin, metaclass=ABCMeta): """ @@ -860,7 +863,6 @@ def xml_element(self, doc): element.setAttribute( "climatological", str(self.climatological) ) - if self.attributes: attributes_element = doc.createElement("attributes") for name in sorted(self.attributes.keys()): @@ -1593,6 +1595,8 @@ def __init__( self.bounds = bounds self.climatological = climatological + self._ignore_axis = DEFAULT_IGNORE_AXIS + def copy(self, points=None, bounds=None): """ Returns a copy of this coordinate. @@ -1625,6 +1629,10 @@ def copy(self, points=None, bounds=None): # self. new_coord.bounds = bounds + # The state of ignore_axis is controlled by the coordinate rather than + # the metadata manager + new_coord.ignore_axis = self.ignore_axis + return new_coord @classmethod @@ -1644,7 +1652,14 @@ def from_coord(cls, coord): if issubclass(cls, DimCoord): # DimCoord introduces an extra constructor keyword. kwargs["circular"] = getattr(coord, "circular", False) - return cls(**kwargs) + + new_coord = cls(**kwargs) + + # The state of ignore_axis is controlled by the coordinate rather than + # the metadata manager + new_coord.ignore_axis = coord.ignore_axis + + return new_coord @property def points(self): @@ -1736,6 +1751,24 @@ def climatological(self, value): self._metadata_manager.climatological = value + @property + def ignore_axis(self): + """ + A boolean that controls whether guess_coord_axis acts on this + coordinate. + + Defaults to False, and when set to True it will be skipped by + guess_coord_axis. + """ + return self._ignore_axis + + @ignore_axis.setter + def ignore_axis(self, value): + if not isinstance(value, bool): + emsg = "'ignore_axis' can only be set to 'True' or 'False'" + raise ValueError(emsg) + self._ignore_axis = value + def lazy_points(self): """ Return a lazy array representing the coord points. @@ -2694,7 +2727,6 @@ def __init__( Will set to True when a climatological time axis is loaded from NetCDF. Always False if no bounds exist. - """ # Configure the metadata manager. self._metadata_manager = metadata_manager_factory(DimCoordMetadata) diff --git a/lib/iris/tests/unit/conftest.py b/lib/iris/tests/unit/conftest.py new file mode 100644 index 0000000000..a4ddb89294 --- /dev/null +++ b/lib/iris/tests/unit/conftest.py @@ -0,0 +1,14 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +"""Unit tests fixture infra-structure.""" +import pytest + +import iris + + +@pytest.fixture +def sample_coord(): + sample_coord = iris.coords.DimCoord(points=(1, 2, 3, 4, 5)) + return sample_coord diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 1c9c3cce2d..14dcdf7ca0 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -14,6 +14,7 @@ import dask.array as da import numpy as np +import pytest import iris from iris.coords import AuxCoord, Coord, DimCoord @@ -1149,6 +1150,39 @@ def test_change_units(self): self.assertFalse(coord.climatological) +class TestIgnoreAxis: + def test_default(self, sample_coord): + assert sample_coord.ignore_axis is False + + def test_set_true(self, sample_coord): + sample_coord.ignore_axis = True + assert sample_coord.ignore_axis is True + + def test_set_random_value(self, sample_coord): + with pytest.raises( + ValueError, + match=r"'ignore_axis' can only be set to 'True' or 'False'", + ): + sample_coord.ignore_axis = "foo" + + @pytest.mark.parametrize( + "ignore_axis, copy_or_from, result", + [ + (True, "copy", True), + (True, "from_coord", True), + (False, "copy", False), + (False, "from_coord", False), + ], + ) + def test_copy_coord(self, ignore_axis, copy_or_from, result, sample_coord): + sample_coord.ignore_axis = ignore_axis + if copy_or_from == "copy": + new_coord = sample_coord.copy() + elif copy_or_from == "from_coord": + new_coord = sample_coord.from_coord(sample_coord) + assert new_coord.ignore_axis is result + + class Test___init____abstractmethod(tests.IrisTest): def test(self): emsg = ( diff --git a/lib/iris/tests/unit/util/test_guess_coord_axis.py b/lib/iris/tests/unit/util/test_guess_coord_axis.py new file mode 100644 index 0000000000..d946565196 --- /dev/null +++ b/lib/iris/tests/unit/util/test_guess_coord_axis.py @@ -0,0 +1,50 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. +"""Test function :func:`iris.util.guess_coord_axis`.""" + +import pytest + +from iris.util import guess_coord_axis + + +class TestGuessCoord: + @pytest.mark.parametrize( + "coordinate, axis", + [ + ("longitude", "X"), + ("grid_longitude", "X"), + ("projection_x_coordinate", "X"), + ("latitude", "Y"), + ("grid_latitude", "Y"), + ("projection_y_coordinate", "Y"), + ], + ) + def test_coord(self, coordinate, axis, sample_coord): + sample_coord.standard_name = coordinate + assert guess_coord_axis(sample_coord) == axis + + @pytest.mark.parametrize( + "units, axis", + [ + ("hPa", "Z"), + ("days since 1970-01-01 00:00:00", "T"), + ], + ) + def test_units(self, units, axis, sample_coord): + sample_coord.units = units + assert guess_coord_axis(sample_coord) == axis + + @pytest.mark.parametrize( + "ignore_axis, result", + [ + (True, None), + (False, "X"), + ], + ) + def test_ignore_axis(self, ignore_axis, result, sample_coord): + sample_coord.standard_name = "longitude" + sample_coord.ignore_axis = ignore_axis + + assert guess_coord_axis(sample_coord) == result diff --git a/lib/iris/util.py b/lib/iris/util.py index ee415d230e..4509f2885b 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -257,10 +257,17 @@ def guess_coord_axis(coord): This function maintains laziness when called; it does not realise data. See more at :doc:`/userguide/real_and_lazy_data`. + The ``guess_coord_axis`` behaviour can be skipped by setting the coordinate property ``ignore_axis`` + to ``False``. + """ + axis = None - if coord.standard_name in ( + if hasattr(coord, "ignore_axis") and coord.ignore_axis is True: + return axis + + elif coord.standard_name in ( "longitude", "grid_longitude", "projection_x_coordinate",