def check_probability_cube_metadata(self, cube: Cube) -> None: """Checks probability-specific metadata""" if cube.units != "1": self.errors.append( f"Expected units of 1 on probability data, got {cube.units}") try: self.diagnostic = get_diagnostic_cube_name_from_probability_name( cube.name()) except ValueError as cause: # if the probability name is not valid self.errors.append(str(cause)) expected_threshold_name = get_threshold_coord_name_from_probability_name( cube.name()) if not cube.coords(expected_threshold_name): msg = f"Cube does not have expected threshold coord '{expected_threshold_name}'; " try: threshold_name = find_threshold_coordinate(cube).name() except CoordinateNotFoundError: coords = [coord.name() for coord in cube.coords()] msg += ( f"no coord with var_name='threshold' found in all coords: {coords}" ) self.errors.append(msg) else: msg += f"threshold coord has incorrect name '{threshold_name}'" self.errors.append(msg) self.check_threshold_coordinate_properties( cube.name(), cube.coord(threshold_name)) else: threshold_coord = cube.coord(expected_threshold_name) self.check_threshold_coordinate_properties(cube.name(), threshold_coord)
def test_in_vicinity(self): """Test correct name is returned from an "in vicinity" probability. Name "cloud_height" is used in this test to illustrate why suffix cannot be removed with "rstrip".""" diagnostic = "cloud_height" result = get_threshold_coord_name_from_probability_name( f"probability_of_{diagnostic}_in_vicinity_above_threshold") self.assertEqual(result, diagnostic)
def _update_cell_methods( cell_methods: Tuple[CellMethod], probabilistic_name: str, new_diagnostic_name: str, ) -> List[CellMethod]: """ Update any cell methods that include a comment that refers to the diagnostic name to refer instead to the new diagnostic name. Those cell methods that do not include the diagnostic name are passed through unmodified. Args: cell_methods: The cell methods found on the cube that is being used as the metadata template. probabilistic_name: The full name of the metadata template cube. new_diagnostic_name: The new diagnostic name to use in the modified cell methods. Returns: A list of modified cell methods to replace the originals. """ # strip probability and vicinity components to provide the diagnostic name diagnostic_name = get_threshold_coord_name_from_probability_name( probabilistic_name ) new_cell_methods = [] for cell_method in cell_methods: try: (cell_comment,) = cell_method.comments except ValueError: new_cell_methods.append(cell_method) else: if diagnostic_name in cell_comment: new_cell_methods.append( CellMethod( cell_method.method, coords=cell_method.coord_names, intervals=cell_method.intervals, comments=f"of {new_diagnostic_name}", ) ) else: new_cell_methods.append(cell_method) return new_cell_methods
def construct_extract_constraint( self, diagnostic: str, threshold: AuxCoord, coord_named_threshold: bool) -> Constraint: """ Construct an iris constraint. Args: diagnostic: The name of the diagnostic to be extracted from the CubeList. threshold: The thresholds within the given diagnostic cube that is needed, including units. Note these are NOT coords from the original cubes, just constructs to associate units with values. coord_named_threshold: If true, use old naming convention for threshold coordinates (coord.long_name=threshold). Otherwise extract threshold coordinate name from diagnostic name Returns: A constraint """ if coord_named_threshold: threshold_coord_name = "threshold" else: threshold_coord_name = get_threshold_coord_name_from_probability_name( diagnostic) threshold_val = threshold.points.item() if abs(threshold_val) < self.float_abs_tolerance: cell_constraint = lambda cell: np.isclose( cell.point, threshold_val, rtol=0, atol=self.float_abs_tolerance, ) else: cell_constraint = lambda cell: np.isclose( cell.point, threshold_val, rtol=self.float_tolerance, atol=0, ) kw_dict = {"{}".format(threshold_coord_name): cell_constraint} constraint = iris.Constraint(name=diagnostic, **kw_dict) return constraint
def construct_extract_constraint(self, diagnostics, thresholds, coord_named_threshold): """ Construct an iris constraint. Args: diagnostics (str or list of str): The names of the diagnostics to be extracted from the CubeList. thresholds (iris.AuxCoord or list of iris.AuxCoord): All thresholds within the given diagnostic cubes that are needed, including units. Note these are NOT coords from the original cubes, just constructs to associate units with values. coord_named_threshold (bool): If true, use old naming convention for threshold coordinates (coord.long_name=threshold). Otherwise extract threshold coordinate name from diagnostic name Returns: str or list of str: String, or list of strings, encoding iris cube constraints. """ def _constraint_string(diagnostic, threshold_name, threshold_val): """ Return iris constraint as a string for deferred creation of the lambda functions. Args: diagnostic (str): Name of diagnostic threshold_name (str): Name of threshold coordinate on input cubes threshold_val (float): Value of threshold coordinate required Returns: (str) """ if abs(threshold_val) < WeatherSymbols().float_abs_tolerance: cell_constraint_str = ( " -{float_abs_tol} < cell < " "{float_abs_tol}".format( float_abs_tol=WeatherSymbols().float_abs_tolerance)) else: cell_constraint_str = ( "{threshold_val} * {float_min} < cell < " "{threshold_val} * {float_max}".format( threshold_val=threshold_val, float_min=(1.0 - WeatherSymbols().float_tolerance), float_max=(1.0 + WeatherSymbols().float_tolerance), )) constraint_str = ( "iris.Constraint(name='{diagnostic}', {threshold_name}=" "lambda cell: {cell_constraint})".format( diagnostic=diagnostic, threshold_name=threshold_name, cell_constraint=cell_constraint_str, )) return constraint_str # if input is list, loop over and return a list of strings if not isinstance(diagnostics, list): diagnostics = [diagnostics] thresholds = [thresholds] constraints = [] for diagnostic, threshold in zip(diagnostics, thresholds): if coord_named_threshold: threshold_coord_name = "threshold" elif diagnostic in self.threshold_coord_names: threshold_coord_name = self.threshold_coord_names[diagnostic] else: threshold_coord_name = get_threshold_coord_name_from_probability_name( diagnostic) threshold_val = threshold.points.item() constraints.append( _constraint_string(diagnostic, threshold_coord_name, threshold_val)) if len(constraints) > 1: return constraints # otherwise, return a string return constraints[0]
def check_input_cubes(self, cubes): """ Check that the input cubes contain all the diagnostics and thresholds required by the decision tree. Sets self.coord_named_threshold to "True" if threshold-type coordinates have the name "threshold" (as opposed to the standard name of the diagnostic), for backward compatibility. Args: cubes (iris.cube.CubeList): A CubeList containing the input diagnostic cubes. Returns: dict or None: A dictionary of (keyword) nodes names where the diagnostic data is missing and (values) node associated with diagnostic_missing_action. Raises: IOError: Raises an IOError if any of the required input data is missing. The error includes details of which fields are missing. """ optional_node_data_missing = {} missing_data = [] for key, query in self.queries.items(): diagnostics = get_parameter_names( expand_nested_lists(query, "diagnostic_fields")) thresholds = expand_nested_lists(query, "diagnostic_thresholds") conditions = expand_nested_lists(query, "diagnostic_conditions") for diagnostic, threshold, condition in zip( diagnostics, thresholds, conditions): # First we check the diagnostic name and units, performing # a conversion is required and possible. test_condition = iris.Constraint(name=diagnostic) matched_cube = cubes.extract(test_condition) if not matched_cube: if "diagnostic_missing_action" in query: optional_node_data_missing.update( {key: query[query["diagnostic_missing_action"]]}) else: missing_data.append([diagnostic, threshold, condition]) continue cube_threshold_units = find_threshold_coordinate( matched_cube[0]).units threshold.convert_units(cube_threshold_units) # Then we check if the required threshold is present in the # cube, and that the thresholding is relative to it correctly. threshold = threshold.points.item() threshold_name = find_threshold_coordinate( matched_cube[0]).name() # Check cube and threshold coordinate names match according to # expected convention. If not, add to exception dictionary. if (get_threshold_coord_name_from_probability_name(diagnostic) != threshold_name): self.threshold_coord_names[diagnostic] = threshold_name # Set flag to check for old threshold coordinate names if threshold_name == "threshold" and not self.coord_named_threshold: self.coord_named_threshold = True # Check threshold == 0.0 if abs(threshold) < self.float_abs_tolerance: coord_constraint = { threshold_name: lambda cell: (-self.float_abs_tolerance < cell < self. float_abs_tolerance) } else: coord_constraint = { threshold_name: lambda cell: (threshold * (1.0 - self.float_tolerance) < cell < threshold * (1.0 + self.float_tolerance)) } # Checks whether the spp__relative_to_threshold attribute is above # or below a threshold and and compares to the diagnostic_condition. test_condition = iris.Constraint( coord_values=coord_constraint, cube_func=lambda cube: (probability_is_above_or_below(cube) == condition), ) matched_threshold = matched_cube.extract(test_condition) if not matched_threshold: missing_data.append([diagnostic, threshold, condition]) if missing_data: msg = ("Weather Symbols input cubes are missing" " the following required" " input fields:\n") dyn_msg = "name: {}, threshold: {}, " "spp__relative_to_threshold: {}\n" for item in missing_data: msg = msg + dyn_msg.format(*item) raise IOError(msg) if not optional_node_data_missing: optional_node_data_missing = None return optional_node_data_missing
def test_error_not_probability(self): """Test exception if input is not a probability cube name""" with self.assertRaises(ValueError): get_threshold_coord_name_from_probability_name( "lwe_precipitation_rate")
def test_between_thresholds(self): """Test correct name is returned from a probability between thresholds """ result = get_threshold_coord_name_from_probability_name( "probability_of_visibility_in_air_between_thresholds") self.assertEqual(result, "visibility_in_air")
def test_below_threshold(self): """Test correct name is returned from a probability below threshold""" result = get_threshold_coord_name_from_probability_name( "probability_of_air_temperature_below_threshold") self.assertEqual(result, "air_temperature")
def test_above_threshold(self): """Test correct name is returned from a standard (above threshold) probability field""" result = get_threshold_coord_name_from_probability_name( "probability_of_air_temperature_above_threshold") self.assertEqual(result, "air_temperature")