Coverage for src/CSET/operators/maths.py: 100%
42 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-11 15:11 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-11 15:11 +0000
1# © Crown copyright, Met Office (2022-2025) and CSET contributors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""Miscellaneous operators."""
17import logging
19import iris
20import iris.analysis.calculus
21from iris.cube import Cube, CubeList
23from CSET._common import iter_maybe
25logger = logging.getLogger(__name__)
28def addition(addend_1, addend_2):
29 """Addition of two fields.
31 Parameters
32 ----------
33 addend_1: Cube
34 Any field to have another field added to it.
35 addend_2: Cube
36 Any field to be added to another field.
38 Returns
39 -------
40 Cube
42 Raises
43 ------
44 ValueError, iris.exceptions.NotYetImplementedError
45 When the cubes are not compatible.
47 Notes
48 -----
49 This is a simple operator designed for combination of diagnostics or
50 creating new diagnostics by using recipes.
52 Examples
53 --------
54 >>> field_addition = maths.addition(kinetic_energy_u, kinetic_energy_v)
56 """
57 return addend_1 + addend_2
60def subtraction(
61 minuend: Cube | CubeList, subtrahend: Cube | CubeList
62) -> Cube | CubeList:
63 """Subtraction of two fields.
65 Parameters
66 ----------
67 minuend: Cube | CubeList
68 Any field(s) to have another field subtracted from it.
69 subtrahend: Cube | CubeList
70 Any field(s) to be subtracted from to another field.
72 Returns
73 -------
74 Cube | CubeList
76 Raises
77 ------
78 ValueError, iris.exceptions.NotYetImplementedError
79 When the cubes are not compatible.
81 Notes
82 -----
83 This is a simple operator designed for combination of diagnostics or
84 creating new diagnostics by using recipes. It can be used for model
85 differences to allow for comparisons between the same field in different
86 models or model configurations.
88 If called with 2 Cubes as input, will return a difference cube.
89 If called with 2 CubeLists as input, with return a CubeList of differences.
90 If called with CubeList as minuend and Cube as subtrahend, will return CubeList of differences subtracting Cube from each element of input CubeList.
91 If called with Cube as minuend and CubeList as subtrahend, will return CubeList of differences subtracting each element on CubeList from input Cube.
93 Examples
94 --------
95 >>> model_diff = maths.subtraction(temperature_model_A, temperature_model_B)
97 """
99 def subtract_preserve_attributes(cube_a: Cube, cube_b: Cube) -> Cube:
100 result = cube_a - cube_b
101 result.attributes.update(cube_a.attributes)
102 return result
104 # Case where both inputs are single cubes
105 if isinstance(minuend, iris.cube.Cube) and isinstance(subtrahend, iris.cube.Cube):
106 return subtract_preserve_attributes(minuend, subtrahend)
108 # Check if minuend is iterable
109 cubes_a = iter_maybe(minuend)
111 # Case: subtrahend also iterable
112 if isinstance(subtrahend, iris.cube.CubeList):
113 cubes_b = iter_maybe(subtrahend)
114 if isinstance(minuend, iris.cube.CubeList):
115 # Case: subtract cubelist from cubelist - assume both same sizes
116 result = iris.cube.CubeList(
117 [
118 subtract_preserve_attributes(cube_a, cube_b)
119 for cube_a, cube_b in zip(cubes_a, cubes_b, strict=True)
120 ]
121 )
122 else:
123 # Case: subtract each element of subtrahend from single cube
124 result = iris.cube.CubeList(
125 [subtract_preserve_attributes(minuend, cube_b) for cube_b in cubes_b]
126 )
127 else:
128 # Case: subtract single cube from each minuend
129 result = iris.cube.CubeList(
130 [subtract_preserve_attributes(cube_a, subtrahend) for cube_a in cubes_a]
131 )
133 # Return single cube if only one result, else return CubeList
134 return result[0] if len(result) == 1 else result
137def division(numerator, denominator):
138 """Division of two fields.
140 Parameters
141 ----------
142 numerator: Cube
143 Any field to have the ratio taken with respect to another field.
144 denominator: Cube
145 Any field used to divide another field or provide the reference
146 value in a ratio.
148 Returns
149 -------
150 Cube
152 Raises
153 ------
154 ValueError
155 When the cubes are not compatible.
157 Notes
158 -----
159 This is a simple operator designed for combination of diagnostics or
160 creating new diagnostics by using recipes.
162 Examples
163 --------
164 >>> bowen_ratio = maths.division(sensible_heat_flux, latent_heat_flux)
166 """
167 return numerator / denominator
170def multiplication(
171 multiplicand: Cube | CubeList, multiplier: Cube | CubeList
172) -> Cube | CubeList:
173 """Multiplication of two fields.
175 Parameters
176 ----------
177 multiplicand: Cube | CubeList
178 Any field to be multiplied by another field.
179 multiplier: Cube | CubeList
180 Any field to be multiplied to another field.
182 Returns
183 -------
184 Cube | CubeList
185 The result of multiplicand x multiplier.
187 Raises
188 ------
189 ValueError
190 When the cubes are not compatible.
192 Notes
193 -----
194 This is a simple operator designed for combination of diagnostics or
195 creating new diagnostics by using recipes. CubeLists are multiplied
196 on a strict ordering (e.g. first cube with first cube).
198 Examples
199 --------
200 >>> filtered_CAPE_ratio = maths.multiplication(CAPE_ratio, inflow_layer_properties)
202 """
203 new_cubelist = iris.cube.CubeList([])
204 for cube_a, cube_b in zip(
205 iter_maybe(multiplicand), iter_maybe(multiplier), strict=True
206 ):
207 multiplied_cube = cube_a * cube_b
208 multiplied_cube.rename(f"{cube_a.name()}_x_{cube_b.name()}")
209 new_cubelist.append(multiplied_cube)
210 if len(new_cubelist) == 1:
211 return new_cubelist[0]
212 else:
213 return new_cubelist
216def differentiate(
217 cubes: iris.cube.Cube | iris.cube.CubeList, coordinate: str, **kwargs
218) -> iris.cube.Cube | iris.cube.CubeList:
219 """Differentiate a cube on a specified coordinate.
221 Arguments
222 ---------
223 cubes: iris.cube.Cube | iris.cube.CubeList
224 A Cube or CubeList of a field that is to be differentiated.
226 coordinate: str
227 The coordinate that is to be differentiated over.
229 Returns
230 -------
231 iris.cube.Cube | iris.cube.CubeList
232 The differential of the cube along the specified coordinate.
234 Notes
235 -----
236 The differential is calculated based on a carteisan grid. This calculation
237 is then suitable for vertical and temporal derivatives. It is not sensible
238 for horizontal derivatives if they are based on spherical coordinates (e.g.
239 latitude and longitude). In essence this operator is a CSET wrapper around
240 `iris.analysis.calculus.differentiate <https://scitools-iris.readthedocs.io/en/stable/generated/api/iris.analysis.calculus.html#iris.analysis.calculus.differentiate>`_.
242 Examples
243 --------
244 >>> dT_dz = maths.differentiate(temperature, "altitude")
245 """
246 new_cubelist = iris.cube.CubeList([])
247 for cube in iter_maybe(cubes):
248 dcube = iris.analysis.calculus.differentiate(cube, coordinate)
249 new_cubelist.append(dcube)
250 if len(new_cubelist) == 1:
251 return new_cubelist[0]
252 else:
253 return new_cubelist