Coverage for src/CSET/operators/wind.py: 88%

47 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-02 14:24 +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"""Operators to calculate various forms or properties of wind.""" 

16 

17from __future__ import annotations 

18 

19import logging 

20 

21import iris 

22import numpy as np 

23 

24from CSET._common import iter_maybe 

25from CSET.operators._utils import get_cube_yxcoordname 

26from CSET.operators.regrid import regrid_onto_cube 

27 

28 

29def calculate_vector_wind( 

30 u: iris.cube.Cube | iris.cube.CubeList, 

31 v: iris.cube.Cube | iris.cube.CubeList, 

32) -> iris.cube.CubeList: 

33 """ 

34 

35 Calculate wind speed and wind-from direction from U and V components. 

36 

37 Parameters 

38 ---------- 

39 u : iris.cube.Cube or iris.cube.CubeList 

40 Zonal (eastward) wind component(s). If a CubeList is provided, 

41 it must contain one cube per model. 

42 

43 v : iris.cube.Cube or iris.cube.CubeList 

44 Meridional (northward) wind component(s). Must correspond 

45 one-to-one with `u`. 

46 

47 Returns 

48 ------- 

49 iris.cube.CubeList 

50 CubeList containing, for each (u, v) pair: 

51 - wind_speed cube 

52 - wind_direction cube 

53 

54 Notes 

55 ----- 

56 - Pairs U and V cubes using zip(..., strict=True) 

57 - Regrids U onto V grid if coordinate shapes differ 

58 - Speed = np.hypot(u, v) 

59 - Direction is meteorological "from" direction: 

60 (atan2(-u, -v) + 360) % 360 

61 """ 

62 out = iris.cube.CubeList() 

63 

64 u_list = list(iter_maybe(u)) 

65 v_list = list(iter_maybe(v)) 

66 

67 if not u_list or not v_list: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true

68 raise ValueError("Need at least one U cube and one V cube") 

69 

70 for u_cube, v_cube in zip(iter_maybe(u), iter_maybe(v), strict=True): 

71 # Ensure cubes to compare are on common differencing grid. 

72 # This is triggered if either 

73 # i) latitude and longitude shapes are not the same. Note grid points 

74 # are not compared directly as these can differ through rounding 

75 # errors. 

76 # ii) or variables are known to often sit on different grid staggering 

77 # in different models (e.g. cell center vs cell edge), as is the case 

78 # for UM and LFRic comparisons. 

79 # In future greater choice of regridding method might be applied depending 

80 # on variable type. Linear regridding can in general be appropriate for smooth 

81 # variables. Care should be taken with interpretation of differences 

82 # given this dependency on regridding. 

83 u_lat, u_lon = get_cube_yxcoordname(u_cube) 

84 v_lat, v_lon = get_cube_yxcoordname(v_cube) 

85 

86 if ( 86 ↛ 90line 86 didn't jump to line 90 because the condition on line 86 was never true

87 u_cube.coord(u_lat).shape != v_cube.coord(v_lat).shape 

88 or u_cube.coord(u_lon).shape != v_cube.coord(v_lon).shape 

89 ): 

90 logging.debug( 

91 "Regridding U cube onto V cube grid for vector wind calculation" 

92 ) 

93 u_cube = regrid_onto_cube(u_cube, v_cube, method="Linear") 

94 

95 # --- optional: sanity check units --- 

96 if u_cube.units != v_cube.units: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 raise ValueError("U and V cubes must have the same units") 

98 

99 # --- compute vector wind --- 

100 u_data = u_cube.data 

101 v_data = v_cube.data 

102 

103 speed = np.hypot(u_data, v_data) 

104 direction = (np.degrees(np.arctan2(-u_data, -v_data)) + 360) % 360 

105 

106 speed_cube = u_cube.copy(data=speed) 

107 speed_cube.rename("wind_speed") 

108 speed_cube.units = u_cube.units 

109 

110 direction_cube = u_cube.copy(data=direction) 

111 direction_cube.rename("wind_direction") 

112 direction_cube.units = "degrees" 

113 direction_cube.standard_name = "wind_from_direction" 

114 

115 out.extend([speed_cube, direction_cube]) 

116 

117 return out 

118 

119 

120def convert_to_beaufort_scale( 

121 cubes: iris.cube.Cube | iris.cube.CubeList, 

122) -> iris.cube.Cube | iris.cube.CubeList: 

