From a6b8dc166e476404f8e8ffb10e4e7f4a518226f0 Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Tue, 26 Sep 2023 09:52:04 +0100 Subject: [PATCH 1/9] Adds in plugin and tests to calculate the maximum value over a height coordinate --- improver/cli/max_in_height.py | 69 ++++++++++++++++++ improver/utilities/spatial.py | 52 +++++++++++++ improver_tests/acceptance/SHA256SUMS | 3 + .../acceptance/test_max_in_height.py | 73 +++++++++++++++++++ improver_tests/utilities/test_spatial.py | 32 ++++++++ 5 files changed, 229 insertions(+) create mode 100644 improver/cli/max_in_height.py create mode 100644 improver_tests/acceptance/test_max_in_height.py diff --git a/improver/cli/max_in_height.py b/improver/cli/max_in_height.py new file mode 100644 index 0000000000..4650f01366 --- /dev/null +++ b/improver/cli/max_in_height.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# (C) British Crown copyright. The Met Office. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""Script to calculate the maximum over the height coordinate""" + +from improver import cli + + +@cli.clizefy +@cli.with_output +def process( + cube: cli.inputcube, + *, + lower_height_bound: float = None, + upper_height_bound: float = None, +): + """Calculate the maximum value over the height coordinate of a cube. If height bounds are specified + then the maximum value between these height levels is calculated. + + Args: + cube (iris.cube.Cube): + A cube with a height coordinate. + lower_height_bound (float): + The lower bound for the height coordinate. This is either a float of None if no lower + bound is desired. Any specified bounds should have the same units as the height + coordinate of cube. + upper_height_bound (float): + The upper bound for the height coordinate. This is either a float of None if no upper + bound is desired. Any specified bounds should have the same units as the height + coordinate of cube. + Returns: + A cube of the maximum value over the height coordinate or maximum value between the provided + height bounds.""" + + from improver.utilities.spatial import maximum_in_height + + return maximum_in_height( + cube, + lower_height_bound=lower_height_bound, + upper_height_bound=upper_height_bound, + ) diff --git a/improver/utilities/spatial.py b/improver/utilities/spatial.py index ccc6a3e447..fd311cc0e4 100644 --- a/improver/utilities/spatial.py +++ b/improver/utilities/spatial.py @@ -761,3 +761,55 @@ def add_vicinity_coordinate( point, units=units, long_name="radius_of_vicinity", attributes=attributes ) cube.add_aux_coord(coord) + + +def maximum_in_height( + cube: Cube, lower_height_bound: float = None, upper_height_bound: float = None +) -> Cube: + """Calculate the maximum value over the height coordinate. If bounds are specified + then the maximum value between the lower_height_bound and upper_height_bound is calculated. + + If either the upper or lower bound is None then no bound is applied. For example if no + lower bound is provided but an upper bound of 300m is provided then the maximum is + calculated for all height levels less than 300m. + + Args: + cube: + A cube with a height coordinate. + lower_height_bound: + The lower bound for the height coordinate. This is either a float of None if no + lower bound is desired. Any specified bounds should have the same units as the + height coordinate of cube. + upper_height_bound: + The upper bound for the height coordinate. This is either a float of None if no + upper bound is desired. Any specified bounds should have the same units as the + height coordinate of cube. + Returns: + A cube of the maximum value over the height coordinate or maximum value between the desired + height values. + """ + cube_name = cube.name() + height_levels = cube.coord("height").points + + # replace None in bounds with a numerical value either below or above the range of height + # levels in the cube so it can be used as a constraint. + print(lower_height_bound) + if lower_height_bound is None: + lower_height_bound = min(height_levels) + if upper_height_bound is None: + upper_height_bound = max(height_levels) + + height_constraint = iris.Constraint( + height=lambda height: lower_height_bound <= height <= upper_height_bound + ) + cube_subsetted = cube.extract(height_constraint) + + if len(cube_subsetted.coord("height").points) > 1: + max_cube = cube_subsetted.collapsed("height", iris.analysis.MAX) + else: + max_cube = cube_subsetted + + max_cube.rename( + f"maximum_{cube_name}_between_{lower_height_bound}m_and_{upper_height_bound}m" + ) + return max_cube diff --git a/improver_tests/acceptance/SHA256SUMS b/improver_tests/acceptance/SHA256SUMS index 4fe6753521..dcbda7d840 100644 --- a/improver_tests/acceptance/SHA256SUMS +++ b/improver_tests/acceptance/SHA256SUMS @@ -424,6 +424,9 @@ a709db26d352457bf4f1bddf5dbade499fe89d455669e567af8965eccbebe9c4 ./manipulate-r 5bb4bb6ac2ce9ab29277275b26964a7f3633ee4c7cfa4aa4f48769e091379188 ./manipulate-reliability-table/basic/reliability_table_precip.nc 7f1093fc474320887110f28cbce1881ca68f3ed30e0fa6b2a265633e31fdd269 ./manipulate-reliability-table/point_by_point/kgo_point_by_point.nc a162ff6c31dd3f0d84e1633c0cca28083e731876a69d40b7f14c6ff4a3431f25 ./manipulate-reliability-table/point_by_point/reliability_table_point_by_point.nc +6ea87b3fd108055dcba84ff0349f95aea82cfb9665bd7c93cd2c1a065349b2a0 ./max-in-height/input.nc +7863604e127a498bd34dbe8f4410f4283058df72f785b5b292a07e5c1add468f ./max-in-height/kgo_with_bounds.nc +56f51a92c0dc3e853b6ef2abb342395e5dd4cc8ab339ea20994b95caafda4e21 ./max-in-height/kgo_without_bounds.nc a2de3ea5608d30d4ac2759e9ff4c4a6573e2c0e8e655595682a2ec2691c2de74 ./max-in-time-window/input_PT0029H00M.nc ac81531fa507a2a3a12d4adb542ed014594eff4a38137f947d3a68a2063fab49 ./max-in-time-window/input_PT0032H00M.nc fb02306f960fa36cf9a86921ec6599541e0a0823dfa546856000f810cdd96d73 ./max-in-time-window/kgo.nc diff --git a/improver_tests/acceptance/test_max_in_height.py b/improver_tests/acceptance/test_max_in_height.py new file mode 100644 index 0000000000..0808aa5bb3 --- /dev/null +++ b/improver_tests/acceptance/test_max_in_height.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# (C) British Crown copyright. The Met Office. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""Tests the max_in_height CLI""" + +import pytest + +from . import acceptance as acc + +pytestmark = [pytest.mark.acc, acc.skip_if_kgo_missing] +CLI = acc.cli_name_with_dashes(__file__) +run_cli = acc.run_cli(CLI) + + +def test_with_bounds(tmp_path): + """Test max_in_height computation with specified bounds""" + + kgo_dir = acc.kgo_root() / "max-in-height" + input_path = kgo_dir / "input.nc" + output_path = tmp_path / "output.nc" + args = [ + input_path, + "--upper-height-bound", + "3000", + "--lower-height-bound", + "500", + "--output", + f"{output_path}", + ] + + kgo_path = kgo_dir / "kgo_with_bounds.nc" + run_cli(args) + acc.compare(output_path, kgo_path) + + +def test_without_bounds(tmp_path): + """Test max_in_height computation without bounds.""" + + kgo_dir = acc.kgo_root() / "max-in-height" + input_path = kgo_dir / "input.nc" + output_path = tmp_path / "output.nc" + args = [input_path, "--output", f"{output_path}"] + + kgo_path = kgo_dir / "kgo_without_bounds.nc" + run_cli(args) + acc.compare(output_path, kgo_path) diff --git a/improver_tests/utilities/test_spatial.py b/improver_tests/utilities/test_spatial.py index dc5db34ebb..095b7a07c1 100644 --- a/improver_tests/utilities/test_spatial.py +++ b/improver_tests/utilities/test_spatial.py @@ -55,6 +55,7 @@ distance_to_number_of_grid_cells, get_grid_y_x_values, lat_lon_determine, + maximum_in_height, number_of_grid_cells_to_distance, transform_grid_to_lat_lon, update_name_and_vicinity_coord, @@ -643,3 +644,34 @@ def test_update_name_and_vicinity_coord(vicinity_radius, input_has_coord): assert coord_comment is None else: assert coord_comment == "Maximum" + + +@pytest.mark.parametrize( + "lower_bound,upper_bound,expected", + ( + (None, None, [300, 400, 300]), + (None, 200, [300, 400, 100]), + (250, None, [200, 300, 300]), + (50, 1000, [300, 400, 300]), + ), +) +def test_maximum_in_height(lower_bound, upper_bound, expected): + """Test that the maximum over the height coordinate is correctly calculated for + different combinations of upper and lower bounds.""" + + data = np.array( + [ + [[100, 200, 100], [100, 200, 100]], + [[300, 400, 100], [300, 400, 100]], + [[200, 300, 300], [200, 300, 300]], + ] + ) + cube = set_up_variable_cube( + data=data, name="wet_bulb_temperature", height_levels=[100, 200, 300] + ) + result = maximum_in_height( + cube, lower_height_bound=lower_bound, upper_height_bound=upper_bound + ) + + assert np.allclose(result.data, [expected] * 2) + assert "maximum_wet_bulb_temperature_between" in result.name() From b6b95b47e0ea7c6a515237062fcfadf2fd150660 Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Tue, 26 Sep 2023 10:05:31 +0100 Subject: [PATCH 2/9] formatting line length --- improver/cli/max_in_height.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/improver/cli/max_in_height.py b/improver/cli/max_in_height.py index 4650f01366..28bef9f025 100644 --- a/improver/cli/max_in_height.py +++ b/improver/cli/max_in_height.py @@ -42,8 +42,8 @@ def process( lower_height_bound: float = None, upper_height_bound: float = None, ): - """Calculate the maximum value over the height coordinate of a cube. If height bounds are specified - then the maximum value between these height levels is calculated. + """Calculate the maximum value over the height coordinate of a cube. If height bounds are + specified then the maximum value between these height levels is calculated. Args: cube (iris.cube.Cube): From 0d6b3d03fdff3c137c0b8204ba3d270392bce5b5 Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Wed, 27 Sep 2023 14:07:15 +0100 Subject: [PATCH 3/9] remove print and update docstring --- improver/utilities/spatial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/improver/utilities/spatial.py b/improver/utilities/spatial.py index fd311cc0e4..3cbd5edf40 100644 --- a/improver/utilities/spatial.py +++ b/improver/utilities/spatial.py @@ -786,14 +786,14 @@ def maximum_in_height( height coordinate of cube. Returns: A cube of the maximum value over the height coordinate or maximum value between the desired - height values. + height values. This cube inherits Iris' meta-data updates to the height coordinate and to the + cell methods """ cube_name = cube.name() height_levels = cube.coord("height").points # replace None in bounds with a numerical value either below or above the range of height # levels in the cube so it can be used as a constraint. - print(lower_height_bound) if lower_height_bound is None: lower_height_bound = min(height_levels) if upper_height_bound is None: From 41557df4db22e083a6a0019385624cf553d22b4f Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Wed, 27 Sep 2023 15:25:12 +0100 Subject: [PATCH 4/9] Move plugin to cube_manipulation --- improver/cli/max_in_height.py | 6 +- improver/utilities/cube_manipulation.py | 51 +++++++++++++ improver/utilities/spatial.py | 54 +------------- .../test_maximum_in_height.py | 71 +++++++++++++++++++ improver_tests/utilities/test_spatial.py | 34 +-------- 5 files changed, 127 insertions(+), 89 deletions(-) create mode 100644 improver_tests/utilities/cube_manipulation/test_maximum_in_height.py diff --git a/improver/cli/max_in_height.py b/improver/cli/max_in_height.py index 28bef9f025..102716d139 100644 --- a/improver/cli/max_in_height.py +++ b/improver/cli/max_in_height.py @@ -49,18 +49,18 @@ def process( cube (iris.cube.Cube): A cube with a height coordinate. lower_height_bound (float): - The lower bound for the height coordinate. This is either a float of None if no lower + The lower bound for the height coordinate. This is either a float or None if no lower bound is desired. Any specified bounds should have the same units as the height coordinate of cube. upper_height_bound (float): - The upper bound for the height coordinate. This is either a float of None if no upper + The upper bound for the height coordinate. This is either a float or None if no upper bound is desired. Any specified bounds should have the same units as the height coordinate of cube. Returns: A cube of the maximum value over the height coordinate or maximum value between the provided height bounds.""" - from improver.utilities.spatial import maximum_in_height + from improver.utilities.cube_manipulation import maximum_in_height return maximum_in_height( cube, diff --git a/improver/utilities/cube_manipulation.py b/improver/utilities/cube_manipulation.py index 948f288999..a407a859eb 100644 --- a/improver/utilities/cube_manipulation.py +++ b/improver/utilities/cube_manipulation.py @@ -705,3 +705,54 @@ def add_coordinate_to_cube( output_cube.transpose(final_dim_order) return output_cube + +def maximum_in_height( + cube: Cube, lower_height_bound: float = None, upper_height_bound: float = None +) -> Cube: + """Calculate the maximum value over the height coordinate. If bounds are specified + then the maximum value between the lower_height_bound and upper_height_bound is calculated. + + If either the upper or lower bound is None then no bound is applied. For example if no + lower bound is provided but an upper bound of 300m is provided then the maximum is + calculated for all height levels less than 300m. + + Args: + cube: + A cube with a height coordinate. + lower_height_bound: + The lower bound for the height coordinate. This is either a float or None if no + lower bound is desired. Any specified bounds should have the same units as the + height coordinate of cube. + upper_height_bound: + The upper bound for the height coordinate. This is either a float or None if no + upper bound is desired. Any specified bounds should have the same units as the + height coordinate of cube. + Returns: + A cube of the maximum value over the height coordinate or maximum value between the desired + height values. This cube inherits Iris' meta-data updates to the height coordinate and to the + cell methods + """ + cube_name = cube.name() + height_levels = cube.coord("height").points + + # replace None in bounds with a numerical value either below or above the range of height + # levels in the cube so it can be used as a constraint. + if lower_height_bound is None: + lower_height_bound = min(height_levels) + if upper_height_bound is None: + upper_height_bound = max(height_levels) + + height_constraint = iris.Constraint( + height=lambda height: lower_height_bound <= height <= upper_height_bound + ) + cube_subsetted = cube.extract(height_constraint) + + if len(cube_subsetted.coord("height").points) > 1: + max_cube = cube_subsetted.collapsed("height", iris.analysis.MAX) + else: + max_cube = cube_subsetted + + max_cube.rename( + f"maximum_{cube_name}_between_{lower_height_bound}m_and_{upper_height_bound}m" + ) + return max_cube diff --git a/improver/utilities/spatial.py b/improver/utilities/spatial.py index 3cbd5edf40..894fe6fb09 100644 --- a/improver/utilities/spatial.py +++ b/improver/utilities/spatial.py @@ -760,56 +760,4 @@ def add_vicinity_coordinate( coord = AuxCoord( point, units=units, long_name="radius_of_vicinity", attributes=attributes ) - cube.add_aux_coord(coord) - - -def maximum_in_height( - cube: Cube, lower_height_bound: float = None, upper_height_bound: float = None -) -> Cube: - """Calculate the maximum value over the height coordinate. If bounds are specified - then the maximum value between the lower_height_bound and upper_height_bound is calculated. - - If either the upper or lower bound is None then no bound is applied. For example if no - lower bound is provided but an upper bound of 300m is provided then the maximum is - calculated for all height levels less than 300m. - - Args: - cube: - A cube with a height coordinate. - lower_height_bound: - The lower bound for the height coordinate. This is either a float of None if no - lower bound is desired. Any specified bounds should have the same units as the - height coordinate of cube. - upper_height_bound: - The upper bound for the height coordinate. This is either a float of None if no - upper bound is desired. Any specified bounds should have the same units as the - height coordinate of cube. - Returns: - A cube of the maximum value over the height coordinate or maximum value between the desired - height values. This cube inherits Iris' meta-data updates to the height coordinate and to the - cell methods - """ - cube_name = cube.name() - height_levels = cube.coord("height").points - - # replace None in bounds with a numerical value either below or above the range of height - # levels in the cube so it can be used as a constraint. - if lower_height_bound is None: - lower_height_bound = min(height_levels) - if upper_height_bound is None: - upper_height_bound = max(height_levels) - - height_constraint = iris.Constraint( - height=lambda height: lower_height_bound <= height <= upper_height_bound - ) - cube_subsetted = cube.extract(height_constraint) - - if len(cube_subsetted.coord("height").points) > 1: - max_cube = cube_subsetted.collapsed("height", iris.analysis.MAX) - else: - max_cube = cube_subsetted - - max_cube.rename( - f"maximum_{cube_name}_between_{lower_height_bound}m_and_{upper_height_bound}m" - ) - return max_cube + cube.add_aux_coord(coord) \ No newline at end of file diff --git a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py new file mode 100644 index 0000000000..ed2fb88150 --- /dev/null +++ b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# (C) British Crown copyright. The Met Office. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +""" +Unit tests for the function "cube_manipulation.maximum_in_height". +""" + +import pytest +import numpy as np + +from improver.synthetic_data.set_up_test_cubes import ( + set_up_variable_cube, +) +from improver.utilities.cube_manipulation import maximum_in_height + +@pytest.mark.parametrize( + "lower_bound,upper_bound,expected", + ( + (None, None, [300, 400, 300]), + (None, 200, [300, 400, 100]), + (250, None, [200, 300, 300]), + (50, 1000, [300, 400, 300]), + ), +) +def test_maximum_in_height(lower_bound, upper_bound, expected): + """Test that the maximum over the height coordinate is correctly calculated for + different combinations of upper and lower bounds.""" + + data = np.array( + [ + [[100, 200, 100], [100, 200, 100]], + [[300, 400, 100], [300, 400, 100]], + [[200, 300, 300], [200, 300, 300]], + ] + ) + cube = set_up_variable_cube( + data=data, name="wet_bulb_temperature", height_levels=[100, 200, 300] + ) + result = maximum_in_height( + cube, lower_height_bound=lower_bound, upper_height_bound=upper_bound + ) + + assert np.allclose(result.data, [expected] * 2) + assert "maximum_wet_bulb_temperature_between" in result.name() \ No newline at end of file diff --git a/improver_tests/utilities/test_spatial.py b/improver_tests/utilities/test_spatial.py index 095b7a07c1..be47e331e4 100644 --- a/improver_tests/utilities/test_spatial.py +++ b/improver_tests/utilities/test_spatial.py @@ -55,7 +55,6 @@ distance_to_number_of_grid_cells, get_grid_y_x_values, lat_lon_determine, - maximum_in_height, number_of_grid_cells_to_distance, transform_grid_to_lat_lon, update_name_and_vicinity_coord, @@ -643,35 +642,4 @@ def test_update_name_and_vicinity_coord(vicinity_radius, input_has_coord): if input_has_coord: assert coord_comment is None else: - assert coord_comment == "Maximum" - - -@pytest.mark.parametrize( - "lower_bound,upper_bound,expected", - ( - (None, None, [300, 400, 300]), - (None, 200, [300, 400, 100]), - (250, None, [200, 300, 300]), - (50, 1000, [300, 400, 300]), - ), -) -def test_maximum_in_height(lower_bound, upper_bound, expected): - """Test that the maximum over the height coordinate is correctly calculated for - different combinations of upper and lower bounds.""" - - data = np.array( - [ - [[100, 200, 100], [100, 200, 100]], - [[300, 400, 100], [300, 400, 100]], - [[200, 300, 300], [200, 300, 300]], - ] - ) - cube = set_up_variable_cube( - data=data, name="wet_bulb_temperature", height_levels=[100, 200, 300] - ) - result = maximum_in_height( - cube, lower_height_bound=lower_bound, upper_height_bound=upper_bound - ) - - assert np.allclose(result.data, [expected] * 2) - assert "maximum_wet_bulb_temperature_between" in result.name() + assert coord_comment == "Maximum" \ No newline at end of file From 67e52a3fdbafdd595103b67d7c0713d321b5bc73 Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Wed, 27 Sep 2023 15:26:54 +0100 Subject: [PATCH 5/9] formatting --- improver/utilities/cube_manipulation.py | 1 + improver/utilities/spatial.py | 2 +- .../utilities/cube_manipulation/test_maximum_in_height.py | 7 +++---- improver_tests/utilities/test_spatial.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/improver/utilities/cube_manipulation.py b/improver/utilities/cube_manipulation.py index a407a859eb..9b1575c922 100644 --- a/improver/utilities/cube_manipulation.py +++ b/improver/utilities/cube_manipulation.py @@ -706,6 +706,7 @@ def add_coordinate_to_cube( return output_cube + def maximum_in_height( cube: Cube, lower_height_bound: float = None, upper_height_bound: float = None ) -> Cube: diff --git a/improver/utilities/spatial.py b/improver/utilities/spatial.py index 894fe6fb09..ccc6a3e447 100644 --- a/improver/utilities/spatial.py +++ b/improver/utilities/spatial.py @@ -760,4 +760,4 @@ def add_vicinity_coordinate( coord = AuxCoord( point, units=units, long_name="radius_of_vicinity", attributes=attributes ) - cube.add_aux_coord(coord) \ No newline at end of file + cube.add_aux_coord(coord) diff --git a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py index ed2fb88150..38bf12126a 100644 --- a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py +++ b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py @@ -35,11 +35,10 @@ import pytest import numpy as np -from improver.synthetic_data.set_up_test_cubes import ( - set_up_variable_cube, -) +from improver.synthetic_data.set_up_test_cubes import set_up_variable_cube from improver.utilities.cube_manipulation import maximum_in_height + @pytest.mark.parametrize( "lower_bound,upper_bound,expected", ( @@ -68,4 +67,4 @@ def test_maximum_in_height(lower_bound, upper_bound, expected): ) assert np.allclose(result.data, [expected] * 2) - assert "maximum_wet_bulb_temperature_between" in result.name() \ No newline at end of file + assert "maximum_wet_bulb_temperature_between" in result.name() diff --git a/improver_tests/utilities/test_spatial.py b/improver_tests/utilities/test_spatial.py index be47e331e4..dc5db34ebb 100644 --- a/improver_tests/utilities/test_spatial.py +++ b/improver_tests/utilities/test_spatial.py @@ -642,4 +642,4 @@ def test_update_name_and_vicinity_coord(vicinity_radius, input_has_coord): if input_has_coord: assert coord_comment is None else: - assert coord_comment == "Maximum" \ No newline at end of file + assert coord_comment == "Maximum" From ee1ee4f0797befa03af5c03a480d19dfe41bebf8 Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Wed, 27 Sep 2023 15:31:39 +0100 Subject: [PATCH 6/9] Formatting isort and flake8 --- improver/utilities/cube_manipulation.py | 4 ++-- .../utilities/cube_manipulation/test_maximum_in_height.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/improver/utilities/cube_manipulation.py b/improver/utilities/cube_manipulation.py index 9b1575c922..1751e45033 100644 --- a/improver/utilities/cube_manipulation.py +++ b/improver/utilities/cube_manipulation.py @@ -730,8 +730,8 @@ def maximum_in_height( height coordinate of cube. Returns: A cube of the maximum value over the height coordinate or maximum value between the desired - height values. This cube inherits Iris' meta-data updates to the height coordinate and to the - cell methods + height values. This cube inherits Iris' meta-data updates to the height coordinate and to + the cell methods. """ cube_name = cube.name() height_levels = cube.coord("height").points diff --git a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py index 38bf12126a..8924b2ae3c 100644 --- a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py +++ b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py @@ -32,8 +32,8 @@ Unit tests for the function "cube_manipulation.maximum_in_height". """ -import pytest import numpy as np +import pytest from improver.synthetic_data.set_up_test_cubes import set_up_variable_cube from improver.utilities.cube_manipulation import maximum_in_height From 7eaf42e300c3bf57d7ceab538ff4f056912a9f8d Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Tue, 3 Oct 2023 13:29:08 +0100 Subject: [PATCH 7/9] Update metadata of cube --- improver/utilities/cube_manipulation.py | 4 ---- improver_tests/acceptance/SHA256SUMS | 4 ++-- .../utilities/cube_manipulation/test_maximum_in_height.py | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/improver/utilities/cube_manipulation.py b/improver/utilities/cube_manipulation.py index 1751e45033..1ca48a6568 100644 --- a/improver/utilities/cube_manipulation.py +++ b/improver/utilities/cube_manipulation.py @@ -733,7 +733,6 @@ def maximum_in_height( height values. This cube inherits Iris' meta-data updates to the height coordinate and to the cell methods. """ - cube_name = cube.name() height_levels = cube.coord("height").points # replace None in bounds with a numerical value either below or above the range of height @@ -753,7 +752,4 @@ def maximum_in_height( else: max_cube = cube_subsetted - max_cube.rename( - f"maximum_{cube_name}_between_{lower_height_bound}m_and_{upper_height_bound}m" - ) return max_cube diff --git a/improver_tests/acceptance/SHA256SUMS b/improver_tests/acceptance/SHA256SUMS index dcbda7d840..1e6e3487ef 100644 --- a/improver_tests/acceptance/SHA256SUMS +++ b/improver_tests/acceptance/SHA256SUMS @@ -425,8 +425,8 @@ a709db26d352457bf4f1bddf5dbade499fe89d455669e567af8965eccbebe9c4 ./manipulate-r 7f1093fc474320887110f28cbce1881ca68f3ed30e0fa6b2a265633e31fdd269 ./manipulate-reliability-table/point_by_point/kgo_point_by_point.nc a162ff6c31dd3f0d84e1633c0cca28083e731876a69d40b7f14c6ff4a3431f25 ./manipulate-reliability-table/point_by_point/reliability_table_point_by_point.nc 6ea87b3fd108055dcba84ff0349f95aea82cfb9665bd7c93cd2c1a065349b2a0 ./max-in-height/input.nc -7863604e127a498bd34dbe8f4410f4283058df72f785b5b292a07e5c1add468f ./max-in-height/kgo_with_bounds.nc -56f51a92c0dc3e853b6ef2abb342395e5dd4cc8ab339ea20994b95caafda4e21 ./max-in-height/kgo_without_bounds.nc +4e269623426087ee9583fce5819162770ca63fc043a5fb90eb1b1d2934a6d5ca ./max-in-height/kgo_with_bounds.nc +96a6de95ee4750164b11bbae92ebf5da3f1bcc90b96ed4b9db266b27abf10346 ./max-in-height/kgo_without_bounds.nc a2de3ea5608d30d4ac2759e9ff4c4a6573e2c0e8e655595682a2ec2691c2de74 ./max-in-time-window/input_PT0029H00M.nc ac81531fa507a2a3a12d4adb542ed014594eff4a38137f947d3a68a2063fab49 ./max-in-time-window/input_PT0032H00M.nc fb02306f960fa36cf9a86921ec6599541e0a0823dfa546856000f810cdd96d73 ./max-in-time-window/kgo.nc diff --git a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py index 8924b2ae3c..9c72c870bc 100644 --- a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py +++ b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py @@ -67,4 +67,4 @@ def test_maximum_in_height(lower_bound, upper_bound, expected): ) assert np.allclose(result.data, [expected] * 2) - assert "maximum_wet_bulb_temperature_between" in result.name() + assert "wet_bulb_temperature" == result.name() From 74cb410e819d64349ac02df85267ccbcade74d5e Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Wed, 4 Oct 2023 16:33:59 +0100 Subject: [PATCH 8/9] Update checksums for new data --- improver_tests/acceptance/SHA256SUMS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/improver_tests/acceptance/SHA256SUMS b/improver_tests/acceptance/SHA256SUMS index 1e6e3487ef..38b6099454 100644 --- a/improver_tests/acceptance/SHA256SUMS +++ b/improver_tests/acceptance/SHA256SUMS @@ -424,9 +424,9 @@ a709db26d352457bf4f1bddf5dbade499fe89d455669e567af8965eccbebe9c4 ./manipulate-r 5bb4bb6ac2ce9ab29277275b26964a7f3633ee4c7cfa4aa4f48769e091379188 ./manipulate-reliability-table/basic/reliability_table_precip.nc 7f1093fc474320887110f28cbce1881ca68f3ed30e0fa6b2a265633e31fdd269 ./manipulate-reliability-table/point_by_point/kgo_point_by_point.nc a162ff6c31dd3f0d84e1633c0cca28083e731876a69d40b7f14c6ff4a3431f25 ./manipulate-reliability-table/point_by_point/reliability_table_point_by_point.nc -6ea87b3fd108055dcba84ff0349f95aea82cfb9665bd7c93cd2c1a065349b2a0 ./max-in-height/input.nc -4e269623426087ee9583fce5819162770ca63fc043a5fb90eb1b1d2934a6d5ca ./max-in-height/kgo_with_bounds.nc -96a6de95ee4750164b11bbae92ebf5da3f1bcc90b96ed4b9db266b27abf10346 ./max-in-height/kgo_without_bounds.nc +0f35c52998ea93be4d8ed08f0ed1164b68bd6a197557afb95ae466fbec81c22e ./max-in-height/input.nc +2856432e02159afc04dadf37a0f8033cba25a9b29dfd73baea10369ccadfb645 ./max-in-height/kgo_with_bounds.nc +823232b882186fd7b7bd6172f5cedcaf5fb980b86a271553d2e4313e5e9b2b4b ./max-in-height/kgo_without_bounds.nc a2de3ea5608d30d4ac2759e9ff4c4a6573e2c0e8e655595682a2ec2691c2de74 ./max-in-time-window/input_PT0029H00M.nc ac81531fa507a2a3a12d4adb542ed014594eff4a38137f947d3a68a2063fab49 ./max-in-time-window/input_PT0032H00M.nc fb02306f960fa36cf9a86921ec6599541e0a0823dfa546856000f810cdd96d73 ./max-in-time-window/kgo.nc From 087b10b872e1a9b95505c2c101d9e38a8c35c355 Mon Sep 17 00:00:00 2001 From: Marcus Spelman Date: Tue, 10 Oct 2023 09:58:56 +0100 Subject: [PATCH 9/9] Raises an error if requested height bounds don't cover any height levels --- improver/utilities/cube_manipulation.py | 10 +++++ .../test_maximum_in_height.py | 45 ++++++++++++++----- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/improver/utilities/cube_manipulation.py b/improver/utilities/cube_manipulation.py index 1ca48a6568..b14d0c928e 100644 --- a/improver/utilities/cube_manipulation.py +++ b/improver/utilities/cube_manipulation.py @@ -732,6 +732,10 @@ def maximum_in_height( A cube of the maximum value over the height coordinate or maximum value between the desired height values. This cube inherits Iris' meta-data updates to the height coordinate and to the cell methods. + + Raises: + ValueError: + If the cube has no height levels between the lower_height_bound and upper_height_bound """ height_levels = cube.coord("height").points @@ -747,6 +751,12 @@ def maximum_in_height( ) cube_subsetted = cube.extract(height_constraint) + if cube_subsetted is None: + raise ValueError( + f"""The provided cube doesn't have any height levels between the provided bounds. + The provided bounds were {lower_height_bound},{upper_height_bound}.""" + ) + if len(cube_subsetted.coord("height").points) > 1: max_cube = cube_subsetted.collapsed("height", iris.analysis.MAX) else: diff --git a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py index 9c72c870bc..2a81ae546a 100644 --- a/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py +++ b/improver_tests/utilities/cube_manipulation/test_maximum_in_height.py @@ -34,11 +34,28 @@ import numpy as np import pytest +from iris.cube import Cube from improver.synthetic_data.set_up_test_cubes import set_up_variable_cube from improver.utilities.cube_manipulation import maximum_in_height +@pytest.fixture() +def wet_bulb_temperature() -> Cube: + "Generate a cube of wet bulb temperature on height levels" + data = np.array( + [ + [[100, 200, 100], [100, 200, 100]], + [[300, 400, 100], [300, 400, 100]], + [[200, 300, 300], [200, 300, 300]], + ] + ) + cube = set_up_variable_cube( + data=data, name="wet_bulb_temperature", height_levels=[100, 200, 300] + ) + return cube + + @pytest.mark.parametrize( "lower_bound,upper_bound,expected", ( @@ -48,23 +65,27 @@ (50, 1000, [300, 400, 300]), ), ) -def test_maximum_in_height(lower_bound, upper_bound, expected): +def test_maximum_in_height(lower_bound, upper_bound, expected, wet_bulb_temperature): """Test that the maximum over the height coordinate is correctly calculated for different combinations of upper and lower bounds.""" - data = np.array( - [ - [[100, 200, 100], [100, 200, 100]], - [[300, 400, 100], [300, 400, 100]], - [[200, 300, 300], [200, 300, 300]], - ] - ) - cube = set_up_variable_cube( - data=data, name="wet_bulb_temperature", height_levels=[100, 200, 300] - ) result = maximum_in_height( - cube, lower_height_bound=lower_bound, upper_height_bound=upper_bound + wet_bulb_temperature, + lower_height_bound=lower_bound, + upper_height_bound=upper_bound, ) assert np.allclose(result.data, [expected] * 2) assert "wet_bulb_temperature" == result.name() + + +def test_height_bounds_error(wet_bulb_temperature): + """Test an error is raised if the input cube doesn't have any height levels + between the height bounds.""" + + with pytest.raises( + ValueError, match="any height levels between the provided bounds" + ): + maximum_in_height( + wet_bulb_temperature, lower_height_bound=50, upper_height_bound=75 + )