diff --git a/improver/categorical/decision_tree.py b/improver/categorical/decision_tree.py index ed8299a71c..47e149124c 100644 --- a/improver/categorical/decision_tree.py +++ b/improver/categorical/decision_tree.py @@ -6,6 +6,7 @@ import copy import operator +import warnings from typing import Dict, List, Optional, Tuple, Union import iris @@ -179,11 +180,13 @@ def prepare_input_cubes( indicated as allowed by the if_diagnostic_missing key. Raises: - ValueError: - Raises a ValueError if multiple matching thresholds are found. IOError: Raises an IOError if any of the required input data is missing. The error includes details of which fields are missing. + + Warnings: + - If multiple thresholds have been found and the closest matching threshold + will be used. """ cubes = as_cubelist(*cubes) @@ -276,14 +279,33 @@ def prepare_input_cubes( f"spp__relative_to_threshold: {condition}\n" ) else: - num_thresholds = len( + threshold_points = ( matched_threshold[0].coord(threshold_name).points ) + num_thresholds = len(threshold_points) + + # If multiple thresholds are found, the closest threshold to the + # desired threshold, is chosen. if num_thresholds > 1: - raise ValueError( + diff = [ + abs(point - threshold) for point in threshold_points + ] + closest_point = threshold_points[diff.index(min(diff))] + + warnings.warn( f"Multiple ({num_thresholds}) matching thresholds found" - f" for name: {diagnostic}, threshold {threshold}" + f" for name: {diagnostic}, threshold {threshold}. " + f"Using closest match: {closest_point}" ) + + closest_point_constraint = iris.Constraint( + coord_values={threshold_name: closest_point} + ) + closest_cube = matched_threshold[0].extract( + closest_point_constraint + ) + used_cubes.append(closest_cube) + else: used_cubes.extend(matched_threshold) diff --git a/improver_tests/categorical/decision_tree/test_ApplyDecisionTree.py b/improver_tests/categorical/decision_tree/test_ApplyDecisionTree.py index 715d72c4e6..cd4c4c5883 100644 --- a/improver_tests/categorical/decision_tree/test_ApplyDecisionTree.py +++ b/improver_tests/categorical/decision_tree/test_ApplyDecisionTree.py @@ -588,28 +588,39 @@ def test_returns_used_cubes(self): for constraint in unexpected: self.assertEqual(len(result.extract(constraint)), 0) - def test_raises_error_matching_threshold(self): - """Test prepare_input_cubes method raises error for matching thresholds in a - diagnostic.""" + def test_selects_closest_threshold_when_multiple_matches(self): + """Test prepare_input_cubes method raises a warning for matching thresholds + in a diagnostic and selects the closest threshold.""" + + # Modify the cube snowfall_rate to add an additional threshold threshold_coord = find_threshold_coordinate(self.cubes[0]) - additional_threshold = threshold_coord.points[0] * ( + original_threshold = threshold_coord.points[0] + additional_threshold = original_threshold * ( 1 + 0.5 * self.plugin.float_tolerance ) threshold_coord.points = np.array( [ - threshold_coord.points[0], + original_threshold, additional_threshold, threshold_coord.points[2], ], dtype=np.float32, ) - msg = ( - r"Multiple \(2\) matching thresholds found for name: " - "probability_of_lwe_snowfall_rate_above_threshold" - ) - - with self.assertRaisesRegex(ValueError, msg): - self.plugin.prepare_input_cubes(self.cubes) + # Check warning is raised + msg = r"Multiple \(2\) matching thresholds found.*Using closest match" + with self.assertWarnsRegex(UserWarning, msg): + result, _ = self.plugin.prepare_input_cubes(self.cubes) + # Check thresholds extracted for snowfall_cubes + # We expect the original_threshold to be selected over the additional_threshold + expected_thresholds = {original_threshold, threshold_coord.points[2]} + snowfall_cubes = [cube for cube in result if "lwe_snowfall_rate" in cube.name()] + self.assertGreater(len(snowfall_cubes), 0) + for cube in snowfall_cubes: + cube_thresholds = cube.coord(find_threshold_coordinate(cube).name()).points + self.assertEqual(len(cube_thresholds), 1) + cube_threshold_value = cube_thresholds[0] + self.assertIn(cube_threshold_value, expected_thresholds) + self.assertNotEqual(cube_threshold_value, additional_threshold) def test_zero_threshold_uses_absolute_tolerance(self): """Test prepare_input_cubes method uses absolute tolerance when the threshold