From 0befe8ecd3696dd427c8f87b2a1c9ebad1e00b36 Mon Sep 17 00:00:00 2001 From: daflack Date: Mon, 18 May 2026 11:49:57 +0100 Subject: [PATCH 1/9] Adds windspeed below the MAUL Fixes #1963 --- src/CSET/operators/precipitation.py | 12 ++++++------ tests/operators/test_precipitation.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index a7e23b513..b12aafa5d 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -26,7 +26,7 @@ def MAUL_properties( cubes: iris.cube.Cube | iris.cube.CubeList, - output: Literal["number", "base", "depth"], + output: Literal["number", "base", "depth", "wind_below"], ) -> iris.cube.Cube | iris.cube.CubeList: """Identify properties of Moist Absolutely Unstable Layers. @@ -35,11 +35,11 @@ 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"] + 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 ------- @@ -73,9 +73,9 @@ def MAUL_properties( maul_d = iris.cube.CubeList([]) maul_b = 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): diff --git a/tests/operators/test_precipitation.py b/tests/operators/test_precipitation.py index b76d04a09..7b5dcf770 100644 --- a/tests/operators/test_precipitation.py +++ b/tests/operators/test_precipitation.py @@ -26,7 +26,7 @@ def test_maul_properties_wrong_output(maul_mask): """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") From 3231deca67db84dd2c9fe893b2e7f4bf9cb3deb9 Mon Sep 17 00:00:00 2001 From: daflack Date: Mon, 18 May 2026 12:36:03 +0100 Subject: [PATCH 2/9] Adds initial implementation of wind_below_maul --- src/CSET/operators/precipitation.py | 153 +++++++++++++++++++++++++++- 1 file changed, 148 insertions(+), 5 deletions(-) diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index b12aafa5d..83099f884 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -22,10 +22,13 @@ 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, + 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,12 +38,17 @@ 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. + 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, the depth of the deepest MAUL, or the average windspeed below the MAUL, respectively. + Returns ------- cube: iris.cube.Cube | iris.cube.CubeList @@ -61,7 +69,7 @@ def MAUL_properties( 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 + and then finds the top and base of each layer. It will also raise 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. @@ -72,13 +80,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", "wind_below"): raise ValueError( 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 +101,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 +146,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 +264,124 @@ def MAUL_properties( else: maul_depth.data[lat_point, lon_point] = np.nan maul_base.data[lat_point, lon_point] = np.nan + 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] + ) + # 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: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: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:index, lat_point, lon_point + ].data + ) + else: + wind_below_maul.data[lat_point, lon_point] = ( + np.mean( + windspeed[ + 0:index, lat_point, lon_point + ].data + ) + ) + # Here a ValueError is raised if a MAUL is not found, or 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: + 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 +393,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 +414,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 From 2ae246fba5e4f9ef42f8630078160fc973dd469a Mon Sep 17 00:00:00 2001 From: daflack Date: Mon, 18 May 2026 13:50:47 +0100 Subject: [PATCH 3/9] Adds additional data for tests --- tests/conftest.py | 96 ++++++++++ tests/operators/test_precipitation.py | 170 +++++++++++++----- tests/test_data/precipitation/u_wind_all.nc | Bin 0 -> 14288 bytes tests/test_data/precipitation/u_wind_basic.nc | Bin 0 -> 13887 bytes .../test_data/precipitation/u_wind_member.nc | Bin 0 -> 13021 bytes tests/test_data/precipitation/u_wind_time.nc | Bin 0 -> 13100 bytes tests/test_data/precipitation/v_wind_all.nc | Bin 0 -> 14285 bytes tests/test_data/precipitation/v_wind_basic.nc | Bin 0 -> 13887 bytes .../test_data/precipitation/v_wind_member.nc | Bin 0 -> 13029 bytes tests/test_data/precipitation/v_wind_time.nc | Bin 0 -> 13101 bytes 10 files changed, 220 insertions(+), 46 deletions(-) create mode 100644 tests/test_data/precipitation/u_wind_all.nc create mode 100644 tests/test_data/precipitation/u_wind_basic.nc create mode 100644 tests/test_data/precipitation/u_wind_member.nc create mode 100644 tests/test_data/precipitation/u_wind_time.nc create mode 100644 tests/test_data/precipitation/v_wind_all.nc create mode 100644 tests/test_data/precipitation/v_wind_basic.nc create mode 100644 tests/test_data/precipitation/v_wind_member.nc create mode 100644 tests/test_data/precipitation/v_wind_time.nc diff --git a/tests/conftest.py b/tests/conftest.py index 4daba7627..ae6bf5395 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -585,3 +585,99 @@ 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() diff --git a/tests/operators/test_precipitation.py b/tests/operators/test_precipitation.py index 7b5dcf770..b56e691c3 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, 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,38 @@ 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" + ) 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 +173,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 +190,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 +207,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_time, v_wind_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_time, v_wind_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_time, v_wind_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_time, v_wind_time, output="base" + ).data, precalc_maul_base_4d_time.data, rtol=1e-2, atol=1e-6, @@ -180,10 +236,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_time, v_wind_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_time, v_wind_time, output="depth" + ).data, precalc_maul_depth_4d_time.data, rtol=1e-2, atol=1e-6, @@ -192,11 +252,13 @@ 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_member, v_wind_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_member, v_wind_member, output="number" + ).data, precalc_maul_number_4d_realization.data, rtol=1e-2, atol=1e-6, @@ -204,11 +266,13 @@ 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_member, v_wind_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_member, v_wind_member, output="base" + ).data, precalc_maul_base_4d_realization.data, rtol=1e-2, atol=1e-6, @@ -217,11 +281,13 @@ 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_member, v_wind_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_member, v_wind_member, output="depth" + ).data, precalc_maul_depth_4d_realization.data, rtol=1e-2, atol=1e-6, @@ -229,20 +295,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_all, v_wind_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_all, v_wind_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_all, v_wind_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_all, v_wind_all, output="base" + ).data, precalc_maul_base_5d.data, rtol=1e-2, atol=1e-6, @@ -250,10 +324,14 @@ 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_all, v_wind_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_all, v_wind_all, output="depth" + ).data, precalc_maul_depth_5d.data, rtol=1e-2, atol=1e-6, 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 0000000000000000000000000000000000000000..e59b51f8c20b91c701b9dc4f0d4c1bf6b53fe7e1 GIT binary patch literal 14288 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 ZsQaE9k{u*Wc%4iPugdxyUikj6zX4CXvbF#K literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a27bd9a71bc072b1b9e518fc8ef3f2726a34b8e6 GIT binary patch literal 13021 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 F&c6(dJf;8u literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f6bc048d70915fd0df10665115be1037ea1bae90 GIT binary patch literal 14285 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..869b7699550811dd897bc6d68bd62ace8ee74557 GIT binary patch literal 13029 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 PiMty$K4m|X-!#s@#gsuo literal 0 HcmV?d00001 From 993cf749ae803623e1b3e5c35e7c38d2cd023202 Mon Sep 17 00:00:00 2001 From: daflack Date: Mon, 18 May 2026 13:54:41 +0100 Subject: [PATCH 4/9] Corrects test data --- tests/operators/test_precipitation.py | 45 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/tests/operators/test_precipitation.py b/tests/operators/test_precipitation.py index b56e691c3..8894f8dff 100644 --- a/tests/operators/test_precipitation.py +++ b/tests/operators/test_precipitation.py @@ -208,12 +208,12 @@ def test_maul_properties_3D_depth_cubelist( def test_maul_properties_4D_time_number( - maul_mask_time, u_wind_time, v_wind_time, precalc_maul_number_4d_time + 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, u_wind_time, v_wind_time, output="number" + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="number" ).data, precalc_maul_number_4d_time.data, rtol=1e-2, @@ -222,12 +222,12 @@ def test_maul_properties_4D_time_number( def test_maul_properties_4d_time_base( - maul_mask_time, u_wind_time, v_wind_time, precalc_maul_base_4d_time + 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, u_wind_time, v_wind_time, output="base" + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="base" ).data, precalc_maul_base_4d_time.data, rtol=1e-2, @@ -237,12 +237,12 @@ def test_maul_properties_4d_time_base( def test_maul_properties_4d_time_depth( - maul_mask_time, u_wind_time, v_wind_time, precalc_maul_depth_4d_time + 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, u_wind_time, v_wind_time, output="depth" + maul_mask_time, u_wind_maul_time, v_wind_maul_time, output="depth" ).data, precalc_maul_depth_4d_time.data, rtol=1e-2, @@ -252,12 +252,15 @@ def test_maul_properties_4d_time_depth( def test_maul_properties_4d_realization_number( - maul_mask_member, u_wind_member, v_wind_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, u_wind_member, v_wind_member, output="number" + maul_mask_member, u_wind_maul_member, v_wind_maul_member, output="number" ).data, precalc_maul_number_4d_realization.data, rtol=1e-2, @@ -266,12 +269,15 @@ def test_maul_properties_4d_realization_number( def test_maul_properties_4D_realization_base( - maul_mask_member, u_wind_member, v_wind_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, u_wind_member, v_wind_member, output="base" + maul_mask_member, u_wind_maul_member, v_wind_maul_member, output="base" ).data, precalc_maul_base_4d_realization.data, rtol=1e-2, @@ -281,12 +287,15 @@ def test_maul_properties_4D_realization_base( def test_maul_properties_4D_realization_depth( - maul_mask_member, u_wind_member, v_wind_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, u_wind_member, v_wind_member, output="depth" + maul_mask_member, u_wind_maul_member, v_wind_maul_member, output="depth" ).data, precalc_maul_depth_4d_realization.data, rtol=1e-2, @@ -296,12 +305,12 @@ def test_maul_properties_4D_realization_depth( def test_maul_properties_5D_number( - maul_mask_all, u_wind_all, v_wind_all, precalc_maul_number_5d + 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, u_wind_all, v_wind_all, output="number" + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="number" ).data, precalc_maul_number_5d.data, rtol=1e-2, @@ -310,12 +319,12 @@ def test_maul_properties_5D_number( def test_maul_properties_5D_base( - maul_mask_all, u_wind_all, v_wind_all, precalc_maul_base_5d + 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, u_wind_all, v_wind_all, output="base" + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="base" ).data, precalc_maul_base_5d.data, rtol=1e-2, @@ -325,12 +334,12 @@ def test_maul_properties_5D_base( def test_maul_properties_5D_depth( - maul_mask_all, u_wind_all, v_wind_all, precalc_maul_depth_5d + 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, u_wind_all, v_wind_all, output="depth" + maul_mask_all, u_wind_maul_all, v_wind_maul_all, output="depth" ).data, precalc_maul_depth_5d.data, rtol=1e-2, From 0f670b5017dc84fdc23ee38ae2498fa695f38f87 Mon Sep 17 00:00:00 2001 From: daflack Date: Mon, 18 May 2026 13:57:36 +0100 Subject: [PATCH 5/9] Update looping to use itertools.product --- src/CSET/operators/precipitation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index 83099f884..ffca6eb3f 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -14,6 +14,7 @@ """Operators to perform various kinds of image processing.""" +import itertools from typing import Literal import iris @@ -87,7 +88,7 @@ def MAUL_properties( f"""Unexpected value for output. Expected number, base, depth or wind_below. Got {output}.""" ) - for cube, u, v in zip( + for cube, u, v in itertools.product( iter_maybe(cubes), iter_maybe(u_cubes), iter_maybe(v_cubes), strict=True ): # Check for binary fields. From d4333223b26d4495742c64773366d5019edacc9d Mon Sep 17 00:00:00 2001 From: daflack Date: Mon, 18 May 2026 14:04:08 +0100 Subject: [PATCH 6/9] Fixes tests --- src/CSET/operators/precipitation.py | 3 +-- tests/operators/test_precipitation.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index ffca6eb3f..83099f884 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -14,7 +14,6 @@ """Operators to perform various kinds of image processing.""" -import itertools from typing import Literal import iris @@ -88,7 +87,7 @@ def MAUL_properties( f"""Unexpected value for output. Expected number, base, depth or wind_below. Got {output}.""" ) - for cube, u, v in itertools.product( + for cube, u, v in zip( iter_maybe(cubes), iter_maybe(u_cubes), iter_maybe(v_cubes), strict=True ): # Check for binary fields. diff --git a/tests/operators/test_precipitation.py b/tests/operators/test_precipitation.py index 8894f8dff..6f22bd101 100644 --- a/tests/operators/test_precipitation.py +++ b/tests/operators/test_precipitation.py @@ -163,6 +163,7 @@ def test_maul_properties_3D_number_cubelist( 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( From 7739590c8dc1c45b0ee625bb8720722419d120b9 Mon Sep 17 00:00:00 2001 From: daflack Date: Tue, 19 May 2026 09:21:27 +0100 Subject: [PATCH 7/9] Add tests, fixes operator, adds test data --- src/CSET/operators/precipitation.py | 26 +++-- tests/conftest.py | 58 ++++++++++ tests/operators/test_precipitation.py | 107 ++++++++++++++++++ .../precalculated_wind_below_maul_3d.nc | Bin 0 -> 21404 bytes ...lculated_wind_below_maul_4d_realization.nc | Bin 0 -> 22454 bytes .../precalculated_wind_below_maul_4d_time.nc | Bin 0 -> 22569 bytes .../precalculated_wind_below_maul_5d.nc | Bin 0 -> 24664 bytes 7 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 tests/test_data/precipitation/precalculated_wind_below_maul_3d.nc create mode 100644 tests/test_data/precipitation/precalculated_wind_below_maul_4d_realization.nc create mode 100644 tests/test_data/precipitation/precalculated_wind_below_maul_4d_time.nc create mode 100644 tests/test_data/precipitation/precalculated_wind_below_maul_5d.nc diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index 83099f884..25022edaa 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -295,6 +295,10 @@ def MAUL_properties( 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 @@ -312,7 +316,7 @@ def MAUL_properties( windspeed[ mem_number, time_point, - 0:index, + 0:height_index, lat_point, lon_point, ].data @@ -326,7 +330,10 @@ def MAUL_properties( mem_number, lat_point, lon_point ] = np.mean( windspeed[ - mem_number, 0:index, lat_point, lon_point + mem_number, + 0:height_index, + lat_point, + lon_point, ].data ) elif ( @@ -338,24 +345,27 @@ def MAUL_properties( time_point, lat_point, lon_point ] = np.mean( windspeed[ - time_point, 0:index, lat_point, lon_point + time_point, + 0:height_index, + lat_point, + lon_point, ].data ) else: wind_below_maul.data[lat_point, lon_point] = ( np.mean( windspeed[ - 0:index, lat_point, lon_point + 0:height_index, lat_point, lon_point ].data ) ) - # Here a ValueError is raised if a MAUL is not found, or if - # the MAUL starts at the surface and so there is no wind - # below the MAUL however these are a valid answers, + # 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: + except (ValueError, IndexError): if ( len(number_of_MAULs.coord("realization").points) != 1 diff --git a/tests/conftest.py b/tests/conftest.py index ae6bf5395..5b3cfb107 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -681,3 +681,61 @@ def v_wind_maul_read_only(): 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 6f22bd101..1684c0550 100644 --- a/tests/operators/test_precipitation.py +++ b/tests/operators/test_precipitation.py @@ -347,3 +347,110 @@ def test_maul_properties_5D_depth( 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 0000000000000000000000000000000000000000..ed60a80c6d3cfdf449cf0e63296b485796b3556f GIT binary patch literal 21404 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 literal 0 HcmV?d00001 From 9308b865edd9555f40225e7bbe68be01009d6fe7 Mon Sep 17 00:00:00 2001 From: daflack Date: Tue, 19 May 2026 09:42:23 +0100 Subject: [PATCH 8/9] Update recipes and add new recipe for wind below MAUL --- src/CSET/operators/precipitation.py | 10 +- ...age_windspeed_below_maul_spatial_plot.yaml | 162 ++++++++++++++++++ ...solutely_unstable_layers_spatial_plot.yaml | 24 ++- ...solutely_unstable_layers_spatial_plot.yaml | 24 ++- ...le_layers_present_in_a_spatial_column.yaml | 24 ++- 5 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 src/CSET/recipes/derived_diagnostics/precipitation/extreme_precipitation/average_windspeed_below_maul_spatial_plot.yaml diff --git a/src/CSET/operators/precipitation.py b/src/CSET/operators/precipitation.py index 25022edaa..f6c37ce5a 100644 --- a/src/CSET/operators/precipitation.py +++ b/src/CSET/operators/precipitation.py @@ -58,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 @@ -68,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. It will also raise 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. @@ -264,6 +265,7 @@ 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. 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 From b0a6337a39414d10c0ecf36fdf246d804013d008 Mon Sep 17 00:00:00 2001 From: daflack Date: Tue, 19 May 2026 09:48:56 +0100 Subject: [PATCH 9/9] Adds plumbing of recipe into the workflow --- .../meta/diagnostics/rose-meta.conf | 20 ++++++++++++++++--- .../cset_workflow/rose-suite.conf.example | 1 + src/CSET/loaders/spatial_field.py | 16 +++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) 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,