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

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. 

14 

15"""Miscellaneous operators.""" 

16 

17import logging 

18 

19import iris 

20import iris.analysis.calculus 

21from iris.cube import Cube, CubeList 

22 

23from CSET._common import iter_maybe 

24 

25logger = logging.getLogger(__name__) 

26 

27 

28def addition(addend_1, addend_2): 

29 """Addition of two fields. 

30 

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. 

37 

38 Returns 

39 ------- 

40 Cube 

41 

42 Raises 

43 ------ 

44 ValueError, iris.exceptions.NotYetImplementedError 

45 When the cubes are not compatible. 

46 

47 Notes 

48 ----- 

49 This is a simple operator designed for combination of diagnostics or 

50 creating new diagnostics by using recipes. 

51 

52 Examples 

53 -------- 

54 >>> field_addition = maths.addition(kinetic_energy_u, kinetic_energy_v) 

55 

56 """ 

57 return addend_1 + addend_2 

58 

59 

60def subtraction( 

61 minuend: Cube | CubeList, subtrahend: Cube | CubeList 

62) -> Cube | CubeList: 

63 """Subtraction of two fields. 

64 

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. 

71 

72 Returns 

73 ------- 

74 Cube | CubeList 

75 

76 Raises 

77 ------ 

78 ValueError, iris.exceptions.NotYetImplementedError 

79 When the cubes are not compatible. 

80 

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. 

87 

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. 

92 

93 Examples 

94 -------- 

95 >>> model_diff = maths.subtraction(temperature_model_A, temperature_model_B) 

96 

97 """ 

98 

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 

103 

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) 

107 

108 # Check if minuend is iterable 

109 cubes_a = iter_maybe(minuend) 

110 

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 ) 

132 

133 # Return single cube if only one result, else return CubeList 

134 return result[0] if len(result) == 1 else result 

135 

136 

137def division(numerator, denominator): 

138 """Division of two fields. 

139 

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. 

147 

148 Returns 

149 ------- 

150 Cube 

151 

152 Raises 

153 ------ 

154 ValueError 

155 When the cubes are not compatible. 

156 

157 Notes 

158 ----- 

159 This is a simple operator designed for combination of diagnostics or 

160 creating new diagnostics by using recipes. 

161 

162 Examples 

163 -------- 

164 >>> bowen_ratio = maths.division(sensible_heat_flux, latent_heat_flux) 

165 

166 """ 

167 return numerator / denominator 

168 

169 

170def multiplication( 

171 multiplicand: Cube | CubeList, multiplier: Cube | CubeList 

172) -> Cube | CubeList: 

173 """Multiplication of two fields. 

174 

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. 

181 

182 Returns 

183 ------- 

184 Cube | CubeList 

185 The result of multiplicand x multiplier. 

186 

187 Raises 

188 ------ 

189 ValueError 

190 When the cubes are not compatible. 

191 

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). 

197 

198 Examples 

199 -------- 

200 >>> filtered_CAPE_ratio = maths.multiplication(CAPE_ratio, inflow_layer_properties) 

201 

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 

214 

215 

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. 

220 

221 Arguments 

222 --------- 

223 cubes: iris.cube.Cube | iris.cube.CubeList 

224 A Cube or CubeList of a field that is to be differentiated. 

225 

226 coordinate: str 

227 The coordinate that is to be differentiated over. 

228 

229 Returns 

230 ------- 

231 iris.cube.Cube | iris.cube.CubeList 

232 The differential of the cube along the specified coordinate. 

233 

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>`_. 

241 

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