diff --git a/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf b/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf index 77483988b..9bc23b577 100644 --- a/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf +++ b/src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf @@ -1823,7 +1823,7 @@ title= Number of moist absolutely unstable layers (MAULs) in a column description=PROCESS-BASED DIAGNOSTIC. Determines the number of moist absolutely unstable layers (MAUL) present in a grid column regardless of depth. - Requires "air_temperature", "air_pressure", and "specific_humidity" on model levels. + Requires "air_temperature", "air_pressure", "specific_humidity", "x_wind" and "y_wind" on model levels. help=This diagnostic identifies the numbers of MAUL that are present within the column. It does not provide any information about the properties of the MAUL. A MAUL is defined as an unstable layer (equivalent potential temperature gradient @@ -1840,7 +1840,7 @@ title= Depth of moist absolutely unstable layer (MAUL) in a column description=PROCESS-BASED DIAGNOSTIC. Determines the depth of the deepest moist absolutely unstable layer (MAUL) in a grid column. - Requires "air_temperature", "air_pressure", and "specific_humidity" on model levels. + Requires "air_temperature", "air_pressure", "specific_humidity", "x_wind" and "y_wind" on model levels. help=This diagnostic identifies only the deepest MAUL within the column. A MAUL is defined as an unstable layer (equivalent potential temperature gradient with height less than zero) that has a relatively humidity above '90%' following @@ -1856,7 +1856,7 @@ title= Base (AGL) of moist absolutely unstable layer (MAUL) in a column description=PROCESS-BASED DIAGNOSTIC. Determines the base height (AGL) of the deepest moist absolutely unstable layer (MAUL) in a grid column. - Requires "air_temperature", "air_pressure", and "specific_humidity" on model levels. + Requires "air_temperature", "air_pressure", "specific_humidity", "x_wind" and "y_wind" on model levels. help=This diagnostic identifies the base of the deepest MAUL within the column. A MAUL is defined as an unstable layer (equivalent potential temperature gradient with height less than zero) that has a relatively humidity above '90%' following @@ -1866,6 +1866,20 @@ type=python_boolean compulsory=true sort-key=xp-maulp3 +[tepmplate variable=AVERAGE_WIND_BELOW_MAUL] +ns = Diagnostics/Derived/XPPN +title= Average windspeed below the deepest moist absolutely unstable layer (MAUL) in a column +description=PROCESS-BASED DIAGNOSTIC. + Determines the average windspeed below the deepest moist absolutely unstable layer (MAUL) + in a grid column. + Requires "air_temperature", "air_pressure", "specific_humidity", "x_wind" and "y_wind" on model levels. +help=This diagnostic identifies the base of the deepest MAUL within the column and + averages the windspeed below that base. A MAUL is defined as an unstable layer + (equivalent potential temperature gradient with height less than zero) that has + a relatively humidity above '90%' following Takemi and Unuma (2020). +type=python_boolean +compulsory=true +sort-key=xp-maulp4 ################################### # Ensembles [Diagnostics/Ensembles] diff --git a/src/CSET/cset_workflow/rose-suite.conf.example b/src/CSET/cset_workflow/rose-suite.conf.example index 5a8612e3f..6defcfbf3 100644 --- a/src/CSET/cset_workflow/rose-suite.conf.example +++ b/src/CSET/cset_workflow/rose-suite.conf.example @@ -6,6 +6,7 @@ ANALYSIS_LENGTH="" !!AOA_CYCLIC=False AOA_DIAG=False !!AOA_PLEV=[] +AVERAGE_WIND_BELOW_MAUL=False AVIATION_COLOUR_STATE=False AVIATION_COLOUR_STATE_CLOUD_BASE=False AVIATION_COLOUR_STATE_VISIBILITY=False diff --git a/src/CSET/loaders/spatial_field.py b/src/CSET/loaders/spatial_field.py index 1517c6816..bfb413971 100644 --- a/src/CSET/loaders/spatial_field.py +++ b/src/CSET/loaders/spatial_field.py @@ -651,6 +651,22 @@ def load(conf: Config): aggregation=False, ) + # Wind Below Moist Absolutely Unstable Layer (of deepest) + if conf.AVERAGE_WIND_BELOW_MAUL: + for model in models: + yield RawRecipe( + recipe="average_windspeed_below_maul_spatial_plot.yaml", + variables={ + "MODEL_NAME": model["name"], + "SUBAREA_TYPE": conf.SUBAREA_TYPE if conf.SELECT_SUBAREA else None, + "SUBAREA_EXTENT": conf.SUBAREA_EXTENT + if conf.SELECT_SUBAREA + else None, + }, + model_ids=model["id"], + aggregation=False, + ) + # Screen-level temperature probabilities for model, condition, threshold in itertools.product( models, diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index a7e23b513..f6c37ce5a 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -22,11 +22,14 @@ from skimage.measure import label from CSET._common import iter_maybe +from CSET.operators.wind import calculate_vector_wind def MAUL_properties( cubes: iris.cube.Cube | iris.cube.CubeList, - output: Literal["number", "base", "depth"], + u_cubes: iris.cube.Cube | iris.cube.CubeList, + v_cubes: iris.cube.Cube | iris.cube.CubeList, + output: Literal["number", "base", "depth", "wind_below"], ) -> iris.cube.Cube | iris.cube.CubeList: """Identify properties of Moist Absolutely Unstable Layers. @@ -35,11 +38,16 @@ def MAUL_properties( cubes: iris.cube.Cube | iris.cube.CubeList A cube or cubelist of a mask(s) as to whether a MAUL exists. This input must be a binary field. - output: Literal["number", "base", "depth"] + u_cubes: iris.cube.Cube | iris.cube.CubeList + A cube or cubelist of the wind in the u direction. + v_cubes: iris.cube.Cube | iris.cube.CubeList + A cube or cubelist of the wind in the v direction. + output: Literal["number", "base", "depth", "wind_below"] The output is the desired property required. It can be number, base, depth for the number of MAULs, base height - of the deepest MAUL, or the depth of the deepest MAUL, - respectively. + of the deepest MAUL, the depth of the deepest MAUL, or the + average windspeed below the MAUL, respectively. + Returns ------- @@ -50,7 +58,7 @@ def MAUL_properties( ------ ValueError: Data contains values that are not 0 or 1, only masked data should be used. This error is raised when a mask field is not provided to the operator. - ValueError: Unexpected value for output. Expected number, depth or base. Got {output}. + ValueError: Unexpected value for output. Expected number, base, depth or wind_below. Got {output}. This error is raised when the wrong output string is specified. Notes @@ -60,9 +68,10 @@ def MAUL_properties( set out in a recipe. The operator applies image processing to the mask to each point in the latitude/longitude coordinates. It uses the image processing to identify continuous layers (1s), and labels them. - It identifies the number of layesr by identifying the maximum label number, - and then finds the top and base of each layer. Depending on the output - desired it will output information for the deepest MAUL. + It identifies the number of layers by identifying the maximum label number, + and then finds the top and base of each layer. It will also find the average + windspeed below the MAUL for indications of presence of low-level jets. + Depending on the output desired it will output information for the deepest MAUL. When a MAUL is not present the output will be set to NaN for depth and base. If number of MAULs is the desired output it will be set to zero. @@ -72,13 +81,16 @@ def MAUL_properties( num_MAULs = iris.cube.CubeList([]) maul_d = iris.cube.CubeList([]) maul_b = iris.cube.CubeList([]) + windspeed_below_MAUL = iris.cube.CubeList([]) - if output not in ("number", "base", "depth"): + if output not in ("number", "base", "depth", "wind_below"): raise ValueError( - f"""Unexpected value for output. Expected number, depth or base. Got {output}.""" + f"""Unexpected value for output. Expected number, base, depth or wind_below. Got {output}.""" ) - for cube in iter_maybe(cubes): + for cube, u, v in zip( + iter_maybe(cubes), iter_maybe(u_cubes), iter_maybe(v_cubes), strict=True + ): # Check for binary fields. if not np.array_equal(cube.data, cube.data.astype(bool)): raise ValueError( @@ -90,6 +102,12 @@ def MAUL_properties( number_of_MAULs.data[:] = 0.0 maul_depth = number_of_MAULs.copy() maul_base = number_of_MAULs.copy() + wind_below_maul = number_of_MAULs.copy() + # Calculate windspeed and direction. + windspeed_and_direction = calculate_vector_wind(u, v) + # Select windspeed, hard coded as always in same position from output + # of calculate_vector_wind. + windspeed = windspeed_and_direction[0] # Loop over realization. for mem_number, member in enumerate(cube.slices_over("realization")): # Loop over time. @@ -129,7 +147,7 @@ def MAUL_properties( ) else: number_of_MAULs.data[lat_point, lon_point] = np.max(labels) - if output != "number": + if output not in ("number", "wind_below"): # Find the base, top, and depth for each object # using cube metadata. maul_start = [] @@ -247,6 +265,135 @@ def MAUL_properties( else: maul_depth.data[lat_point, lon_point] = np.nan maul_base.data[lat_point, lon_point] = np.nan + # Separate loop for calculating wind properties. + elif output not in ("number"): + # Find the base, top, and depth for each object + # using cube metadata. + maul_start = [] + maul_end = [] + maul_dep = [] + # Loop over the number of MAULs (plus one to ensure + # the case for only one MAUL being present). + for maul in range(1, np.max(labels) + 1): + # Find all vertical indices belonging to a MAUL. + maul_range = np.where(labels == maul) + # Find the height at the base of the MAUL + # (lowest level). + maul_start_point = lon.coord("level_height").points[ + maul_range[0][0] + ] + # Find the height at the top of the MAUL + # (highest level). + maul_end_point = lon.coord("level_height").points[ + maul_range[0][-1] + ] + # Calculate the MAUL depth, and store + # base and top heights. + maul_dep.append(maul_end_point - maul_start_point) + maul_start.append(maul_start_point) + maul_end.append(maul_end_point) + try: + # Idendtify where the deepest MAUL is. + index = int( + np.where(maul_dep == np.max(maul_dep))[0][0] + ) + maul_base_value = maul_start[index] + height_index = np.abs( + lon.coord("level_height").points - maul_base_value + ).argmin() + # As with number the code checks for whether + # there are multiple realization and/or time + # points for correct indexing of the output data + # and applies accordingly. + if ( + len(number_of_MAULs.coord("realization").points) + != 1 + and len(number_of_MAULs.coord("time").points) != 1 + ): + # Store and calculate the windspeed below the + # deepest MAUL. + wind_below_maul.data[ + mem_number, time_point, lat_point, lon_point + ] = np.mean( + windspeed[ + mem_number, + time_point, + 0:height_index, + lat_point, + lon_point, + ].data + ) + elif ( + len(number_of_MAULs.coord("realization").points) + != 1 + and len(number_of_MAULs.coord("time").points) == 1 + ): + wind_below_maul.data[ + mem_number, lat_point, lon_point + ] = np.mean( + windspeed[ + mem_number, + 0:height_index, + lat_point, + lon_point, + ].data + ) + elif ( + len(number_of_MAULs.coord("time").points) != 1 + and len(number_of_MAULs.coord("realization").points) + == 1 + ): + wind_below_maul.data[ + time_point, lat_point, lon_point + ] = np.mean( + windspeed[ + time_point, + 0:height_index, + lat_point, + lon_point, + ].data + ) + else: + wind_below_maul.data[lat_point, lon_point] = ( + np.mean( + windspeed[ + 0:height_index, lat_point, lon_point + ].data + ) + ) + # Here a ValueError is raised if a MAUL is not found, or an + # IndexError if the MAUL starts at the surface and so there + # is no wind below the MAUL however these are a valid answers, + # and so output data is set to NaN. + # The dimensionality logic for output data is identical + # to that used previously. + except (ValueError, IndexError): + if ( + len(number_of_MAULs.coord("realization").points) + != 1 + and len(number_of_MAULs.coord("time").points) != 1 + ): + wind_below_maul.data[ + mem_number, time_point, lat_point, lon_point + ] = np.nan + elif ( + len(number_of_MAULs.coord("realization").points) + != 1 + and len(number_of_MAULs.coord("time").points) == 1 + ): + wind_below_maul.data[ + mem_number, lat_point, lon_point + ] = np.nan + elif ( + len(number_of_MAULs.coord("time").points) != 1 + and len(number_of_MAULs.coord("realization").points) + == 1 + ): + wind_below_maul.data[ + time_point, lat_point, lon_point + ] = np.nan + else: + wind_below_maul.data[lat_point, lon_point] = np.nan # Units and renaming for number, depth and base (the other case). match output: @@ -258,10 +405,14 @@ def MAUL_properties( maul_depth.units = "m" maul_depth.rename("MAUL_depth") maul_d.append(maul_depth) - case _: + case "base": maul_base.units = "m" maul_base.rename("MAUL_base_height") maul_b.append(maul_base) + case _: + wind_below_maul.units = "m s^-1" + wind_below_maul.rename("windspeed_below_MAUL") + windspeed_below_MAUL.append(wind_below_maul) # Output data. match output: @@ -275,5 +426,9 @@ def MAUL_properties( return maul_d case "base" if len(maul_b) == 1: return maul_b[0] - case _: + case "base": return maul_b + case "wind_below" if len(windspeed_below_MAUL) == 1: + return windspeed_below_MAUL[0] + case _: + return windspeed_below_MAUL diff --git a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/average_windspeed_below_maul_spatial_plot.yaml b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/average_windspeed_below_maul_spatial_plot.yaml new file mode 100644 index 000000000..d1ac7d3ce --- /dev/null +++ b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/average_windspeed_below_maul_spatial_plot.yaml @@ -0,0 +1,162 @@ +category: Extreme precipitation +title: $MODEL_NAME average windspeed below Moist Absolutely Unstable Layers (MAULs) spatial plot +description: | + Generates a mask of whether Moist Absolutely Unstable Layers (MAULs) are present + in a model column and then averages the windspeed below the deepest MAULs in a + column and creates a spatial plot. + + A MAUL is defined as an unstable layer (with height) based on equivalent potential + temperature alongside the relative humidity of that layer exceeding 90 %, following + [Takemi and Unuma (2020)](https://doi.org/10.2151/sola.2020-006). The equivalent + potential temperature is calculated using air temperature, pressure and specific humidity. + A vertical differentitation occurs to determine the stability (< 0 with dz implies unstable air). + The relative humidity is linearly interpolated onto the same grid as the vertical + derivative of the equivalent potenital temperature. The masks for each criterion are multiplied + together and then image processing is used to identify the number of objects (values of + ones that are connected) the depth of each object is found the deepest MAUL is used. The + windspeed is then averaged from the base of the MAUL to the surface. Should there be + no MAUL present, or the MAUL begins at the surface the value will be set to NaN. + +steps: + - operator: read.read_cubes + file_paths: $INPUT_PATHS + model_names: $MODEL_NAME + constraints: ["air_temperature", "air_pressure", "specific_humidity","x_wind","y_wind"] + subarea_type: $SUBAREA_TYPE + subarea_extent: $SUBAREA_EXTENT + + - operator: precipitation.MAUL_properties + cubes: + operator: misc.multiplication + multiplicand: + operator: filters.generate_mask + mask_field: + operator: misc.differentiate + cubes: + operator: temperature.equivalent_potential_temperature + temperature: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.generate_var_constraint + varname: "air_temperature" + relative_humidity: + operator: humidity.relative_humidity_from_specific_humidity + specific_humidity: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "specific_humidity" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: 0 + temperature: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.generate_var_constraint + varname: "air_temperature" + pressure: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "m01s00i408" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: 0 + pressure: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "m01s00i408" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: 0 + coordinate: "level_height" + condition: 'lt' + value: 0.0 + multiplier: + operator: filters.generate_mask + mask_field: + operator: regrid.vertical_interpolation + cubes: + operator: humidity.relative_humidity_from_specific_humidity + specific_humidity: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "specific_humidity" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: 0 + temperature: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.generate_var_constraint + varname: "air_temperature" + pressure: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "m01s00i408" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: 0 + coordinate: "model_level_number" + target: + operator: misc.differentiate + cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "m01s00i408" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: 0 + coordinate: "level_height" + condition: 'gt' + value: 90.0 + u_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "x_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" + v_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "y_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" + output: "wind_below" + + - operator: plot.spatial_pcolormesh_plot + + - operator: write.write_cube_to_nc + overwrite: True diff --git a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/base_of_moist_absolutely_unstable_layers_spatial_plot.yaml b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/base_of_moist_absolutely_unstable_layers_spatial_plot.yaml index 5acdbb52b..a536d7434 100644 --- a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/base_of_moist_absolutely_unstable_layers_spatial_plot.yaml +++ b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/base_of_moist_absolutely_unstable_layers_spatial_plot.yaml @@ -19,7 +19,7 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME - constraints: ["air_temperature", "air_pressure", "specific_humidity"] + constraints: ["air_temperature", "air_pressure", "specific_humidity", "x_wind", "y_wind"] subarea_type: $SUBAREA_TYPE subarea_extent: $SUBAREA_EXTENT @@ -130,6 +130,28 @@ steps: coordinate: "level_height" condition: 'gt' value: 90.0 + u_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "x_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" + v_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "y_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" output: "base" - operator: plot.spatial_pcolormesh_plot diff --git a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/depth_of_moist_absolutely_unstable_layers_spatial_plot.yaml b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/depth_of_moist_absolutely_unstable_layers_spatial_plot.yaml index 5b65c4b58..c010dbb2b 100644 --- a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/depth_of_moist_absolutely_unstable_layers_spatial_plot.yaml +++ b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/depth_of_moist_absolutely_unstable_layers_spatial_plot.yaml @@ -19,7 +19,7 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME - constraints: ["air_temperature", "air_pressure", "specific_humidity"] + constraints: ["air_temperature", "air_pressure", "specific_humidity", "x_wind", "y_wind"] subarea_type: $SUBAREA_TYPE subarea_extent: $SUBAREA_EXTENT @@ -130,6 +130,28 @@ steps: coordinate: "level_height" condition: 'gt' value: 90.0 + u_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "x_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" + v_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "y_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" output: "depth" - operator: plot.spatial_pcolormesh_plot diff --git a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/number_of_moist_absolutely_unstable_layers_present_in_a_spatial_column.yaml b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/number_of_moist_absolutely_unstable_layers_present_in_a_spatial_column.yaml index c672efe0c..552482ae5 100644 --- a/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/number_of_moist_absolutely_unstable_layers_present_in_a_spatial_column.yaml +++ b/src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/number_of_moist_absolutely_unstable_layers_present_in_a_spatial_column.yaml @@ -21,7 +21,7 @@ steps: - operator: read.read_cubes file_paths: $INPUT_PATHS model_names: $MODEL_NAME - constraints: ["air_temperature", "air_pressure", "specific_humidity"] + constraints: ["air_temperature", "air_pressure", "specific_humidity","x_wind","y_wind"] subarea_type: $SUBAREA_TYPE subarea_extent: $SUBAREA_EXTENT @@ -132,6 +132,28 @@ steps: coordinate: "level_height" condition: 'gt' value: 90.0 + u_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "x_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" + v_cubes: + operator: filters.filter_multiple_cubes + constraint: + operator: constraints.combine_constraints + constraint_1: + operator: constraints.generate_var_constraint + varname: "y_wind" + constraint_2: + operator: constraints.generate_remove_single_level_constraint + coord: "model_level_number" + level: "*" output: "number" - operator: plot.spatial_pcolormesh_plot diff --git a/tests/conftest.py b/tests/conftest.py index 4daba7627..5b3cfb107 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -585,3 +585,157 @@ def precalc_maul_depth_5d_read_only(): def precalc_maul_depth_5d(precalc_maul_depth_5d_read_only): """Get precalculated depth for 5D data. It is safe to modify.""" return precalc_maul_depth_5d_read_only.copy() + + +@pytest.fixture() +def u_wind_maul_all_read_only(): + """Get u wind for 5D data. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/u_wind_all.nc") + + +@pytest.fixture() +def u_wind_maul_all(u_wind_maul_all_read_only): + """Get u wind for 5D data. It is safe to modify.""" + return u_wind_maul_all_read_only.copy() + + +@pytest.fixture() +def u_wind_maul_time_read_only(): + """Get u wind for 4D data varying in time. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/u_wind_time.nc") + + +@pytest.fixture() +def u_wind_maul_time(u_wind_maul_time_read_only): + """Get v wind for 4D data varying in time. It is safe to modify.""" + return u_wind_maul_time_read_only.copy() + + +@pytest.fixture() +def u_wind_maul_member_read_only(): + """Get u wind for 4D data varying in realization. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/u_wind_member.nc") + + +@pytest.fixture() +def u_wind_maul_member(u_wind_maul_member_read_only): + """Get u wind for 4D data varying in realization. It is safe to modify.""" + return u_wind_maul_member_read_only.copy() + + +@pytest.fixture() +def u_wind_maul_read_only(): + """Get u wind for 3D data. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/u_wind_basic.nc") + + +@pytest.fixture() +def u_wind_maul(u_wind_maul_read_only): + """Get u wind for 3D data. It is safe to modify.""" + return u_wind_maul_read_only.copy() + + +@pytest.fixture() +def v_wind_maul_all_read_only(): + """Get v wind for 5D data. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/v_wind_all.nc") + + +@pytest.fixture() +def v_wind_maul_all(v_wind_maul_all_read_only): + """Get pv wind for 5D data. It is safe to modify.""" + return v_wind_maul_all_read_only.copy() + + +@pytest.fixture() +def v_wind_maul_time_read_only(): + """Get v wind for 4D data varying in time. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/u_wind_time.nc") + + +@pytest.fixture() +def v_wind_maul_time(v_wind_maul_time_read_only): + """Get v wind for 4D data varying in time. It is safe to modify.""" + return v_wind_maul_time_read_only.copy() + + +@pytest.fixture() +def v_wind_maul_member_read_only(): + """Get u wind for 4D data varying in realization. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/v_wind_member.nc") + + +@pytest.fixture() +def v_wind_maul_member(v_wind_maul_member_read_only): + """Get v wind for 4D data varying in realization. It is safe to modify.""" + return v_wind_maul_member_read_only.copy() + + +@pytest.fixture() +def v_wind_maul_read_only(): + """Get u wind for 3D data. It is NOT safe to modify.""" + return read.read_cube("tests/test_data/precipitation/v_wind_basic.nc") + + +@pytest.fixture() +def v_wind_maul(v_wind_maul_read_only): + """Get v wind for 3D data. It is safe to modify.""" + return v_wind_maul_read_only.copy() + + +@pytest.fixture() +def precalc_wind_below_maul_read_only(): + """Get precalculated wind below maul 3D data. It is NOT safe to modify.""" + return read.read_cube( + "tests/test_data/precipitation/precalculated_wind_below_maul_3d.nc" + ) + + +@pytest.fixture() +def precalc_wind_below_maul(precalc_wind_below_maul_read_only): + """Get precalculated wind below maul for 3D data. It is safe to modify.""" + return precalc_wind_below_maul_read_only.copy() + + +@pytest.fixture() +def precalc_wind_below_maul_4d_time_read_only(): + """Get precalculated wind below maul 4D data varying time. It is NOT safe to modify.""" + return read.read_cube( + "tests/test_data/precipitation/precalculated_wind_below_maul_4d_time.nc" + ) + + +@pytest.fixture() +def precalc_wind_below_maul_4d_time(precalc_wind_below_maul_4d_time_read_only): + """Get precalculated wind below maul for 4D data varying time. It is safe to modify.""" + return precalc_wind_below_maul_4d_time_read_only.copy() + + +@pytest.fixture() +def precalc_wind_below_maul_4d_realization_read_only(): + """Get precalculated wind below maul 4D data varying realization. It is NOT safe to modify.""" + return read.read_cube( + "tests/test_data/precipitation/precalculated_wind_below_maul_4d_realization.nc" + ) + + +@pytest.fixture() +def precalc_wind_below_maul_4d_realization( + precalc_wind_below_maul_4d_realization_read_only, +): + """Get precalculated wind below maul for 4D data varying realization. It is safe to modify.""" + return precalc_wind_below_maul_4d_realization_read_only.copy() + + +@pytest.fixture() +def precalc_wind_below_maul_5d_read_only(): + """Get precalculated wind below maul 5D data. It is NOT safe to modify.""" + return read.read_cube( + "tests/test_data/precipitation/precalculated_wind_below_maul_5d.nc" + ) + + +@pytest.fixture() +def precalc_wind_below_maul_5d(precalc_wind_below_maul_5d_read_only): + """Get precalculated wind below maul for 5D data. It is safe to modify.""" + return precalc_wind_below_maul_5d_read_only.copy() diff --git a/tests/operators/test_precipitation.py b/tests/operators/test_precipitation.py index b76d04a09..1684c0550 100644 --- a/tests/operators/test_precipitation.py +++ b/tests/operators/test_precipitation.py @@ -22,57 +22,71 @@ from CSET.operators import precipitation -def test_maul_properties_wrong_output(maul_mask): +def test_maul_properties_wrong_output(maul_mask, u_wind_maul, v_wind_maul): """Ensure fails if get unexpected output argument.""" with pytest.raises( ValueError, - match="Unexpected value for output. Expected number, depth or base. Got top.", + match="Unexpected value for output. Expected number, base, depth or wind_below. Got top.", ): - precipitation.MAUL_properties(maul_mask, output="top") + precipitation.MAUL_properties(maul_mask, u_wind_maul, v_wind_maul, output="top") -def test_maul_properties_not_binary_input(maul_mask): +def test_maul_properties_not_binary_input(maul_mask, u_wind_maul, v_wind_maul): """Ensure fails if get non-binary input.""" maul_mask.data += 1.0 with pytest.raises( ValueError, match="Data contains values that are not 0 or 1, only masked data should be used.", ): - precipitation.MAUL_properties(maul_mask, output="number") + precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="number" + ) -def test_maul_properties_3D_number(maul_mask, precalc_maul_number_3d): +def test_maul_properties_3D_number( + maul_mask, u_wind_maul, v_wind_maul, precalc_maul_number_3d +): """Ensure correct number of MAULs generated for 3D field.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask, output="number").data, + precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="number" + ).data, precalc_maul_number_3d.data, rtol=1e-2, atol=1e-6, ) -def test_maul_properties_3D_number_name(maul_mask): +def test_maul_properties_3D_number_name(maul_mask, u_wind_maul, v_wind_maul): """Ensure correct name given to cube in maul properties for number of mauls.""" expected_name = "Number_of_MAULs" assert ( expected_name - == precipitation.MAUL_properties(maul_mask, output="number").name() + == precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="number" + ).name() ) -def test_maul_properties_3D_number_units(maul_mask): +def test_maul_properties_3D_number_units(maul_mask, u_wind_maul, v_wind_maul): """Ensure correct units given to cube in maul properties for number of mauls.""" expected_units = cf_units.Unit("1") assert ( expected_units - == precipitation.MAUL_properties(maul_mask, output="number").units + == precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="number" + ).units ) -def test_maul_properties_3D_base(maul_mask, precalc_maul_base_3d): +def test_maul_properties_3D_base( + maul_mask, u_wind_maul, v_wind_maul, precalc_maul_base_3d +): """Ensure correct base of MAULs generated for 3D field.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask, output="base").data, + precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="base" + ).data, precalc_maul_base_3d.data, rtol=1e-2, atol=1e-6, @@ -80,26 +94,36 @@ def test_maul_properties_3D_base(maul_mask, precalc_maul_base_3d): ) -def test_maul_properties_3D_base_name(maul_mask): +def test_maul_properties_3D_base_name(maul_mask, u_wind_maul, v_wind_maul): """Ensure correct name given to cube in maul properties for MAUL base.""" expected_name = "MAUL_base_height" assert ( - expected_name == precipitation.MAUL_properties(maul_mask, output="base").name() + expected_name + == precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="base" + ).name() ) -def test_maul_properties_3D_base_units(maul_mask): +def test_maul_properties_3D_base_units(maul_mask, u_wind_maul, v_wind_maul): """Ensure correct units given to cube in maul properties for MAUL base.""" expected_units = cf_units.Unit("m") assert ( - expected_units == precipitation.MAUL_properties(maul_mask, output="base").units + expected_units + == precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="base" + ).units ) -def test_maul_properties_3D_depth(maul_mask, precalc_maul_depth_3d): +def test_maul_properties_3D_depth( + maul_mask, u_wind_maul, v_wind_maul, precalc_maul_depth_3d +): """Ensure correct depth of MAULs generated for 3D field.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask, output="depth").data, + precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="depth" + ).data, precalc_maul_depth_3d.data, rtol=1e-2, atol=1e-6, @@ -107,26 +131,39 @@ def test_maul_properties_3D_depth(maul_mask, precalc_maul_depth_3d): ) -def test_maul_properties_3D_depth_name(maul_mask): +def test_maul_properties_3D_depth_name(maul_mask, u_wind_maul, v_wind_maul): """Ensure correct name given to cube in maul properties for MAUL depth.""" expected_name = "MAUL_depth" assert ( - expected_name == precipitation.MAUL_properties(maul_mask, output="depth").name() + expected_name + == precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="depth" + ).name() ) -def test_maul_properties_3D_depth_units(maul_mask): +def test_maul_properties_3D_depth_units(maul_mask, u_wind_maul, v_wind_maul): """Ensure correct units given to cube in maul properties for MAUL depth.""" expected_units = cf_units.Unit("m") assert ( - expected_units == precipitation.MAUL_properties(maul_mask, output="depth").units + expected_units + == precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="depth" + ).units ) -def test_maul_properties_3D_number_cubelist(maul_mask, precalc_maul_number_3d): +def test_maul_properties_3D_number_cubelist( + maul_mask, u_wind_maul, v_wind_maul, precalc_maul_number_3d +): """Ensure correct number of MAULs generated for 3D field in a cubelist.""" input_list = iris.cube.CubeList([maul_mask, maul_mask]) - expected_list = precipitation.MAUL_properties(input_list, output="number") + v_list = iris.cube.CubeList([v_wind_maul, v_wind_maul]) + u_list = iris.cube.CubeList([u_wind_maul, u_wind_maul]) + expected_list = precipitation.MAUL_properties( + input_list, u_list, v_list, output="number" + ) + print(expected_list) actual_list = iris.cube.CubeList([precalc_maul_number_3d, precalc_maul_number_3d]) for cube_a, cube_b in zip(expected_list, actual_list, strict=True): assert np.allclose( @@ -137,10 +174,16 @@ def test_maul_properties_3D_number_cubelist(maul_mask, precalc_maul_number_3d): ) -def test_maul_properties_3D_base_cubelist(maul_mask, precalc_maul_base_3d): +def test_maul_properties_3D_base_cubelist( + maul_mask, u_wind_maul, v_wind_maul, precalc_maul_base_3d +): """Ensure correct base of MAULs generated for 3D field in a cubelist.""" input_list = iris.cube.CubeList([maul_mask, maul_mask]) - expected_list = precipitation.MAUL_properties(input_list, output="base") + v_list = iris.cube.CubeList([v_wind_maul, v_wind_maul]) + u_list = iris.cube.CubeList([u_wind_maul, u_wind_maul]) + expected_list = precipitation.MAUL_properties( + input_list, u_list, v_list, output="base" + ) actual_list = iris.cube.CubeList([precalc_maul_base_3d, precalc_maul_base_3d]) for cube_a, cube_b in zip(expected_list, actual_list, strict=True): assert np.allclose( @@ -148,10 +191,16 @@ def test_maul_properties_3D_base_cubelist(maul_mask, precalc_maul_base_3d): ) -def test_maul_properties_3D_depth_cubelist(maul_mask, precalc_maul_depth_3d): +def test_maul_properties_3D_depth_cubelist( + maul_mask, u_wind_maul, v_wind_maul, precalc_maul_depth_3d +): """Ensure correct depth of MAULs generated for 3D field in a cubelist.""" input_list = iris.cube.CubeList([maul_mask, maul_mask]) - expected_list = precipitation.MAUL_properties(input_list, output="depth") + v_list = iris.cube.CubeList([v_wind_maul, v_wind_maul]) + u_list = iris.cube.CubeList([u_wind_maul, u_wind_maul]) + expected_list = precipitation.MAUL_properties( + input_list, u_list, v_list, output="depth" + ) actual_list = iris.cube.CubeList([precalc_maul_depth_3d, precalc_maul_depth_3d]) for cube_a, cube_b in zip(expected_list, actual_list, strict=True): assert np.allclose( @@ -159,20 +208,28 @@ def test_maul_properties_3D_depth_cubelist(maul_mask, precalc_maul_depth_3d): ) -def test_maul_properties_4D_time_number(maul_mask_time, precalc_maul_number_4d_time): +def test_maul_properties_4D_time_number( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, precalc_maul_number_4d_time +): """Ensure correct number of MAULs generated for 4D field varying in time.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_time, output="number").data, + precipitation.MAUL_properties( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="number" + ).data, precalc_maul_number_4d_time.data, rtol=1e-2, atol=1e-6, ) -def test_maul_properties_4d_time_base(maul_mask_time, precalc_maul_base_4d_time): +def test_maul_properties_4d_time_base( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, precalc_maul_base_4d_time +): """Ensure correct base of MAULs generated for 4D field varying in time.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_time, output="base").data, + precipitation.MAUL_properties( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="base" + ).data, precalc_maul_base_4d_time.data, rtol=1e-2, atol=1e-6, @@ -180,10 +237,14 @@ def test_maul_properties_4d_time_base(maul_mask_time, precalc_maul_base_4d_time) ) -def test_maul_properties_4d_time_depth(maul_mask_time, precalc_maul_depth_4d_time): +def test_maul_properties_4d_time_depth( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, precalc_maul_depth_4d_time +): """Ensure correct depth of MAULs generated for 4D field varying in time.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_time, output="depth").data, + precipitation.MAUL_properties( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="depth" + ).data, precalc_maul_depth_4d_time.data, rtol=1e-2, atol=1e-6, @@ -192,11 +253,16 @@ def test_maul_properties_4d_time_depth(maul_mask_time, precalc_maul_depth_4d_tim def test_maul_properties_4d_realization_number( - maul_mask_member, precalc_maul_number_4d_realization + maul_mask_member, + u_wind_maul_member, + v_wind_maul_member, + precalc_maul_number_4d_realization, ): """Ensure correct number of MAULs generated for 4D field varying with realization.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_member, output="number").data, + precipitation.MAUL_properties( + maul_mask_member, u_wind_maul_member, v_wind_maul_member, output="number" + ).data, precalc_maul_number_4d_realization.data, rtol=1e-2, atol=1e-6, @@ -204,11 +270,16 @@ def test_maul_properties_4d_realization_number( def test_maul_properties_4D_realization_base( - maul_mask_member, precalc_maul_base_4d_realization + maul_mask_member, + u_wind_maul_member, + v_wind_maul_member, + precalc_maul_base_4d_realization, ): """Ensure correct base of MAULs generated for 4D field with varying realization.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_member, output="base").data, + precipitation.MAUL_properties( + maul_mask_member, u_wind_maul_member, v_wind_maul_member, output="base" + ).data, precalc_maul_base_4d_realization.data, rtol=1e-2, atol=1e-6, @@ -217,11 +288,16 @@ def test_maul_properties_4D_realization_base( def test_maul_properties_4D_realization_depth( - maul_mask_member, precalc_maul_depth_4d_realization + maul_mask_member, + u_wind_maul_member, + v_wind_maul_member, + precalc_maul_depth_4d_realization, ): """Ensure correct depth of MAULs generated for 4D field with varying realization.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_member, output="depth").data, + precipitation.MAUL_properties( + maul_mask_member, u_wind_maul_member, v_wind_maul_member, output="depth" + ).data, precalc_maul_depth_4d_realization.data, rtol=1e-2, atol=1e-6, @@ -229,20 +305,28 @@ def test_maul_properties_4D_realization_depth( ) -def test_maul_properties_5D_number(maul_mask_all, precalc_maul_number_5d): +def test_maul_properties_5D_number( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, precalc_maul_number_5d +): """Ensure correct number of MAULs generated for 5D field.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_all, output="number").data, + precipitation.MAUL_properties( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="number" + ).data, precalc_maul_number_5d.data, rtol=1e-2, atol=1e-6, ) -def test_maul_properties_5D_base(maul_mask_all, precalc_maul_base_5d): +def test_maul_properties_5D_base( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, precalc_maul_base_5d +): """Ensure correct base of MAULs generated for 5D field.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_all, output="base").data, + precipitation.MAUL_properties( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="base" + ).data, precalc_maul_base_5d.data, rtol=1e-2, atol=1e-6, @@ -250,12 +334,123 @@ def test_maul_properties_5D_base(maul_mask_all, precalc_maul_base_5d): ) -def test_maul_properties_5D_depth(maul_mask_all, precalc_maul_depth_5d): +def test_maul_properties_5D_depth( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, precalc_maul_depth_5d +): """Ensure correct depth of MAULs generated for 5D field.""" assert np.allclose( - precipitation.MAUL_properties(maul_mask_all, output="depth").data, + precipitation.MAUL_properties( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="depth" + ).data, precalc_maul_depth_5d.data, rtol=1e-2, atol=1e-6, equal_nan=True, ) + + +def test_maul_properties_wind_below( + maul_mask, u_wind_maul, v_wind_maul, precalc_wind_below_maul +): + """Ensure correct average wind below maul.""" + assert np.allclose( + precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="wind_below" + ).data, + precalc_wind_below_maul.data, + rtol=1e-2, + atol=1e-6, + equal_nan=True, + ) + + +def test_maul_properties_wind_below_name( + maul_mask, u_wind_maul, v_wind_maul, precalc_wind_below_maul +): + """Ensure correct average wind below maul name.""" + assert ( + precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="wind_below" + ).name() + == "windspeed_below_MAUL" + ) + + +def test_maul_properties_wind_below_units( + maul_mask, u_wind_maul, v_wind_maul, precalc_wind_below_maul +): + """Ensure correct average wind below maul units.""" + assert precipitation.MAUL_properties( + maul_mask, u_wind_maul, v_wind_maul, output="wind_below" + ).units == cf_units.Unit("m s^-1") + + +def test_maul_properties_wind_below_cubelist( + maul_mask, u_wind_maul, v_wind_maul, precalc_wind_below_maul +): + """Ensure correct average wind below maul in cubelist.""" + input_list = iris.cube.CubeList([maul_mask, maul_mask]) + v_list = iris.cube.CubeList([v_wind_maul, v_wind_maul]) + u_list = iris.cube.CubeList([u_wind_maul, u_wind_maul]) + expected_list = precipitation.MAUL_properties( + input_list, u_list, v_list, output="wind_below" + ) + actual_list = iris.cube.CubeList([precalc_wind_below_maul, precalc_wind_below_maul]) + for cube_a, cube_b in zip(expected_list, actual_list, strict=True): + assert np.allclose( + cube_a.data, cube_b.data, rtol=1e-2, atol=1e-6, equal_nan=True + ) + + +def test_maul_properties_wind_below_4d_realization( + maul_mask_member, + u_wind_maul_member, + v_wind_maul_member, + precalc_wind_below_maul_4d_realization, +): + """Ensure correct wind below MAUL generated for 4D field with varying realization.""" + assert np.allclose( + precipitation.MAUL_properties( + maul_mask_member, + u_wind_maul_member, + v_wind_maul_member, + output="wind_below", + ).data, + precalc_wind_below_maul_4d_realization.data, + rtol=1e-2, + atol=1e-6, + equal_nan=True, + ) + + +def test_maul_properties_wind_below_4d_time( + maul_mask_time, + u_wind_maul_time, + v_wind_maul_time, + precalc_wind_below_maul_4d_time, +): + """Ensure correct wind below MAUL generated for 4D field with varying time.""" + assert np.allclose( + precipitation.MAUL_properties( + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="wind_below" + ).data, + precalc_wind_below_maul_4d_time.data, + rtol=1e-2, + atol=1e-6, + equal_nan=True, + ) + + +def test_maul_properties_wind_below_5d( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, precalc_wind_below_maul_5d +): + """Ensure correct wind below MAUL generated for 5D field.""" + assert np.allclose( + precipitation.MAUL_properties( + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="wind_below" + ).data, + precalc_wind_below_maul_5d.data, + rtol=1e-2, + atol=1e-6, + equal_nan=True, + ) diff --git a/tests/test_data/precipitation/precalculated_wind_below_maul_3d.nc b/tests/test_data/precipitation/precalculated_wind_below_maul_3d.nc new file mode 100644 index 000000000..ed60a80c6 Binary files /dev/null and b/tests/test_data/precipitation/precalculated_wind_below_maul_3d.nc differ diff --git a/tests/test_data/precipitation/precalculated_wind_below_maul_4d_realization.nc b/tests/test_data/precipitation/precalculated_wind_below_maul_4d_realization.nc new file mode 100644 index 000000000..180b29d8d Binary files /dev/null and b/tests/test_data/precipitation/precalculated_wind_below_maul_4d_realization.nc differ diff --git a/tests/test_data/precipitation/precalculated_wind_below_maul_4d_time.nc b/tests/test_data/precipitation/precalculated_wind_below_maul_4d_time.nc new file mode 100644 index 000000000..462f74123 Binary files /dev/null and b/tests/test_data/precipitation/precalculated_wind_below_maul_4d_time.nc differ diff --git a/tests/test_data/precipitation/precalculated_wind_below_maul_5d.nc b/tests/test_data/precipitation/precalculated_wind_below_maul_5d.nc new file mode 100644 index 000000000..1d72f054e Binary files /dev/null and b/tests/test_data/precipitation/precalculated_wind_below_maul_5d.nc differ diff --git a/tests/test_data/precipitation/u_wind_all.nc b/tests/test_data/precipitation/u_wind_all.nc new file mode 100644 index 000000000..e59b51f8c Binary files /dev/null and b/tests/test_data/precipitation/u_wind_all.nc differ diff --git a/tests/test_data/precipitation/u_wind_basic.nc b/tests/test_data/precipitation/u_wind_basic.nc new file mode 100644 index 000000000..5223899f6 Binary files /dev/null and b/tests/test_data/precipitation/u_wind_basic.nc differ diff --git a/tests/test_data/precipitation/u_wind_member.nc b/tests/test_data/precipitation/u_wind_member.nc new file mode 100644 index 000000000..a27bd9a71 Binary files /dev/null and b/tests/test_data/precipitation/u_wind_member.nc differ diff --git a/tests/test_data/precipitation/u_wind_time.nc b/tests/test_data/precipitation/u_wind_time.nc new file mode 100644 index 000000000..c276bb046 Binary files /dev/null and b/tests/test_data/precipitation/u_wind_time.nc differ diff --git a/tests/test_data/precipitation/v_wind_all.nc b/tests/test_data/precipitation/v_wind_all.nc new file mode 100644 index 000000000..f6bc048d7 Binary files /dev/null and b/tests/test_data/precipitation/v_wind_all.nc differ diff --git a/tests/test_data/precipitation/v_wind_basic.nc b/tests/test_data/precipitation/v_wind_basic.nc new file mode 100644 index 000000000..3d2388834 Binary files /dev/null and b/tests/test_data/precipitation/v_wind_basic.nc differ diff --git a/tests/test_data/precipitation/v_wind_member.nc b/tests/test_data/precipitation/v_wind_member.nc new file mode 100644 index 000000000..869b76995 Binary files /dev/null and b/tests/test_data/precipitation/v_wind_member.nc differ diff --git a/tests/test_data/precipitation/v_wind_time.nc b/tests/test_data/precipitation/v_wind_time.nc new file mode 100644 index 000000000..5f2ff00db Binary files /dev/null and b/tests/test_data/precipitation/v_wind_time.nc differ