From ff561d3ab66c898856d96f5d67cb9263e49cf67d Mon Sep 17 00:00:00 2001 From: daflack Date: Tue, 17 Mar 2026 16:05:42 +0000 Subject: [PATCH 1/4] Adds a differentiation operator Fixes #1959 --- src/CSET/operators/misc.py | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/CSET/operators/misc.py b/src/CSET/operators/misc.py index a7480127d..f543cd3f1 100644 --- a/src/CSET/operators/misc.py +++ b/src/CSET/operators/misc.py @@ -19,6 +19,7 @@ from collections.abc import Iterable import iris +import iris.analysis.calculus import numpy as np from iris.cube import Cube, CubeList @@ -480,3 +481,43 @@ def rename_cube(cubes: iris.cube.Cube | iris.cube.CubeList, name: str): return new_cubelist[0] else: return new_cubelist + + +def differentiate( + cubes: iris.cube.Cube | iris.cube.CubeList, coordinate: str, **kwargs +) -> iris.cube.Cube | iris.cube.CubeList: + """Differentiate a cube on a specified coordinate. + + Arguments + --------- + cubes: iris.cube.Cube | iris.cube.CubeList + A Cube or CubeList of a field that is to be differentiated. + + coordinate: str + The coordinate that is to be differentiated over. + + Returns + ------- + iris.cube.Cube | iris.cube.CubeList + The differential of the cube along the specified coordinate. + + Notes + ----- + The differential is calculated based on a carteisan grid. This calculation + is then suitable for vertical and temporal derivatives. It is not sensible + for horizontal derivatives if they are based on spherical coordinates (e.g. + latitude and longitude). In essensce this operator is a CSET wrapper around + iris.analysis.calculus.differentiate. + + Examples + -------- + >>> dT_dz = misc.differentiate(temperature, "altitude") + """ + new_cubelist = iris.cube.CubeList([]) + for cube in iter_maybe(cubes): + dcube = iris.analysis.calculus.differentiate(cube, coordinate) + new_cubelist.append(dcube) + if len(new_cubelist) == 1: + return new_cubelist[0] + else: + return new_cubelist From 73b21029860ff160507aa5c2ec93cd5d79b4dec0 Mon Sep 17 00:00:00 2001 From: daflack Date: Tue, 17 Mar 2026 16:12:22 +0000 Subject: [PATCH 2/4] Adds tests for differentitate operator --- tests/operators/test_misc.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/operators/test_misc.py b/tests/operators/test_misc.py index 98550f29b..864a8b01a 100644 --- a/tests/operators/test_misc.py +++ b/tests/operators/test_misc.py @@ -17,6 +17,7 @@ import datetime import iris +import iris.analysis.calculus import iris.coords import iris.cube import iris.exceptions @@ -407,3 +408,24 @@ def test_rename_cube_for_cubelist(cube): new_cubelist = misc.rename_cube(cube_list, "air_temperature_at_screen_level") for new in new_cubelist: assert new.name() == "air_temperature_at_screen_level" + + +def test_differentitate(vertical_profile_cube): + """Test a differentitation of a vertical profile cube.""" + expected_cube = iris.analysis.calculus.differentiate( + vertical_profile_cube, "pressure" + ) + actual_cube = misc.differentiate(vertical_profile_cube, coordinate="pressure") + assert np.allclose(actual_cube.data, expected_cube.data, rtol=1e-6, atol=1e-2) + + +def test_differentitate_cubelist(long_forecast): + """Test a differentiation of a CubeList.""" + # Create input CubeList. + input_cubes = iris.cube.CubeList([long_forecast, long_forecast]) + # Create expected cube and then convert to a CubeList + expectedcube = iris.analysis.calculus.differentiate(long_forecast, "time") + expected_cubelist = iris.cube.CubeList([expectedcube, expectedcube]) + new_cubelist = misc.differentiate(input_cubes, "time") + for actual, expected in zip(new_cubelist, expected_cubelist, strict=True): + assert np.allclose(actual.data, expected.data, rtol=1e-6, atol=1e-2) From 647c96a7ee083b420f7d43b86edce4344c13802d Mon Sep 17 00:00:00 2001 From: daflack Date: Tue, 17 Mar 2026 16:18:38 +0000 Subject: [PATCH 3/4] Links in documentation to the iris operator it wraps around --- src/CSET/operators/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/misc.py b/src/CSET/operators/misc.py index f543cd3f1..11f642ca7 100644 --- a/src/CSET/operators/misc.py +++ b/src/CSET/operators/misc.py @@ -507,7 +507,7 @@ def differentiate( is then suitable for vertical and temporal derivatives. It is not sensible for horizontal derivatives if they are based on spherical coordinates (e.g. latitude and longitude). In essensce this operator is a CSET wrapper around - iris.analysis.calculus.differentiate. + `iris.analysis.calculus.differentiate `_. Examples -------- From c9bb981935bdbb8a44a5d1c70fe474bb233d847b Mon Sep 17 00:00:00 2001 From: David Flack <77390156+daflack@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:20:41 +0000 Subject: [PATCH 4/4] Fix typo --- src/CSET/operators/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/misc.py b/src/CSET/operators/misc.py index 11f642ca7..3d7d12298 100644 --- a/src/CSET/operators/misc.py +++ b/src/CSET/operators/misc.py @@ -506,7 +506,7 @@ def differentiate( The differential is calculated based on a carteisan grid. This calculation is then suitable for vertical and temporal derivatives. It is not sensible for horizontal derivatives if they are based on spherical coordinates (e.g. - latitude and longitude). In essensce this operator is a CSET wrapper around + latitude and longitude). In essence this operator is a CSET wrapper around `iris.analysis.calculus.differentiate `_. Examples