From d0c576b6a8633f51fd2ab7a6f1bf0a09574dcf29 Mon Sep 17 00:00:00 2001 From: Sana Mahmood Date: Thu, 18 Jun 2026 10:41:15 +0100 Subject: [PATCH] adding cth diagnostic --- src/CSET/operators/_colorbar_definition.json | 33 +++++ src/CSET/operators/_stash_to_lfric.py | 7 + src/CSET/operators/plot.py | 130 ++++++++++++++++++- src/CSET/operators/read.py | 9 ++ 4 files changed, 178 insertions(+), 1 deletion(-) diff --git a/src/CSET/operators/_colorbar_definition.json b/src/CSET/operators/_colorbar_definition.json index 757219d00..244c73b35 100644 --- a/src/CSET/operators/_colorbar_definition.json +++ b/src/CSET/operators/_colorbar_definition.json @@ -129,6 +129,39 @@ "max": 1, "min": 0.0001 }, + "cloud_top_altitude": { + "cmap": "cividis", + "levels": [ + 0, + 1, + 500, + 1000, + 1500, + 2000, + 3000, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + 15000, + 20000, + 25000, + 30000, + 35000, + 40000, + 45000 + ], + "ymax": 1.0, + "ymin": 0.0 + }, + "cloud_top_altitude_difference": { + "cmap": "PRGn", + "max": 40.0, + "min": -40.0 + }, "combined_cloud_amount": { "cmap": "Greys_r", "max": 1, diff --git a/src/CSET/operators/_stash_to_lfric.py b/src/CSET/operators/_stash_to_lfric.py index 537287df8..3a29855b0 100644 --- a/src/CSET/operators/_stash_to_lfric.py +++ b/src/CSET/operators/_stash_to_lfric.py @@ -7,6 +7,11 @@ # Set up UM STASH code to cube name mapping # Form of entry is : (, ) STASH_TO_LFRIC = { + "m01s00i075": ("number_concentration_of_cloud_droplets_after_microphysics", "face"), + "m01s00i076": ("number_concentration_of_rain_after_microphysics", "face"), + "m01s00i078": ("number_concentration_of_cloud_ice_after_microphysics", "face"), + "m01s00i079": ("number_concentration_of_snow_after_microphysics", "face"), + "m01s00i081": ("number_concentration_of_graupel_after_microphysics", "face"), "m01s00i002": ("eastward_wind_at_cell_centres", "face"), "m01s00i003": ("northward_wind_at_cell_centres", "face"), "m01s00i004": ("air_potential_temperature", "face"), @@ -320,6 +325,7 @@ "ceilometer_filtered_combined_cloud_amount_maximum_random_overlap", "face", ), + "m01s09i240": ("cloud_top_altitude", "face"), "m01s10i185": ("eastward_wind_increment_from_solver", "face"), "m01s10i186": ("northward_wind_increment_from_solver", "face"), "m01s10i187": ("vertical_air_velocity_increment_from_solver", "face"), @@ -522,6 +528,7 @@ "m01s09i204": ("medium_type_cloud_area_fraction", "face"), "m01s09i205": ("high_type_cloud_area_fraction", "face"), "m01s09i210": ("cloud_base_altitude", "face"), + "m01s09i240": ("cloud_top_altitude", "face"), "m01s09i218": ("cloud_fraction_below_1000feet_asl", "face"), "m01s09i231": ("combined_cloud_amount", "face"), "m01s15i229": ("potential_vorticity_at_pressure_levels", "face"), diff --git a/src/CSET/operators/plot.py b/src/CSET/operators/plot.py index c73570ca7..a16e03b51 100644 --- a/src/CSET/operators/plot.py +++ b/src/CSET/operators/plot.py @@ -346,6 +346,9 @@ def _colorbar_map_levels(cube: iris.cube.Cube, axis: Literal["x", "y"] | None = # Overwrite cmap, levels and norm for specific variables that # require custom colorbar_map as these can not be defined in the # JSON file. + cmap, levels, norm = _custom_colourmap_cloud_top_altitude( + cube, cmap, levels, norm + ) cmap, levels, norm = _custom_colourmap_precipitation(cube, cmap, levels, norm) cmap, levels, norm = _custom_colourmap_visibility_in_air( cube, cmap, levels, norm @@ -443,6 +446,11 @@ def _setup_spatial_map( axes = figure.add_subplot(projection=projection) # Add coastlines and borderlines if cube contains x and y map coordinates. + # add coastlines if cloud top altitude + if "cloud_top_altitude" in cube.name(): + coastcol = "white" + axes.coastlines(resolution="10m", color=coastcol) + # Avoid adding lines for masked data or specific fixed ancillary spatial plots. if iris.util.is_masked(cube.data) or any( name in cube.name() for name in ["land_", "orography", "altitude"] @@ -818,8 +826,61 @@ def _plot_and_save_spatial_plot( cbar.set_ticklabels([f"{level:.2f}" for level in levels]) if "rainfall" or "snowfall" or "visibility" in cube.name(): cbar.set_ticklabels([f"{level:.3g}" for level in levels]) - logging.debug("Set colorbar ticks and labels.") + logging.debug("Set colorbar ticks and labels.") + # if "cloud_top_altitude" in cube.name(): + if ( + "cloud_top_altitude" in cube.name() and "difference" not in cube.long_name + # and "mask" not in cube.long_name + ): + cbar.set_ticks( + [ + 0.001, + 0.5, + 1.0000, + 1.500, + 2.000, + 3.000, + 4.000, + 5.000, + 6.000, + 7.000, + 8.000, + 9.000, + 10.000, + 15.000, + 20.000, + 25.000, + 30.000, + 35.000, + 40.000, + ] + ) + cbar.set_ticklabels( + [ + "1", + "500", + "1000", + "1500", + "2000", + "3000", + "4000", + "5000", + "6000", + "7000", + "8000", + "9000", + "10kft", + "15kft", + "20kft", + "25kft", + "30kft", + "35kft", + "40kft", + ], + size=6, + ) + cbar.set_label(label=f"{cube.name()}", size=14) # Save plot. fig.savefig(filename, bbox_inches="tight", dpi=_get_plot_resolution()) logging.info("Saved spatial plot to %s", filename) @@ -2178,6 +2239,73 @@ def _custom_colormap_probability( return cmap, levels, norm +def _custom_colourmap_cloud_top_altitude(cube: iris.cube.Cube, cmap, levels, norm): + """Return a custom colourmap for the current recipe.""" + varnames = filter(None, [cube.long_name, cube.standard_name, cube.var_name]) + if ( + any("cloud_top_altitude" in name for name in varnames) + and "difference" not in cube.long_name + and "mask" not in cube.long_name + ): + levels = [ + 0, + 0.001, + 0.500, + 1, + 1.5, + 2.0, + 3.0, + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + 9.0, + 10.0, + 15.0, + 20.0, + 25.0, + 30.0, + 35.0, + 40.0, + 45.0, + ] + colors = [ + "#000000", # 0–1 + "#ff1a1a", # 1–500 + "#ff7f00", # 500–1000 + "#f0b67a", # 1000–1500 + "#e6e67a", # 1500–2000 + "#8c008c", # 2000–3000 + "#ff00ff", # 3000–4000 + "#d9a3d9", # 4000–5000 + "#00e673", # 5000–6000 + "#66ff00", # 6000–7000 + "#7fbf00", # 7000–8000 + "#808000", # 8000–9000 + "#008000", # 9000–10000 + "#8c4b00", # 10–15kft + "#0b3d91", # 15–20kft + "#1a00ff", # 20–25kft + "#4aa3d9", # 25–30kft + "#1fd1d1", # 30–35kft + "#bfbfbf", # 35–40kft + "#ffffff", # 40k+ + ] + # Create a custom colormap + cmap = mcolors.ListedColormap(colors) + # Normalize the levels + norm = mcolors.BoundaryNorm(levels, cmap.N) + cmap.set_bad("black") + logging.info("change colormap for cloud_top_altitude variable colorbar.") + else: + # do nothing and keep existing colorbar attributes + cmap = cmap + levels = levels + norm = norm + return cmap, levels, norm + + def _custom_colourmap_precipitation(cube: iris.cube.Cube, cmap, levels, norm): """Return a custom colourmap for the current recipe.""" varnames = filter(None, [cube.long_name, cube.standard_name, cube.var_name]) diff --git a/src/CSET/operators/read.py b/src/CSET/operators/read.py index e61f93236..089007b72 100644 --- a/src/CSET/operators/read.py +++ b/src/CSET/operators/read.py @@ -373,6 +373,7 @@ def _loading_callback(cube: iris.cube.Cube, field, filename: str) -> iris.cube.C cube = _convert_cube_units_callback(cube) cube = _grid_longitude_fix_callback(cube) _fix_lfric_cloud_base_altitude(cube) + _fix_lfric_cloud_top_altitude(cube) _proleptic_gregorian_fix(cube) _lfric_time_callback(cube) _lfric_forecast_period_callback(cube) @@ -847,6 +848,14 @@ def _fix_lfric_cloud_base_altitude(cube: iris.cube.Cube): cube.data = dask.array.ma.masked_greater(cube.core_data(), 144.0) +def _fix_lfric_cloud_top_altitude(cube: iris.cube.Cube): + """Mask cloud_top_altitude diagnostic in regions with no cloud.""" + varnames = filter(None, [cube.long_name, cube.standard_name, cube.var_name]) + if any("cloud_top_altitude" in name for name in varnames): + # Mask cube where set < 0 to catch default negative numbers + cube.data = dask.array.ma.masked_less(cube.core_data(), 0) + + def _fix_um_winds(cubes: iris.cube.CubeList): """To make winds from the UM consistent with those from LFRic.