diff --git a/improver/blending/utilities.py b/improver/blending/utilities.py index 2262a97747..fdd1008f12 100644 --- a/improver/blending/utilities.py +++ b/improver/blending/utilities.py @@ -30,10 +30,11 @@ # POSSIBILITY OF SUCH DAMAGE. """Utilities to support weighted blending""" +from datetime import datetime from typing import Dict, List, Optional import numpy as np -from iris.cube import Cube +from iris.cube import Cube, CubeList from numpy import int64 from improver.blending import MODEL_BLEND_COORD, MODEL_NAME_COORD @@ -44,6 +45,7 @@ ) from improver.metadata.constants.time_types import TIME_COORDS from improver.metadata.forecast_times import add_blend_time, forecast_period_coord +from improver.utilities.cube_checker import is_model_blended from improver.utilities.round import round_close from improver.utilities.temporal import cycletime_to_number @@ -225,3 +227,99 @@ def _get_cycletime_point(cube: Cube, cycletime: str) -> int64: cycletime, time_unit=frt_units, calendar=frt_calendar ) return round_close(cycletime_point, dtype=np.int64) + + +def set_record_run_attr( + cubelist: CubeList, record_run_attr: str, model_id_attr: Optional[str] +) -> None: + """Set a record_run attribute that records the model identifier and + forecast reference time of each cube in the cubelist. From the list of cubes, + pre-existing record_run attributes, model IDs and forecast reference + times are extracted as required to build a new record_run attribute. + + The new attribute is applied to each cube in the cubelist in preparation + for blending / combining the cubes. The resulting composite product will + have a record of the contributing models and their associated forecast + reference times. + + There are three ways this method may work: + + - None of the input cubes have been previously cycle or model blended. + The model_id_attr argument must be provided to enable the model + identifiers to be extracted and used in conjunction with the forecast + reference time to build the record_run attribute. + - All of the input cubes have been previously cycle or model blended. The + model_id_attr argument is not required as a new record_run attribute + will be constructed by combining the existing record_run attributes on + each input cube. + - Some of the input cubes have been previously cycle or model blended, and + some have not. The model_id_attr argument must be provided so that those + cubes without an existing record_run attribute can be interrogated for + their model identifier. + + The cubes are modified in place. + + Args: + cubelist: + Cubes from which to obtain model and cycle information, and to which + the resulting run record attribute is added. + record_run_attr: + The name of the record run attribute that is to be created. + model_id_attr: + The name of the attribute that contains the source model information. + + Raises: + ValueError: If model_id_attr is not set and is required to construct a + new record_run_attr. + Exception: A cube has previously been model blended but contains no + record_run_attr. + Exception: The model_id_attr name provided is not present on one or more + of the input cubes. + """ + if not model_id_attr and not all( + [record_run_attr in cube.attributes for cube in cubelist] + ): + raise ValueError( + f"Not all input cubes contain an existing {record_run_attr} attribute. " + "A model_id_attr argument must be provided to enable the construction " + f"of a new {record_run_attr} attribute." + ) + + cycle_strings = [] + for cube in cubelist: + if record_run_attr in cube.attributes: + model_attrs = cube.attributes[record_run_attr].splitlines() + for model_attr in model_attrs: + if model_attr not in cycle_strings: + cycle_strings.append(model_attr) + continue + + if is_model_blended(cube): + raise Exception( + "This cube has been through model blending but there is no " + f"record_run attribute. This indicates cube {cube.name()} has " + "been previously model blended without recording the cycles " + "from which data was taken. It is not possible to create a " + "record_run attribute." + ) + + if model_id_attr not in cube.attributes: + raise Exception( + f"Failure to record run information in '{record_run_attr}' " + "during blend: no model id attribute found in cube. " + f"Cube attributes: {cube.attributes}" + ) + + cycle = datetime.utcfromtimestamp( + cube.coord("forecast_reference_time").points[0] + ) + cycle_str = cycle.strftime("%Y%m%dT%H%MZ") + + blending_weight = "" # TODO: include actual blending weight here. + run_attr = f"{cube.attributes[model_id_attr]}:{cycle_str}:{blending_weight}" + if run_attr not in cycle_strings: + cycle_strings.append(run_attr) + + cycle_strings.sort() + for cube in cubelist: + cube.attributes[record_run_attr] = "\n".join(cycle_strings) diff --git a/improver/blending/weighted_blend.py b/improver/blending/weighted_blend.py index b526d9dae3..7571795bd1 100644 --- a/improver/blending/weighted_blend.py +++ b/improver/blending/weighted_blend.py @@ -32,7 +32,6 @@ whole dimension.""" import warnings -from datetime import datetime from typing import List, Optional, Union import iris @@ -45,7 +44,7 @@ from improver import BasePlugin, PostProcessingPlugin from improver.blending import MODEL_BLEND_COORD, MODEL_NAME_COORD -from improver.blending.utilities import find_blend_dim_coord +from improver.blending.utilities import find_blend_dim_coord, set_record_run_attr from improver.metadata.constants import FLOAT_DTYPE, PERC_COORD from improver.metadata.forecast_times import rebadge_forecasts_as_latest_cycle from improver.utilities.cube_manipulation import ( @@ -157,31 +156,6 @@ def _create_model_coordinates(self, cubelist: Union[List[Cube], CubeList]) -> No cube.add_aux_coord(new_model_id_coord) cube.add_aux_coord(new_model_coord) - def _set_record_run_attr(self, cubelist: CubeList) -> None: - """Set a model-cycle record attribute if configured.""" - cycle_strings = [] - for cube in cubelist: - if self.record_run_attr in cube.attributes: - cycle_strings.extend(cube.attributes[self.record_run_attr].splitlines()) - continue - cycle = datetime.utcfromtimestamp( - cube.coord("forecast_reference_time").points[0] - ) - cycle_str = cycle.strftime("%Y%m%dT%H%MZ") - if self.model_id_attr not in cube.attributes: - raise Exception( - f"Failure to record run information in '{self.record_run_attr}' " - "during blend: no model id attribute found in cube. " - f"Cube attributes: {cube.attributes}" - ) - blending_weight = "" # TODO: include actual blending weight here. - cycle_strings.append( - f"{cube.attributes[self.model_id_attr]}:{cycle_str}:{blending_weight}" - ) - cycle_strings.sort() - for cube in cubelist: - cube.attributes[self.record_run_attr] = "\n".join(cycle_strings) - @staticmethod def _remove_blend_time(cube: Cube) -> Cube: """If present on input, remove existing blend time coordinate (as this will @@ -230,7 +204,7 @@ def process( ) if self.record_run_attr is not None and self.model_id_attr is not None: - self._set_record_run_attr(cubelist) + set_record_run_attr(cubelist, self.record_run_attr, self.model_id_attr) if "model" in self.blend_coord: cubelist = [self._remove_blend_time(cube) for cube in cubelist] diff --git a/improver/cli/wxcode.py b/improver/cli/wxcode.py index 0e57214e19..5a892d4a9d 100755 --- a/improver/cli/wxcode.py +++ b/improver/cli/wxcode.py @@ -40,6 +40,7 @@ def process( *cubes: cli.inputcube, wxtree: cli.inputjson = None, model_id_attr: str = None, + record_run_attr: str = None, target_period: int = None, check_tree: bool = False, ): @@ -55,6 +56,9 @@ def process( Name of attribute recording source models that should be inherited by the output cube. The source models are expected as a space-separated string. + record_run_attr: + Name of attribute used to record models and cycles used in + constructing the weather symbols. target_period: The period in seconds that the weather symbol being produced should represent. This should correspond with any period diagnostics, e.g. @@ -86,5 +90,8 @@ def process( raise RuntimeError("Not enough input arguments. See help for more information.") return WeatherSymbols( - wxtree, model_id_attr=model_id_attr, target_period=target_period + wxtree, + model_id_attr=model_id_attr, + record_run_attr=record_run_attr, + target_period=target_period, )(CubeList(cubes)) diff --git a/improver/cli/wxcode_modal.py b/improver/cli/wxcode_modal.py index 85e67cc1ba..155e350f8f 100644 --- a/improver/cli/wxcode_modal.py +++ b/improver/cli/wxcode_modal.py @@ -36,7 +36,9 @@ @cli.clizefy @cli.with_output -def process(*cubes: cli.inputcube, model_id_attr: str = None): +def process( + *cubes: cli.inputcube, model_id_attr: str = None, record_run_attr: str = None +): """Generates a modal weather symbol for the period covered by the input weather symbol cubes. Where there are different weather codes available for night and day, the modal code returned is always a day code, regardless @@ -50,6 +52,9 @@ def process(*cubes: cli.inputcube, model_id_attr: str = None): Name of attribute recording source models that should be inherited by the output cube. The source models are expected as a space-separated string. + record_run_attr: + Name of attribute used to record models and cycles used in + constructing the weather symbols. Returns: iris.cube.Cube: @@ -60,4 +65,6 @@ def process(*cubes: cli.inputcube, model_id_attr: str = None): if not cubes: raise RuntimeError("Not enough input arguments. See help for more information.") - return ModalWeatherCode(model_id_attr=model_id_attr)(cubes) + return ModalWeatherCode( + model_id_attr=model_id_attr, record_run_attr=record_run_attr, + )(cubes) diff --git a/improver/utilities/cube_checker.py b/improver/utilities/cube_checker.py index 5195d82532..a2355a7ff5 100644 --- a/improver/utilities/cube_checker.py +++ b/improver/utilities/cube_checker.py @@ -199,3 +199,20 @@ def spatial_coords_match(first_cube: Cube, second_cube: Cube) -> bool: return first_cube.coord(axis="x") == second_cube.coord( axis="x" ) and first_cube.coord(axis="y") == second_cube.coord(axis="y") + + +def is_model_blended(cube: Cube) -> bool: + """ + Determine whether a cube has been through model blending by looking for a + "blend_time" coordinate. This doesn't guarantee that multiple models have + contributed to the blend, only that it has been through the model blending + process. + + Args: + cube: + The cube to test. + + Returns: + True if the cube has been through model blending, false if not. + """ + return "blend_time" in [c.name() for c in cube.coords()] diff --git a/improver/wxcode/modal_code.py b/improver/wxcode/modal_code.py index 024352cf20..dac0d13135 100644 --- a/improver/wxcode/modal_code.py +++ b/improver/wxcode/modal_code.py @@ -30,6 +30,8 @@ # POSSIBILITY OF SUCH DAMAGE. """Module containing a plugin to calculate the modal weather code in a period.""" +from typing import Optional + import iris import numpy as np from iris.analysis import Aggregator @@ -38,6 +40,7 @@ from scipy import stats from improver import BasePlugin +from improver.blending.utilities import set_record_run_attr from improver.utilities.cube_manipulation import MergeCubes from ..metadata.forecast_times import forecast_period_coord @@ -73,7 +76,9 @@ class ModalWeatherCode(BasePlugin): covered by the input files. """ - def __init__(self, model_id_attr: str = None): + def __init__( + self, model_id_attr: Optional[str] = None, record_run_attr: Optional[str] = None + ): """ Set up plugin and create an aggregator instance for reuse @@ -82,10 +87,14 @@ def __init__(self, model_id_attr: str = None): Name of attribute recording source models that should be inherited by the output cube. The source models are expected as a space-separated string. + record_run_attr: + Name of attribute used to record models and cycles used in + constructing the weather symbols. """ self.aggregator_instance = Aggregator("mode", self.mode_aggregator) self.model_id_attr = model_id_attr + self.record_run_attr = record_run_attr # Create the expected cell method for use with single cube inputs # that do not pass through the aggregator. @@ -204,6 +213,11 @@ def process(self, cubes: CubeList) -> Cube: A single weather code cube with time bounds that span those of the input weather code cubes. """ + # Set the record_run attribute on all cubes. This will survive the + # merge and be present on the output. + if self.record_run_attr: + set_record_run_attr(cubes, self.record_run_attr, self.model_id_attr) + cube = MergeCubes()(cubes) self._unify_day_and_night(cube) diff --git a/improver/wxcode/weather_symbols.py b/improver/wxcode/weather_symbols.py index 5da55c21b2..c325e3febb 100644 --- a/improver/wxcode/weather_symbols.py +++ b/improver/wxcode/weather_symbols.py @@ -43,6 +43,7 @@ from numpy import ndarray from improver import BasePlugin +from improver.blending.utilities import set_record_run_attr from improver.metadata.amend import update_model_id_attr_attribute from improver.metadata.probabilistic import ( find_threshold_coordinate, @@ -98,6 +99,7 @@ def __init__( self, wxtree: dict, model_id_attr: Optional[str] = None, + record_run_attr: Optional[str] = None, target_period: Optional[int] = None, ) -> None: """ @@ -113,6 +115,9 @@ def __init__( Name of attribute recording source models that should be inherited by the output cube. The source models are expected as a space-separated string. + record_run_attr: + Name of attribute used to record models and cycles used in + constructing the weather symbols. target_period: The period in seconds that the weather symbol being produced should represent. This should correspond with any period diagnostics, e.g. @@ -129,6 +134,7 @@ def __init__( """ self.model_id_attr = model_id_attr + self.record_run_attr = record_run_attr self.start_node = list(wxtree.keys())[0] self.target_period = target_period self.queries = update_tree_thresholds(wxtree, target_period) @@ -544,6 +550,11 @@ def create_symbol_cube(self, cubes: Union[List[Cube], CubeList]) -> Cube: optional_attributes.update( update_model_id_attr_attribute(cubes, self.model_id_attr) ) + if self.record_run_attr: + set_record_run_attr(cubes, self.record_run_attr, self.model_id_attr) + optional_attributes.update( + {self.record_run_attr: cubes[0].attributes[self.record_run_attr]} + ) symbols = create_new_diagnostic_cube( "weather_code", diff --git a/improver_tests/acceptance/SHA256SUMS b/improver_tests/acceptance/SHA256SUMS index 29fe4596a9..a146ef8e6b 100644 --- a/improver_tests/acceptance/SHA256SUMS +++ b/improver_tests/acceptance/SHA256SUMS @@ -633,75 +633,75 @@ e653ae271dd789dde8b03e6237cc7b48552dd9d5206d3430ee68d283683247a0 ./wind_downsca de5cee66ebf03a6dc6fe4350fb8fedd782adc0bb5da72726a4098c3b62422130 ./wind_downscaling/with_realization/kgo.nc e50cf0c7e23b12317412e34ea45f959532beb6ba4247e075be08c2a6732e8cd8 ./wind_downscaling/with_realization/sigma.nc 933e1b15c22d3bf302c57dff3fc92c33d45d056b9b6eaa6d677e870537a42e7c ./wind_downscaling/with_realization/standard_orog.nc -6513b722e1fdca6ad68acd768f3506ec814f78e9ccd4799972ec00a2316ef7cf ./wxcode-modal/blend_mismatch_inputs/20201209T0700Z-weather_symbols-PT01H.nc -d4c5d626ad2e5a805f90f2902f6da5d33651eb6fdcb1e16116e2290d0c608bf6 ./wxcode-modal/blend_mismatch_inputs/20201209T0800Z-weather_symbols-PT01H.nc -4642fc43faf4bdde6a678578868fdebb22163064fe6ef0eef41f2f39a959ca89 ./wxcode-modal/blend_mismatch_inputs/20201209T0900Z-weather_symbols-PT01H.nc -ce6a854694aecd515063c91774eeb6160f39c9dbf5e970f72f960a3527e157e9 ./wxcode-modal/blend_mismatch_inputs/20201209T1000Z-weather_symbols-PT01H.nc -300c38e7f522ff1e9257708b183148f3441bb1a861c15eaff2a5f1713bbbc872 ./wxcode-modal/blend_mismatch_inputs/20201209T1100Z-weather_symbols-PT01H.nc -df077177d1ac4b4faccb945f5e64c0bcf2bf19356dee5acef8caffc5db602823 ./wxcode-modal/blend_mismatch_inputs/20201209T1200Z-weather_symbols-PT01H.nc -5ab6b249ec615ddab9995629aaddbd85bba352ad52b7bcb0be5b3037d75c22ca ./wxcode-modal/blend_mismatch_inputs/20201209T1300Z-weather_symbols-PT01H.nc -340205e12625ff9466c1afba8c007343aa40e21b837ab2f2143fa032e6c47bd3 ./wxcode-modal/blend_mismatch_inputs/20201209T1400Z-weather_symbols-PT01H.nc -776739d288b6e43b3d9341ba301feead160986d2540514f0663f243ca003e55a ./wxcode-modal/blend_mismatch_inputs/20201209T1500Z-weather_symbols-PT01H.nc -4655358c22abdd0acfd5f4e08f156a7f4f629a1764dd8314f28f121964725078 ./wxcode-modal/blend_mismatch_inputs/20201209T1600Z-weather_symbols-PT01H.nc -28ee3f9d419bb9766cc68e1c74fa9e8e5010dc63a600635a54356a33795f6644 ./wxcode-modal/blend_mismatch_inputs/20201209T1700Z-weather_symbols-PT01H.nc -073fd0175e35afcf7f9fe61fedfa63bbd9ea5299f68d8b0287bc30085339b942 ./wxcode-modal/blend_mismatch_inputs/20201209T1800Z-weather_symbols-PT01H.nc -a6b3f48f8c2003c2d84da0a4cb8634bed24f0d54594d6cc9235c470858f49c19 ./wxcode-modal/blend_mismatch_inputs/kgo.nc -70adb1084eb1999513abe77fe43e385c04a5dd967ee57516f5113b6f8aec5454 ./wxcode-modal/gridded_input/20201209T0700Z-weather_symbols-PT01H.nc -03ab503a17fe14c03e3bafc03b44bc994e5a77d7d7ee9649d36c01bba91be813 ./wxcode-modal/gridded_input/20201209T0800Z-weather_symbols-PT01H.nc -2f23984f9e31a79d98e80a3426c551e4ddf32f44ec03cf095d11bf5e2e281113 ./wxcode-modal/gridded_input/20201209T0900Z-weather_symbols-PT01H.nc -4c21a13a71682482c8b5aac5babbe442ade17f8485c3d5e8d573f510d4a94080 ./wxcode-modal/gridded_input/20201209T1000Z-weather_symbols-PT01H.nc -6361353d199cb0c3d7c5e1d601e1b82377bcd7deba37f8f00e5910cc87649ab1 ./wxcode-modal/gridded_input/20201209T1100Z-weather_symbols-PT01H.nc -c649787aef87184392252660d9efeb1fd3c9933c308d02041a03a882ce1fbb59 ./wxcode-modal/gridded_input/20201209T1200Z-weather_symbols-PT01H.nc -c7cf5b8141df473b9854008aa25a9bdbdb162f840d6a829a46fbfccafa57d749 ./wxcode-modal/gridded_input/20201209T1300Z-weather_symbols-PT01H.nc -bc1022649e1b0ace39d9da1f3fbca03be855a6ec785e4487b584139e92d7fb59 ./wxcode-modal/gridded_input/20201209T1400Z-weather_symbols-PT01H.nc -724a98e9d311546af88b68796b4ba40968183b06ffd4688a0a24e1b6299864b1 ./wxcode-modal/gridded_input/20201209T1500Z-weather_symbols-PT01H.nc -fd4ee760bee0077bc4abf0e4be0a8703c43e10097fa8de08b1abf9a3ea4a780b ./wxcode-modal/gridded_input/20201209T1600Z-weather_symbols-PT01H.nc -5d2fa6f476337c2c3783b77d52615652f269174d3b8e804e7c6f931c3e7df4dd ./wxcode-modal/gridded_input/20201209T1700Z-weather_symbols-PT01H.nc -8d150f86a12e4b914e7f2ee269e643022db664d26c2539927d83f4424706cb3e ./wxcode-modal/gridded_input/20201209T1800Z-weather_symbols-PT01H.nc -a37d508c6c356325b1c31c245726aa5459484c4fb34fb2fbf309b10265d24712 ./wxcode-modal/gridded_input/kgo.nc -94a8758ca265acf625a21f4f9c6a246181b7b51e35710f312675d22b394f4d54 ./wxcode-modal/gridded_ties/20201209T0700Z-weather_symbols-PT01H.nc -0dbf6702561e52bcd97a86ef3d954e353a5218b78a1376792144eeb0a673a0c8 ./wxcode-modal/gridded_ties/20201209T0800Z-weather_symbols-PT01H.nc -08881255e7b71679aa1eda6e7bec8b85ceba5adc3547ae169e192abfc3238c39 ./wxcode-modal/gridded_ties/20201209T0900Z-weather_symbols-PT01H.nc -fc4a6da438ac558080c11413de21cd9943f356795d6f145f8e43e9ca290d1412 ./wxcode-modal/gridded_ties/20201209T1000Z-weather_symbols-PT01H.nc -ed07ba61392f465d5890a06c6ad3cee9d9adf31ab03ed365e1d9771d005d646c ./wxcode-modal/gridded_ties/20201209T1100Z-weather_symbols-PT01H.nc -350f0754f7bb247a0a81bb81d05f3e69da4d8d768d8269c82f6fef73713b5b06 ./wxcode-modal/gridded_ties/20201209T1200Z-weather_symbols-PT01H.nc -e8dc643ec6446d495a96d4dda6104b958d08137c15c3e832bb397a50229062eb ./wxcode-modal/gridded_ties/20201209T1300Z-weather_symbols-PT01H.nc -77375be1e8258f0b641ea6475c7739beec64252dc66ae4216a5cc02a3f01cd6e ./wxcode-modal/gridded_ties/20201209T1400Z-weather_symbols-PT01H.nc -148cdbc9282a8fb44e4793144852757ee36c5cd63d6efebfcb63337186e96a2f ./wxcode-modal/gridded_ties/20201209T1500Z-weather_symbols-PT01H.nc -44c510792f566329597239006b43ea736b5faa1a884202fd523aba31ebc4f445 ./wxcode-modal/gridded_ties/20201209T1600Z-weather_symbols-PT01H.nc -182211dba9b8d18c2d48dce89f2871854ae9baa9457bc9cfad16c1cea80faf09 ./wxcode-modal/gridded_ties/20201209T1700Z-weather_symbols-PT01H.nc -2ed494728e73e8f740680ae090504770ee6b60131d543698c640db5ed4e08598 ./wxcode-modal/gridded_ties/20201209T1800Z-weather_symbols-PT01H.nc -fab89f125fe7b016a35fcbf52cd9b713cfa28c7009b90150f289f47d7bea6b12 ./wxcode-modal/gridded_ties/kgo.nc -9fbcfc0e0c3f6af0d07690c6be12e2f641123168af0c7e165b8e0ec6a2ca6f98 ./wxcode-modal/single_input/20201210T0000Z-weather_symbols-PT01H.nc -4e52a42de445064a987da6f93b7e888c0a8ca974063a4d9cf9818a57d9b60367 ./wxcode-modal/single_input/kgo.nc -6513b722e1fdca6ad68acd768f3506ec814f78e9ccd4799972ec00a2316ef7cf ./wxcode-modal/spot_input/20201209T0700Z-weather_symbols-PT01H.nc -d4c5d626ad2e5a805f90f2902f6da5d33651eb6fdcb1e16116e2290d0c608bf6 ./wxcode-modal/spot_input/20201209T0800Z-weather_symbols-PT01H.nc -4642fc43faf4bdde6a678578868fdebb22163064fe6ef0eef41f2f39a959ca89 ./wxcode-modal/spot_input/20201209T0900Z-weather_symbols-PT01H.nc -ce6a854694aecd515063c91774eeb6160f39c9dbf5e970f72f960a3527e157e9 ./wxcode-modal/spot_input/20201209T1000Z-weather_symbols-PT01H.nc -300c38e7f522ff1e9257708b183148f3441bb1a861c15eaff2a5f1713bbbc872 ./wxcode-modal/spot_input/20201209T1100Z-weather_symbols-PT01H.nc -48ef16ad1fd0b499f1d38f585ddb0a4299c39e7d7df625ad0fa6c3dc24e0f094 ./wxcode-modal/spot_input/20201209T1200Z-weather_symbols-PT01H.nc -5ab6b249ec615ddab9995629aaddbd85bba352ad52b7bcb0be5b3037d75c22ca ./wxcode-modal/spot_input/20201209T1300Z-weather_symbols-PT01H.nc -340205e12625ff9466c1afba8c007343aa40e21b837ab2f2143fa032e6c47bd3 ./wxcode-modal/spot_input/20201209T1400Z-weather_symbols-PT01H.nc -776739d288b6e43b3d9341ba301feead160986d2540514f0663f243ca003e55a ./wxcode-modal/spot_input/20201209T1500Z-weather_symbols-PT01H.nc -4655358c22abdd0acfd5f4e08f156a7f4f629a1764dd8314f28f121964725078 ./wxcode-modal/spot_input/20201209T1600Z-weather_symbols-PT01H.nc -28ee3f9d419bb9766cc68e1c74fa9e8e5010dc63a600635a54356a33795f6644 ./wxcode-modal/spot_input/20201209T1700Z-weather_symbols-PT01H.nc -073fd0175e35afcf7f9fe61fedfa63bbd9ea5299f68d8b0287bc30085339b942 ./wxcode-modal/spot_input/20201209T1800Z-weather_symbols-PT01H.nc -a6b3f48f8c2003c2d84da0a4cb8634bed24f0d54594d6cc9235c470858f49c19 ./wxcode-modal/spot_input/kgo.nc -db1612786b08a929ad096f13ca603f37cfd78de681e1306d909654f345f9f409 ./wxcode-modal/spot_ties/20201209T0700Z-weather_symbols-PT01H.nc -b5beedd2fa6c58ecaaa51358b8b7abadddc1ad072af2d5141bbf0019b458e84e ./wxcode-modal/spot_ties/20201209T0800Z-weather_symbols-PT01H.nc -c92e6cb002b8ff8f185c097a55ff214793c7595667c8a7ba4afc1ae0f456c13e ./wxcode-modal/spot_ties/20201209T0900Z-weather_symbols-PT01H.nc -fd216b5dc3837202f3293f22e664d4fa623f7a349abda9ee8aef3fb19df3f660 ./wxcode-modal/spot_ties/20201209T1000Z-weather_symbols-PT01H.nc -fbff18f7ef5c2e3d462cbc0d78de30aca7534a37f427472011f6fa1835ec8020 ./wxcode-modal/spot_ties/20201209T1100Z-weather_symbols-PT01H.nc -129beabb8e059e46d3f00be1c5a5b8c1cdd44c4f1f08fc86a4249d4a0f854272 ./wxcode-modal/spot_ties/20201209T1200Z-weather_symbols-PT01H.nc -063e6f151af8a05f4c2276995384fc4058571144a851953752653eff7886130c ./wxcode-modal/spot_ties/20201209T1300Z-weather_symbols-PT01H.nc -4d23d6d1f8861feeb288ec7b68e57e893fa2ffd633de1b25ab4d4bc4aa3316cc ./wxcode-modal/spot_ties/20201209T1400Z-weather_symbols-PT01H.nc -71aac305fce32fb389ea63822cd88511137cf1260c1c0a55e28c1a5fcbc91cab ./wxcode-modal/spot_ties/20201209T1500Z-weather_symbols-PT01H.nc -6fe4b1563f0fbb4579dba46b12654f983cd6486f39986d52231aad5d18acf508 ./wxcode-modal/spot_ties/20201209T1600Z-weather_symbols-PT01H.nc -d506afe1572ded010805662f057926ba19367a8bf0c9f700380b6b63ab440399 ./wxcode-modal/spot_ties/20201209T1700Z-weather_symbols-PT01H.nc -1c0c3b4fcc95ea2d5c9658ebd42cbee8a2043afa607843e0e9013d273f7fc460 ./wxcode-modal/spot_ties/20201209T1800Z-weather_symbols-PT01H.nc -cb558fc0cb3d1f0535e2402fc293982a122a08380df7d5594cacb9cf032acc5a ./wxcode-modal/spot_ties/kgo.nc +f4e13dec400ec945ba5bd03a286780b183665c22d707c038c0990ef3491e888e ./wxcode-modal/blend_mismatch_inputs/20201209T0700Z-weather_symbols-PT01H.nc +ee9557cf229b1099e64b36760a1b2dd82aec663490d75b6be2894bfb7a9103ea ./wxcode-modal/blend_mismatch_inputs/20201209T0800Z-weather_symbols-PT01H.nc +601d331f490953f3ac36ac334b705848b8d4bc9024bfe001f4cd2d8d8b6645a2 ./wxcode-modal/blend_mismatch_inputs/20201209T0900Z-weather_symbols-PT01H.nc +83e19da3fac7e1bdf308b30f2fc3bdec8ec17c3b6f06f1b50d3aff506548d734 ./wxcode-modal/blend_mismatch_inputs/20201209T1000Z-weather_symbols-PT01H.nc +e6468dfeac80f3115d970ee12244b5cd61840c6b8f33e31222d7bcaaf3f7bb76 ./wxcode-modal/blend_mismatch_inputs/20201209T1100Z-weather_symbols-PT01H.nc +b23f50c394a4dc8db391029f83668aaa929250a1246ffcf841940e894b400975 ./wxcode-modal/blend_mismatch_inputs/20201209T1200Z-weather_symbols-PT01H.nc +c2e91aab8af73e749f24d3abf7c26b7a074d33b8063fc9d2372554b263f94bcb ./wxcode-modal/blend_mismatch_inputs/20201209T1300Z-weather_symbols-PT01H.nc +a3c7b63aadfbee89f49c854acb33050e54c6b829b4ca99d12eafa38fdaf74160 ./wxcode-modal/blend_mismatch_inputs/20201209T1400Z-weather_symbols-PT01H.nc +c44a00d49912f13fb17256fa0e01d9425977b5c94f2d41c602c657d39deb74cc ./wxcode-modal/blend_mismatch_inputs/20201209T1500Z-weather_symbols-PT01H.nc +90a6495f65e053e943dd4dcc9731da274f11bc19a3817a70c057218ed8eaa4de ./wxcode-modal/blend_mismatch_inputs/20201209T1600Z-weather_symbols-PT01H.nc +a981f6238828b63d7a9f7c597dd234dcc86dd35e2e3c4ba6c3b6feb491ec872e ./wxcode-modal/blend_mismatch_inputs/20201209T1700Z-weather_symbols-PT01H.nc +de4a49bca4cd930328942cf6bb9a1bc1bdd2589120a57a2335ee4ef2449dbd5d ./wxcode-modal/blend_mismatch_inputs/20201209T1800Z-weather_symbols-PT01H.nc +8006f04fe0922062ed8323174947c45212aa0cb3d34478de043ad51b79dcdeb3 ./wxcode-modal/blend_mismatch_inputs/kgo.nc +deb7f4effb821b2808b647e02ac955c91adae4baa33765b16378cff40e3ec5e8 ./wxcode-modal/gridded_input/20201209T0700Z-weather_symbols-PT01H.nc +a61a70b0ce9e70577ba177462b9f1bfbda2457cc3975f0e9a562e1311e86e671 ./wxcode-modal/gridded_input/20201209T0800Z-weather_symbols-PT01H.nc +64fc223da6c516a1eef11a61119547fb33e899ce86eb570a2917be27a119b517 ./wxcode-modal/gridded_input/20201209T0900Z-weather_symbols-PT01H.nc +e9536caed6f465a52c670f8aa52211a6530ce710123ee997c91750ffc26a94d9 ./wxcode-modal/gridded_input/20201209T1000Z-weather_symbols-PT01H.nc +a11a46a14f082a6765e1200dba264d7c4ad87b1727b0263654d86cebd13e68c6 ./wxcode-modal/gridded_input/20201209T1100Z-weather_symbols-PT01H.nc +e54675a125f5b15c59e805a725bc3d8ac0bdace56400cbb17809e2a5bf25af14 ./wxcode-modal/gridded_input/20201209T1200Z-weather_symbols-PT01H.nc +bcd90ab1d28fd736d4a3d9e481374348438d7716b543f9c7d435b003ba10c344 ./wxcode-modal/gridded_input/20201209T1300Z-weather_symbols-PT01H.nc +4e58e1d45fcf506d9faa0d633690956fe9f4346c4725d42f830a5108131e064a ./wxcode-modal/gridded_input/20201209T1400Z-weather_symbols-PT01H.nc +47e46ed6955d9109aa3f714b282a8e8cf0d71fb9859d1bf300315f599b6a6527 ./wxcode-modal/gridded_input/20201209T1500Z-weather_symbols-PT01H.nc +973c60900aa526818e7119ed016997170055017ee1bbda279b9e640750f96f61 ./wxcode-modal/gridded_input/20201209T1600Z-weather_symbols-PT01H.nc +571bff58be29197e5f946745ed565889ec81499521c38d8f7286488079afb46d ./wxcode-modal/gridded_input/20201209T1700Z-weather_symbols-PT01H.nc +2af4455b0ba7c4124e49eb1ff004e770b6239a9e2e1513f60ba4db3f0beb02cf ./wxcode-modal/gridded_input/20201209T1800Z-weather_symbols-PT01H.nc +b58878e1fee4815302bd9aeede8f1027f66d6833c76f1c45bd7757f743cd772d ./wxcode-modal/gridded_input/kgo.nc +96a8462af571f06dbd8b91a7a90aaef403eefd2b73929a5c6d8a3fbb01159aca ./wxcode-modal/gridded_ties/20201209T0700Z-weather_symbols-PT01H.nc +9f64c7a8aa7cf0e87799f96ebffe1e449e1f5174fb583d44f2479e085672dc84 ./wxcode-modal/gridded_ties/20201209T0800Z-weather_symbols-PT01H.nc +c698b9599219fe89374a2565e55a374d9236904c9dd99a2ae61b5416506e98d3 ./wxcode-modal/gridded_ties/20201209T0900Z-weather_symbols-PT01H.nc +773013d8fffea59a7eca0e41c4457d9ff1148ec7062727b9be1ef02ad1a48d7e ./wxcode-modal/gridded_ties/20201209T1000Z-weather_symbols-PT01H.nc +c76e1e0e04b1d6cc01a33401f1eed25c283e4f11d7134ff079a8379f0ac3a8cd ./wxcode-modal/gridded_ties/20201209T1100Z-weather_symbols-PT01H.nc +59a7bf4aa98cc4a827553a0e3d3c77291886c73a4a2dc603b0de120b462f9caf ./wxcode-modal/gridded_ties/20201209T1200Z-weather_symbols-PT01H.nc +10bb1634e2e895066a6b07ce2d1ccb1a10b435c812dcabb6d64aaeaf55bf64d0 ./wxcode-modal/gridded_ties/20201209T1300Z-weather_symbols-PT01H.nc +947a5a3ede12e2223e22283d679f7a4945cda171d9cbc0b21fdc6fd1e0c76400 ./wxcode-modal/gridded_ties/20201209T1400Z-weather_symbols-PT01H.nc +609ec87425a1ed7237382d3873d4252882b67390520f9ca99ff9a8473d2ec3f8 ./wxcode-modal/gridded_ties/20201209T1500Z-weather_symbols-PT01H.nc +c39ea98f6fe64788c4ea7ea242111a5c8bbeacfaf52b2ead0cf0aed0007d46ab ./wxcode-modal/gridded_ties/20201209T1600Z-weather_symbols-PT01H.nc +6a5b04644ab11d077809f615bac2829656127a0eeee3843940f8d33673bd70c8 ./wxcode-modal/gridded_ties/20201209T1700Z-weather_symbols-PT01H.nc +39d0fa291798366a00ecae79a65de0b0692d5b4db17ac98a97d48e54b75e5dd4 ./wxcode-modal/gridded_ties/20201209T1800Z-weather_symbols-PT01H.nc +537e94efc461fd16fe97c58defb661b86989e37d189fdf5c91ee1402750b3a50 ./wxcode-modal/gridded_ties/kgo.nc +d28a3a25f99ea8666c4cbc088db265eac8bdc376730375dedaaadd4588e927fa ./wxcode-modal/single_input/20201210T0000Z-weather_symbols-PT01H.nc +91ee5a1b67337fd81297740fce177efd296d585aece95bd00d10a3d43f6478bc ./wxcode-modal/single_input/kgo.nc +f4e13dec400ec945ba5bd03a286780b183665c22d707c038c0990ef3491e888e ./wxcode-modal/spot_input/20201209T0700Z-weather_symbols-PT01H.nc +ee9557cf229b1099e64b36760a1b2dd82aec663490d75b6be2894bfb7a9103ea ./wxcode-modal/spot_input/20201209T0800Z-weather_symbols-PT01H.nc +601d331f490953f3ac36ac334b705848b8d4bc9024bfe001f4cd2d8d8b6645a2 ./wxcode-modal/spot_input/20201209T0900Z-weather_symbols-PT01H.nc +83e19da3fac7e1bdf308b30f2fc3bdec8ec17c3b6f06f1b50d3aff506548d734 ./wxcode-modal/spot_input/20201209T1000Z-weather_symbols-PT01H.nc +e6468dfeac80f3115d970ee12244b5cd61840c6b8f33e31222d7bcaaf3f7bb76 ./wxcode-modal/spot_input/20201209T1100Z-weather_symbols-PT01H.nc +d7efe86dc4f9eb3a7b63c01b9f4caf8ab9eed2bb66fcc86258fcd91f33b9cae8 ./wxcode-modal/spot_input/20201209T1200Z-weather_symbols-PT01H.nc +c2e91aab8af73e749f24d3abf7c26b7a074d33b8063fc9d2372554b263f94bcb ./wxcode-modal/spot_input/20201209T1300Z-weather_symbols-PT01H.nc +a3c7b63aadfbee89f49c854acb33050e54c6b829b4ca99d12eafa38fdaf74160 ./wxcode-modal/spot_input/20201209T1400Z-weather_symbols-PT01H.nc +c44a00d49912f13fb17256fa0e01d9425977b5c94f2d41c602c657d39deb74cc ./wxcode-modal/spot_input/20201209T1500Z-weather_symbols-PT01H.nc +90a6495f65e053e943dd4dcc9731da274f11bc19a3817a70c057218ed8eaa4de ./wxcode-modal/spot_input/20201209T1600Z-weather_symbols-PT01H.nc +a981f6238828b63d7a9f7c597dd234dcc86dd35e2e3c4ba6c3b6feb491ec872e ./wxcode-modal/spot_input/20201209T1700Z-weather_symbols-PT01H.nc +de4a49bca4cd930328942cf6bb9a1bc1bdd2589120a57a2335ee4ef2449dbd5d ./wxcode-modal/spot_input/20201209T1800Z-weather_symbols-PT01H.nc +b7f3b879a08375b4c208b82c8e53ae22f7274328120577192662fe59be2c8854 ./wxcode-modal/spot_input/kgo.nc +36f26203008ac401e361f549e39c5c1a0334d31eef2e064528d2c11ba029d1d2 ./wxcode-modal/spot_ties/20201209T0700Z-weather_symbols-PT01H.nc +8543d8168e23975f537767a55a8f6fbd7d15f187556748ab62e4edc3f70a84d3 ./wxcode-modal/spot_ties/20201209T0800Z-weather_symbols-PT01H.nc +cb1a6c410f37132f0faa541a68411e00c38bf77c719f98adcff664f6699d4bf5 ./wxcode-modal/spot_ties/20201209T0900Z-weather_symbols-PT01H.nc +ab2f1980da14cedff1c51428db912052cb09bf5752ca70ea440d5325b9f1d3e2 ./wxcode-modal/spot_ties/20201209T1000Z-weather_symbols-PT01H.nc +b5e442c0728c2fa91e50fd8340fff5a5c2106e24c9bfbe914c870e252e41b12b ./wxcode-modal/spot_ties/20201209T1100Z-weather_symbols-PT01H.nc +94cb0443d6573e13fc106116ffa0a420be5b6ac4da2ea8d7804a5cdf7d55c09c ./wxcode-modal/spot_ties/20201209T1200Z-weather_symbols-PT01H.nc +21b1cb478f0e9d3f62c52235029c18797362d5fb3c99cd83a616d98a19ceb6ac ./wxcode-modal/spot_ties/20201209T1300Z-weather_symbols-PT01H.nc +e95c359b27524b23305ddeb9f0e829911e8a83c2d028ef613e4547889d57c189 ./wxcode-modal/spot_ties/20201209T1400Z-weather_symbols-PT01H.nc +ea67ae7a7f5363ae3692b29c93fb9989449d96e49dc613a1297d98ddb12c578a ./wxcode-modal/spot_ties/20201209T1500Z-weather_symbols-PT01H.nc +947d1cc7278abeb7f1335c9504c0b1daac61d8ee36fd6be03eccbc17c10b0e4b ./wxcode-modal/spot_ties/20201209T1600Z-weather_symbols-PT01H.nc +51f636314a6d1fa894ab98cf750493503b191a779c67d6a15081aab2a3612a31 ./wxcode-modal/spot_ties/20201209T1700Z-weather_symbols-PT01H.nc +134fb1750cf47e868ee67801b1ea5b1f17120e6f58a787a69e54638d7d88ff82 ./wxcode-modal/spot_ties/20201209T1800Z-weather_symbols-PT01H.nc +5e7a4bace8b0325133f9a8526b541b7af636777dab0c8888160aae74b4b4b630 ./wxcode-modal/spot_ties/kgo.nc 803b2a645f8a33db4601a45dc16325f8a90f13fdea430b98ea94f032ee032180 ./wxcode/bad_wx_decision_tree.json -5a3a808f0cc7952e6cc4225d09136276f685c5fdd6798a795c0430aa65ca71fa ./wxcode/basic/kgo.nc +091dd0cfd5f860afd624e67eecc3a8e9db10f40a45d696fc4192b8a3a469dcc4 ./wxcode/basic/kgo.nc 3b86d135373b44989c22bdfbc6cb0798e974ff1d77c4c6b6a83fce650863e59c ./wxcode/basic/kgo_no_lightning.nc 5c1950ac1e5f96a5eb1495369271b667bcc89b2deced21d3a51c70303ef2b4d8 ./wxcode/basic/probability_of_low_and_medium_type_cloud_area_fraction_above_threshold.nc 146d0f003cf6d9b41f87761cb18f52958a41050af716718568cdb2d09d33fdd5 ./wxcode/basic/probability_of_low_type_cloud_area_fraction_above_threshold.nc diff --git a/improver_tests/acceptance/test_wxcode.py b/improver_tests/acceptance/test_wxcode.py index 86236d879b..6370ca7662 100644 --- a/improver_tests/acceptance/test_wxcode.py +++ b/improver_tests/acceptance/test_wxcode.py @@ -71,6 +71,8 @@ def test_basic(tmp_path): wxtree, "--model-id-attr", "mosg__model_configuration", + "--record-run-attr", + "mosg__model_run", "--target-period", "3600", "--output", @@ -100,6 +102,8 @@ def test_native_units(tmp_path): wxtree, "--model-id-attr", "mosg__model_configuration", + "--record-run-attr", + "mosg__model_run", "--target-period", "3600", "--output", diff --git a/improver_tests/acceptance/test_wxcode_modal.py b/improver_tests/acceptance/test_wxcode_modal.py index 21320df4ac..6dcef4e26b 100644 --- a/improver_tests/acceptance/test_wxcode_modal.py +++ b/improver_tests/acceptance/test_wxcode_modal.py @@ -70,6 +70,8 @@ def test_expected(tmp_path, test_path): *input_paths, "--model-id-attr", "mosg__model_configuration", + "--record-run-attr", + "mosg__model_run", "--output", output_path, ] diff --git a/improver_tests/blending/test_utilities.py b/improver_tests/blending/test_utilities.py index a5c67cce35..100e8b3ae5 100644 --- a/improver_tests/blending/test_utilities.py +++ b/improver_tests/blending/test_utilities.py @@ -40,6 +40,7 @@ from improver.blending.utilities import ( find_blend_dim_coord, get_coords_to_remove, + set_record_run_attr, update_blended_metadata, ) from improver.metadata.constants.attributes import MANDATORY_ATTRIBUTE_DEFAULTS @@ -148,3 +149,134 @@ def test_update_blended_metadata(model_cube): # check frt has been updated via fp proxy - input had 3 hours lead time, # output has 2 hours lead time relative to current cycle time assert collapsed_cube.coord("forecast_period").points[0] == 2 * 3600 + + +@pytest.mark.parametrize( + "indices, expected", + ( + ([0, 1], "uk_det:20171110T0100Z:\nuk_ens:20171110T0100Z:"), + ([0, 0], "uk_det:20171110T0100Z:"), + ), +) +def test_set_record_run_attr_basic(model_cube, indices, expected): + """Test use case where the record_run_attr is constructed from other + information on the cubes. There are two tests here: + + - cubes with unique attributes that are combined + - cubes with identical attributes attributes that are combined such that + only a single entry is returned.""" + + record_run_attr = "mosg__model_run" + model_id_attr = "mosg__model_configuration" + cubes = [model_cube[index] for index in indices] + for cube in cubes: + cube.attributes = {model_id_attr: cube.coord("model_configuration").points[0]} + + set_record_run_attr(cubes, record_run_attr, model_id_attr) + + for cube in cubes: + assert cube.attributes[record_run_attr] == expected + + +@pytest.mark.parametrize( + "existing, expected", + ( + ( + ["uk_det:20171110T0100Z:", "uk_ens:20171110T0100Z:"], + "uk_det:20171110T0100Z:\nuk_ens:20171110T0100Z:", + ), + ( + [ + "uk_det:20171110T0100Z:\nuk_ens:20171110T0100Z:", + "uk_ens:20171110T0100Z:", + ], + "uk_det:20171110T0100Z:\nuk_ens:20171110T0100Z:", + ), + ( + ["uk_det:20171110T0100Z:", "uk_det:20171110T0100Z:"], + "uk_det:20171110T0100Z:", + ), + ), +) +def test_set_record_run_attr_existing_attribute(model_cube, existing, expected): + """Test the case in which the cubes already have record_run_attr entries + and these must be combined to create a new shared attribute. There are three + tests here: + + - cubes with unique model_run attributes that are combined + - cubes with distinct but overlapping model_run attributes from which the + elements are combined without duplicates + - cubes with identical model_run attributes that are combined such that + only a single entry is returned. + + The test cubes are only vehicles for the attributes in this test, such that + the attributes imposed do not necessarily match the other cube metadata.""" + + record_run_attr = "mosg__model_run" + model_id_attr = "mosg__model_configuration" + cubes = [model_cube[0], model_cube[1]] + for run_attr, cube in zip(existing, cubes): + cube.attributes.update({record_run_attr: run_attr}) + + set_record_run_attr(cubes, record_run_attr, model_id_attr) + + for cube in cubes: + assert cube.attributes[record_run_attr] == expected + + +def test_set_record_run_attr_mixed_inputs(model_cube): + """Test use case where the record_run_attr is constructed from one cube + with an existing record_run_attr, and one where other information on the + cube is used.""" + + record_run_attr = "mosg__model_run" + model_id_attr = "mosg__model_configuration" + cubes = [model_cube[0], model_cube[1]] + cubes[0].attributes.update({record_run_attr: "uk_det:20171110T0000Z:"}) + cubes[1].attributes = { + model_id_attr: cubes[1].coord("model_configuration").points[0] + } + expected = "uk_det:20171110T0000Z:\nuk_ens:20171110T0100Z:" + + set_record_run_attr(cubes, record_run_attr, model_id_attr) + + for cube in cubes: + assert cube.attributes[record_run_attr] == expected + + +def test_set_record_run_attr_exception_model_id_unset(model_cube): + """Test an exception is raised if the model_id_attr argument provided is + none and the input cubes do not all have an existing record_run_attr.""" + record_run_attr = "mosg__model_run" + model_id_attr = None + cubes = [model_cube[0], model_cube[1]] + + with pytest.raises(Exception, match="Not all input cubes contain an existing"): + set_record_run_attr(cubes, record_run_attr, model_id_attr) + + +def test_set_record_run_attr_exception_model_id(model_cube): + """Test an exception is raised if no model_id_attr is set on the input + cubes and no existing record_run_attr was present.""" + record_run_attr = "mosg__model_run" + model_id_attr = "mosg__model_configuration" + cubes = [model_cube[0], model_cube[1]] + + with pytest.raises(Exception, match="Failure to record run information"): + set_record_run_attr(cubes, record_run_attr, model_id_attr) + + +def test_set_record_run_attr_exception_blended(model_cube): + """Test an exception is raised if an input cube has been through model + blending but has no record_run attribute. It is not possible to create a + record_run attribute in this case as the source cycle information has been + lost on this input cube.""" + record_run_attr = "mosg__model_run" + model_id_attr = "mosg__model_configuration" + cubes = [model_cube[0], model_cube[1]] + blend_time = cubes[0].coord("forecast_reference_time").copy() + blend_time.rename("blend_time") + cubes[0].add_aux_coord(blend_time) + + with pytest.raises(Exception, match="This cube has been through model blending"): + set_record_run_attr(cubes, record_run_attr, model_id_attr) diff --git a/improver_tests/utilities/test_cube_checker.py b/improver_tests/utilities/test_cube_checker.py index 580112de1e..02c66c6801 100644 --- a/improver_tests/utilities/test_cube_checker.py +++ b/improver_tests/utilities/test_cube_checker.py @@ -46,6 +46,7 @@ check_cube_coordinates, check_for_x_and_y_axes, find_dimension_coordinate_mismatch, + is_model_blended, spatial_coords_match, ) @@ -301,5 +302,29 @@ def test_unmatching_y(self): self.assertFalse(result) +class Test_is_model_blended(unittest.TestCase): + + """Test is_model_blended behaves as expected.""" + + def setUp(self): + """Set up a cube.""" + data = np.ones((1, 5, 5), dtype=np.float32) + cube = set_up_variable_cube( + data, "precipitation_amount", "kg m^-2", "equalarea", + ) + self.cube_without = cube + blend_time = cube.coord("forecast_reference_time").copy() + blend_time.rename("blend_time") + self.cube_with = cube.copy() + self.cube_with.add_aux_coord(blend_time) + + def test_basic(self): + """Test correct result is returned for cube with and without a + blend_time coordinate.""" + + self.assertFalse(is_model_blended(self.cube_without)) + self.assertTrue(is_model_blended(self.cube_with)) + + if __name__ == "__main__": unittest.main() diff --git a/improver_tests/wxcode/wxcode/test_ModalCode.py b/improver_tests/wxcode/wxcode/test_ModalCode.py index 17b5230c73..729a6ba950 100644 --- a/improver_tests/wxcode/wxcode/test_ModalCode.py +++ b/improver_tests/wxcode/wxcode/test_ModalCode.py @@ -46,12 +46,18 @@ from . import set_up_wxcube MODEL_ID_ATTR = "mosg__model_configuration" +RECORD_RUN_ATTR = "mosg__model_run" TARGET_TIME = dt(2020, 6, 15, 18) +TIME_FORMAT = "%Y%m%dT%H%MZ" @pytest.fixture(name="wxcode_series") def wxcode_series_fixture( - data, cube_type, offset_reference_times: bool, model_id_attr: bool, + data, + cube_type, + offset_reference_times: bool, + model_id_attr: bool, + record_run_attr: bool, ) -> Tuple[bool, CubeList]: """Generate a time series of weather code cubes for combination to create a period representative code. When offset_reference_times is set, each @@ -97,12 +103,37 @@ def wxcode_series_fixture( scalar_coords=time_coords, ) ) + + # Add a blendtime coordinate as UK weather symbols are constructed + # from model blended data. + blend_time = wxcubes[-1].coord("forecast_reference_time").copy() + blend_time.rename("blend_time") + wxcubes[-1].add_aux_coord(blend_time) + if model_id_attr: - [c.attributes.update({MODEL_ID_ATTR: "uk_ens"}) for c in wxcubes] - wxcubes[0].attributes.update({MODEL_ID_ATTR: "uk_det uk_ens"}) - return model_id_attr, wxcubes + if i == 0: + wxcubes[-1].attributes.update({MODEL_ID_ATTR: "uk_det uk_ens"}) + else: + wxcubes[-1].attributes.update({MODEL_ID_ATTR: "uk_ens"}) + + if record_run_attr: + ukv_time = wxfrt - timedelta(hours=1) + enukx_time = wxfrt - timedelta(hours=3) + if i == 0: + wxcubes[-1].attributes.update( + { + RECORD_RUN_ATTR: f"uk_det:{ukv_time:{TIME_FORMAT}}:\nuk_ens:{enukx_time:{TIME_FORMAT}}:" # noqa: E501 + } + ) + else: + wxcubes[-1].attributes.update( + {RECORD_RUN_ATTR: f"uk_ens:{enukx_time:{TIME_FORMAT}}:"} + ) + return model_id_attr, record_run_attr, offset_reference_times, wxcubes + +@pytest.mark.parametrize("record_run_attr", [False]) @pytest.mark.parametrize("model_id_attr", [False, True]) @pytest.mark.parametrize("offset_reference_times", [False, True]) @pytest.mark.parametrize("cube_type", ["gridded", "spot"]) @@ -141,11 +172,12 @@ def wxcode_series_fixture( ) def test_expected_values(wxcode_series, expected): """Test that the expected period representative symbol is returned.""" - _, wxcode_cubes = wxcode_series + _, _, _, wxcode_cubes = wxcode_series result = ModalWeatherCode()(wxcode_cubes) assert result.data.flatten()[0] == expected +@pytest.mark.parametrize("record_run_attr", [False, True]) @pytest.mark.parametrize("model_id_attr", [False, True]) @pytest.mark.parametrize("offset_reference_times", [False, True]) @pytest.mark.parametrize("cube_type", ["gridded", "spot"]) @@ -164,12 +196,13 @@ def test_metadata(wxcode_series): def as_utc_timestamp(time): return timegm(time.utctimetuple()) - model_id_attr, wxcode_cubes = wxcode_series + model_id_attr, record_run_attr, offset_reference_times, wxcode_cubes = wxcode_series + kwargs = {} if model_id_attr: - kwargs = {"model_id_attr": MODEL_ID_ATTR} - else: - kwargs = {} + kwargs.update({"model_id_attr": MODEL_ID_ATTR}) + if record_run_attr: + kwargs.update({"record_run_attr": RECORD_RUN_ATTR}) result = ModalWeatherCode(**kwargs)(wxcode_cubes) @@ -183,6 +216,17 @@ def as_utc_timestamp(time): expected_forecast_period, ] expected_cell_method = ["mode", "time"] + expected_model_id_attr = "uk_det uk_ens" + expected_record_det = "uk_det:20200614T2300Z:\n" + expected_record_ens = "uk_ens:20200614T{}00Z:" + + # Expected record_run attribute contains all contributing cycle times. + if offset_reference_times and len(wxcode_cubes) > 1: + expected_record_run_attr = expected_record_det + "\n".join( + [expected_record_ens.format(value) for value in range(10, 22)] + ) + else: + expected_record_run_attr = expected_record_det + expected_record_ens.format(21) assert result.coord("time").points[0] == as_utc_timestamp(expected_time) assert result.coord("time").bounds[0][0] == as_utc_timestamp(expected_bounds[0]) @@ -198,6 +242,10 @@ def as_utc_timestamp(time): assert result.cell_methods[0].method == expected_cell_method[0] assert result.cell_methods[0].coord_names[0] == expected_cell_method[1] if model_id_attr: - assert result.attributes[MODEL_ID_ATTR] == "uk_det uk_ens" + assert result.attributes[MODEL_ID_ATTR] == expected_model_id_attr else: assert MODEL_ID_ATTR not in result.attributes.keys() + if record_run_attr: + assert result.attributes[RECORD_RUN_ATTR] == expected_record_run_attr + else: + assert RECORD_RUN_ATTR not in result.attributes.keys() diff --git a/improver_tests/wxcode/wxcode/test_WeatherSymbols.py b/improver_tests/wxcode/wxcode/test_WeatherSymbols.py index 262377f37b..b940fbdca0 100644 --- a/improver_tests/wxcode/wxcode/test_WeatherSymbols.py +++ b/improver_tests/wxcode/wxcode/test_WeatherSymbols.py @@ -1286,24 +1286,28 @@ def setUp(self): data, np.array([288, 290, 292], dtype=np.float32) ) self.cube.attributes["mosg__model_configuration"] = "uk_det uk_ens" + self.cube.attributes[ + "mosg__model_run" + ] = "uk_det:20171109T2300Z:\nuk_ens:20171109T2100Z:" self.wxcode = np.array(list(WX_DICT.keys())) self.wxmeaning = " ".join(WX_DICT.values()) self.plugin = WeatherSymbols(wxtree=wxcode_decision_tree()) def test_basic(self): - """Test cube is constructed with appropriate metadata without - model_id_attr attribute""" + """Test cube is constructed with appropriate metadata without setting + the model_id_attr or record_run attributes""" self.plugin.template_cube = self.cube result = self.plugin.create_symbol_cube([self.cube]) self.assertIsInstance(result, iris.cube.Cube) self.assertArrayEqual(result.attributes["weather_code"], self.wxcode) self.assertEqual(result.attributes["weather_code_meaning"], self.wxmeaning) self.assertNotIn("mosg__model_configuration", result.attributes) + self.assertNotIn("mosg__model_run", result.attributes) self.assertTrue((result.data.mask).all()) def test_model_id_attr(self): - """Test cube is constructed with appropriate metadata with - model_id_attr attribute""" + """Test cube is constructed with appropriate metadata with just the + model_id_attr attribute set""" self.plugin.template_cube = self.cube self.plugin.model_id_attr = "mosg__model_configuration" result = self.plugin.create_symbol_cube([self.cube]) @@ -1313,6 +1317,26 @@ def test_model_id_attr(self): self.assertArrayEqual( result.attributes["mosg__model_configuration"], "uk_det uk_ens" ) + self.assertNotIn("mosg__model_run", result.attributes) + self.assertTrue((result.data.mask).all()) + + def test_record_run_attr(self): + """Test cube is constructed with appropriate metadata when setting both + the model_id_attr and record_run attributes""" + self.plugin.template_cube = self.cube + self.plugin.model_id_attr = "mosg__model_configuration" + self.plugin.record_run_attr = "mosg__model_run" + result = self.plugin.create_symbol_cube([self.cube]) + self.assertIsInstance(result, iris.cube.Cube) + self.assertArrayEqual(result.attributes["weather_code"], self.wxcode) + self.assertEqual(result.attributes["weather_code_meaning"], self.wxmeaning) + self.assertArrayEqual( + result.attributes["mosg__model_run"], + "uk_det:20171109T2300Z:\nuk_ens:20171109T2100Z:", + ) + self.assertArrayEqual( + result.attributes["mosg__model_configuration"], "uk_det uk_ens" + ) self.assertTrue((result.data.mask).all()) def test_bounds_preserved_if_present(self):