From 80a83b21b9d2dff3e9837c5396d806bdf4552dba Mon Sep 17 00:00:00 2001 From: cpelley Date: Fri, 5 Jul 2024 12:56:24 +0100 Subject: [PATCH] MAINT: changes based on feedback --- improver/cli/extract.py | 13 +---- improver/utilities/complex_conversion.py | 3 +- improver/utilities/cube_extraction.py | 15 ++++-- improver/wind_calculations/wind_direction.py | 47 ----------------- .../complex_conversion/test_integration.py | 27 ++++++++++ .../cube_extraction/test_ExtractSubCube.py | 13 ++++- .../wind_direction/test_WindDirection.py | 52 ++----------------- 7 files changed, 58 insertions(+), 112 deletions(-) create mode 100644 improver_tests/utilities/complex_conversion/test_integration.py diff --git a/improver/cli/extract.py b/improver/cli/extract.py index 89fefc0ed6..7047158be9 100755 --- a/improver/cli/extract.py +++ b/improver/cli/extract.py @@ -54,15 +54,6 @@ def process( """ from improver.utilities.cube_extraction import ExtractSubCube - plugin = ExtractSubCube(constraints, units=units) - try: - result = plugin.process(cube) - except ValueError as err: - if ( - err.args[0] == "Constraint(s) could not be matched in input cube" - and ignore_failure - ): - return cube - else: - raise + plugin = ExtractSubCube(constraints, units=units, ignore_failure=ignore_failure) + result = plugin.process(cube) return result diff --git a/improver/utilities/complex_conversion.py b/improver/utilities/complex_conversion.py index a195dcbbe7..b44ee601ea 100644 --- a/improver/utilities/complex_conversion.py +++ b/improver/utilities/complex_conversion.py @@ -13,8 +13,7 @@ def deg_to_complex( ) -> Union[ndarray, float]: """Converts degrees to complex values. - The radius value can be used to weigh values - but it is set - to 1 for now. + The radius argument can be used to weight values. Defaults to 1. Args: angle_deg: diff --git a/improver/utilities/cube_extraction.py b/improver/utilities/cube_extraction.py index 8fcb3486d1..ebf84d2185 100644 --- a/improver/utilities/cube_extraction.py +++ b/improver/utilities/cube_extraction.py @@ -273,6 +273,7 @@ def __init__( constraints: List[str], units: Optional[List[str]] = None, use_original_units: bool = True, + ignore_failure: bool = False, ) -> None: """ Set up the ExtractSubCube plugin. @@ -290,10 +291,14 @@ def __init__( should be converted back to their original units. The default is True, indicating that the units should be converted back to the original units. + ignore_failure: + Option to ignore constraint match failure and return the input + cube. """ self._constraints = constraints self._units = units self._use_original_units = use_original_units + self._ignore_failure = ignore_failure def process(self, cube: Cube): """Perform the subcube extraction. @@ -309,12 +314,14 @@ def process(self, cube: Cube): ValueError: If the constraint(s) could not be matched to the input cube. """ cube = as_cube(cube) - cube = extract_subcube( + res = extract_subcube( cube, self._constraints, self._units, self._use_original_units ) - if cube is None: - raise ValueError("Constraint(s) could not be matched in input cube") - return cube + if res is None: + res = cube + if not self._ignore_failure: + raise ValueError("Constraint(s) could not be matched in input cube") + return res def thin_cube(cube: Cube, thinning_dict: Dict[str, int]) -> Cube: diff --git a/improver/wind_calculations/wind_direction.py b/improver/wind_calculations/wind_direction.py index 9a1dab12c7..bd1f8cea3e 100644 --- a/improver/wind_calculations/wind_direction.py +++ b/improver/wind_calculations/wind_direction.py @@ -3,9 +3,6 @@ # This file is part of IMPROVER and is released under a BSD 3-Clause license. # See LICENSE in the root of the repository for full licensing details. """Module containing wind direction averaging plugins.""" - -from typing import Union - import iris import numpy as np from iris.coords import CellMethod @@ -105,50 +102,6 @@ def _reset(self) -> None: self.wdir_mean_complex = None self.r_vals_slice = None - @staticmethod - def deg_to_complex( - angle_deg: Union[ndarray, float], radius: Union[ndarray, float] = 1 - ) -> Union[ndarray, float]: - """Converts degrees to complex values. - - See deg_to_complex - - The radius value can be used to weigh values - but it is set - to 1 for now. - - Args: - angle_deg: - 3D array or float - wind direction angles in degrees. - radius: - 3D array or float - radius value for each point, default=1. - - Returns: - 3D array or float - wind direction translated to - complex numbers. - """ - return deg_to_complex(angle_deg, radius) - - @staticmethod - def complex_to_deg(complex_in: ndarray) -> ndarray: - """Converts complex to degrees. - - The "np.angle" function returns negative numbers when the input - is greater than 180. Therefore additional processing is needed - to ensure that the angle is between 0-359. - - Args: - complex_in: - 3D array - wind direction angles in - complex number form. - - Returns: - 3D array - wind direction in angle form - - Raises: - TypeError: If complex_in is not an array. - """ - return complex_to_deg(complex_in) - def calc_wind_dir_mean(self) -> None: """Find the mean wind direction using complex average which actually signifies a point between all of the data points in POLAR diff --git a/improver_tests/utilities/complex_conversion/test_integration.py b/improver_tests/utilities/complex_conversion/test_integration.py new file mode 100644 index 0000000000..a68939f6dc --- /dev/null +++ b/improver_tests/utilities/complex_conversion/test_integration.py @@ -0,0 +1,27 @@ +# (C) Crown copyright, Met Office. All rights reserved. +# +# This file is part of IMPROVER and is released under a BSD 3-Clause license. +# See LICENSE in the root of the repository for full licensing details. +import numpy as np +from numpy.testing import assert_array_almost_equal + +from improver.utilities.complex_conversion import deg_to_complex, complex_to_deg + +from . import COMPLEX_ANGLES + + +def test_roundtrip_complex_deg_complex(): + """Tests that array of values are converted to degrees and back.""" + tmp_degrees = complex_to_deg(COMPLEX_ANGLES) + result = deg_to_complex(tmp_degrees) + assert_array_almost_equal(result, COMPLEX_ANGLES) + + +def test_roundtrip_deg_complex_deg(): + """Tests that array of values are converted to complex and back.""" + src_degrees = np.arange(0., 360, 10) + tmp_complex = deg_to_complex(src_degrees) + result = complex_to_deg(tmp_complex) + assert_array_almost_equal(result, src_degrees) + + \ No newline at end of file diff --git a/improver_tests/utilities/cube_extraction/test_ExtractSubCube.py b/improver_tests/utilities/cube_extraction/test_ExtractSubCube.py index b8a654ac4a..e61b07f83f 100644 --- a/improver_tests/utilities/cube_extraction/test_ExtractSubCube.py +++ b/improver_tests/utilities/cube_extraction/test_ExtractSubCube.py @@ -19,6 +19,7 @@ def test_ui(mock_extract_subcube, mock_as_cube): sentinel.constraints, units=sentinel.units, use_original_units=sentinel.use_original_units, + ignore_failure=sentinel.ignore_failure, ) plugin(sentinel.cube) mock_as_cube.assert_called_once_with(sentinel.cube) @@ -31,7 +32,17 @@ def test_no_matching_constraint_exception(): """ Test that a ValueError is raised when no constraints match. """ - plugin = ExtractSubCube(["dummy_name=dummy_value"]) + plugin = ExtractSubCube(["dummy_name=dummy_value"], ignore_failure=False) with pytest.raises(ValueError) as excinfo: plugin(Cube(0)) assert str(excinfo.value) == "Constraint(s) could not be matched in input cube" + + +def test_no_matching_constraint_ignore(): + """ + Test that the original cube is returned when no constraints match and ignore specified. + """ + plugin = ExtractSubCube(["dummy_name=dummy_value"], ignore_failure=True) + src_cube = Cube(0) + res = plugin(src_cube) + assert res is src_cube diff --git a/improver_tests/wind_calculations/wind_direction/test_WindDirection.py b/improver_tests/wind_calculations/wind_direction/test_WindDirection.py index 0203feea77..abb29e59f2 100644 --- a/improver_tests/wind_calculations/wind_direction/test_WindDirection.py +++ b/improver_tests/wind_calculations/wind_direction/test_WindDirection.py @@ -5,7 +5,6 @@ """Unit tests for the wind_direction.WindDirection plugin.""" import unittest -import unittest.mock as mock import numpy as np from iris.coords import DimCoord @@ -13,7 +12,7 @@ from iris.tests import IrisTest from improver.synthetic_data.set_up_test_cubes import set_up_variable_cube -from improver.wind_calculations.wind_direction import WindDirection +from improver.wind_calculations.wind_direction import WindDirection, deg_to_complex # Data to test complex/degree handling functions. # Complex angles equivalent to np.arange(0., 360, 10) degrees. @@ -180,45 +179,6 @@ def test_basic(self): self.assertEqual(result, msg) -@mock.patch("improver.wind_calculations.wind_direction.deg_to_complex") -def test_deg_to_complex(mock_deg_to_complex): - """Tests that the underlying deg_to_complex function is - called via the deg_to_complex method.""" - WindDirection().deg_to_complex(mock.sentinel.angle_deg, mock.sentinel.radius) - mock_deg_to_complex.assert_called_once_with( - mock.sentinel.angle_deg, mock.sentinel.radius - ) - - -@mock.patch("improver.wind_calculations.wind_direction.complex_to_deg") -def test_complex_to_deg(mock_complex_to_deg): - """Tests that the underlying complex_to_deg function is - called via the complex_to_deg method.""" - WindDirection().complex_to_deg(mock.sentinel.complex_in) - mock_complex_to_deg.assert_called_once_with(mock.sentinel.complex_in) - - -class Test_complex_to_deg_roundtrip(IrisTest): - """Test the complex_to_deg and deg_to_complex functions together.""" - - def setUp(self): - """Initialise plugin and supply data for tests""" - self.plugin = WindDirection() - self.cube = make_wdir_cube_534() - - def test_from_deg(self): - """Tests that array of values are converted to complex and back.""" - tmp_complex = self.plugin.deg_to_complex(self.cube.data) - result = self.plugin.complex_to_deg(tmp_complex) - self.assertArrayAlmostEqual(result, self.cube.data, decimal=4) - - def test_from_complex(self): - """Tests that array of values are converted to degrees and back.""" - tmp_degrees = self.plugin.complex_to_deg(COMPLEX_ANGLES) - result = self.plugin.deg_to_complex(tmp_degrees) - self.assertArrayAlmostEqual(result, COMPLEX_ANGLES) - - class Test_calc_wind_dir_mean(IrisTest): """Test the calc_wind_dir_mean function.""" @@ -227,7 +187,7 @@ def setUp(self): self.plugin = WindDirection() # 5x3x4 3D Array containing wind direction in angles. cube = make_wdir_cube_534() - self.plugin.wdir_complex = self.plugin.deg_to_complex(cube.data) + self.plugin.wdir_complex = deg_to_complex(cube.data) self.plugin.wdir_slice_mean = next(cube.slices_over("realization")) self.plugin.realization_axis = 0 @@ -244,7 +204,7 @@ def test_complex(self): """Test that the function defines correct complex mean.""" self.plugin.calc_wind_dir_mean() result = self.plugin.wdir_mean_complex - expected_complex = self.plugin.deg_to_complex( + expected_complex = deg_to_complex( self.expected_wind_mean, radius=np.absolute(result) ) self.assertArrayAlmostEqual(result, expected_complex) @@ -312,9 +272,7 @@ def test_runs_function_1st_member(self): self.plugin.wdir_slice_mean = cube[0].copy( data=np.array([[180.0, 55.0], [280.0, 0.0]]) ) - self.plugin.wdir_mean_complex = self.plugin.deg_to_complex( - self.plugin.wdir_slice_mean.data - ) + self.plugin.wdir_mean_complex = deg_to_complex(self.plugin.wdir_slice_mean.data) expected_out = np.array([[90.0, 55.0], [280.0, 0.0]]) where_low_r = np.array([[True, False], [False, False]]) self.plugin.wind_dir_decider(where_low_r, cube) @@ -344,7 +302,7 @@ def test_runs_function_nbhood(self): self.plugin.realization_axis = 0 self.plugin.n_realizations = 1 self.plugin.wdir_mean_complex = np.pad( - self.plugin.deg_to_complex(wind_dir_deg_mean), + deg_to_complex(wind_dir_deg_mean), ((4, 4), (4, 4)), "constant", constant_values=(0.0, 0.0),