123 r"""Convert windspeed from m/s to the Beaufort Scale. 

124 

125 Arguments 

126 --------- 

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

128 Cubes of windspeed to be converted. 

129 Required: `wind_speed_at_10m`. 

130 

131 Returns 

132 ------- 

133 iris.cube.Cube | iris.cube.CubeList 

134 Converted windspeed. 

135 

136 Notes 

137 ----- 

138 The relationship used to convert the windspeed from m/s to the Beaufort 

139 Scale is an empirical relationship (e.g., [Beer96]_): 

140 

141 .. math:: F = \left(\frac{v}{0.836}\right)^{2/3} 

142 

143 for F the Beaufort Force, and v the windspeed at 10 m in m/s. 

144 

145 The Beaufort Scale was devised in 1805 by Rear Admiral Sir Francis Beaufort. 

146 It is a widely used windscale that categorises the winds into forces and provides 

147 human-understable names (e.g. gale). The table below shows the Beaufort Scale based 

148 on the Handbook of Meteorology ([Berryetal45]_). 

149 

150 .. list-table:: Beaufort Scale 

151 :widths: 5 20 10 10 10 

152 :header-rows: 1 

153 

154 * - Force [1] 

155 - Descriptor 

156 - Windspeed [m/s] 

157 - Windspeed [kn] 

158 - Windspeed [mph] 

159 * - 0 

160 - Calm 

161 - < 0.4 

162 - < 1 

163 - < 1 

164 * - 1 

165 - Light Air 

166 - 0.4 - 1.5 

167 - 1 - 3 

168 - 1 - 3 

169 * - 2 

170 - Light Breeze 

171 - 1.6 - 3.3 

172 - 4 - 6 

173 - 4 - 7 

174 * - 3 

175 - Gentle Breeze 

176 - 3.4 - 5.4 

177 - 7 - 10 

178 - 8 - 12 

179 * - 4 

180 - Moderate Breeze 

181 - 5.5 - 7.9 

182 - 11 - 16 

183 - 13 - 18 

184 * - 5 

185 - Fresh Breeze 

186 - 8.0 - 10.7 

187 - 17 - 21 

188 - 19 - 24 

189 * - 6 

190 - Strong Breeze 

191 - 10.8 - 13.8 

192 - 22 - 27 

193 - 25 - 31 

194 * - 7 

195 - Near Gale 

196 - 13.9 - 17.1 

197 - 28 - 33 

198 - 32 - 38 

199 * - 8 

200 - Gale 

201 - 17.2 - 20.7 

202 - 34 - 40 

203 - 39 - 46 

204 * - 9 

205 - Strong Gale 

206 - 20.8 - 24.4 

207 - 41 - 47 

208 - 47 - 54 

209 * - 10 

210 - Storm 

211 - 24.5 - 28.4 

212 - 48 - 55 

213 - 55 - 63 

214 * - 11 

215 - Violent Storm 

216 - 28.5 - 33.5 

217 - 56 - 63 

218 - 64 - 73 

219 * - 12 (+) 

220 - Hurricane 

221 - > 33.6 

222 - > 64 

223 - > 74 

224 

225 The modern names have been used in this table. However, it should be noted 

226 for historical accuracy that Force 7 was originally "Moderate Gale", Force 8 

227 was originally "Fresh Gale", Force 10 was originally "Whole Gale", and 

228 Force 11 was originally "Storm". Force 9 can also be referred to as 

229 "Severe Gale". Furthermore, it should be noted that there is an extended 

230 Beaufort Scale, sometimes used for tropical cyclones. Hence, why values 

231 can reach above 12 in this diagnostic. However, these are not referred to 

232 in the table as anything above F12 is labelled as Hurricane force. 

233 

234 References 

235 ---------- 

236 .. [Beer96] Beer, T. (1996) Environmental Oceanography, CRC Marince Science, 

237 Vol. 11, 2nd Edition, CRC Press, 402 pp. 

238 .. [Berryetal45] Berry, F. A., Jr., E. Bollay, and N. R. Beers, (1945) Handbook 

239 of Meteorology. McGraw Hill, 1068 pp. 

240 

241 Examples 

242 -------- 

243 >>> Beaufort_Scale=wind.convert_to_Beaufort_scale(winds) 

244 """ 

245 # Create and empty cubelist. 

246 winds = iris.cube.CubeList([]) 

247 # Loop over cubelist. 

248 for cube in iter_maybe(cubes): 

249 # Copy cube so we do not overwrite data. 

250 wind_cube = cube.copy() 

251 # Divide data by 0.836. 

252 wind_cube /= 0.836 

253 # Raise to power of 2/3 to produce decimal Beaufort Scale. 

254 wind_cube.data **= 2.0 / 3.0 

255 # Round using even round (i.e. to nearest even number). 

256 wind_cube.data = np.round(wind_cube.data) 

257 # Convert units. 

258 wind_cube.units = "1" 

259 # Rename cube. 

260 wind_cube.rename(f"{cube.name()}_on_Beaufort_Scale") 

261 winds.append(wind_cube) 

262 # Output as single cube or cubelist depending on if cube of cubelist given 

263 # as input. 

264 if len(winds) == 1: 

265 return winds[0] 

266 else: 

267 return winds