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

45 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-28 14:35 +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 for u_cube, v_cube in zip(iter_maybe(u), iter_maybe(v), strict=True): 

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

66 # This is triggered if either 

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

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

69 # errors. 

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

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

72 # for UM and LFRic comparisons. 

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

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

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

76 # given this dependency on regridding. 

77 

78 u_lat, u_lon = get_cube_yxcoordname(u_cube) 

79 v_lat, v_lon = get_cube_yxcoordname(v_cube) 

80 

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

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

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

84 ): 

85 logging.debug( 

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

87 ) 

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

89 

90 # --- optional: sanity check units --- 

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

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

93 

94 # --- compute vector wind --- 

95 u_data = u_cube.data 

96 v_data = v_cube.data 

97 

98 speed = np.hypot(u_data, v_data) 

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

100 

101 speed_cube = u_cube.copy(data=speed) 

102 speed_cube.rename("wind_speed") 

103 speed_cube.units = u_cube.units 

104 direction_cube = u_cube.copy(data=direction) 

105 direction_cube.standard_name = None 

106 direction_cube.long_name = None 

107 

108 direction_cube.standard_name = "wind_from_direction" 

109 direction_cube.units = "degrees" 

110 direction_cube.long_name = "wind direction" 

111 

112 out.extend([speed_cube, direction_cube]) 

113 

114 return out 

115 

116 

117def convert_to_beaufort_scale( 

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

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

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

121 

122 Arguments 

123 --------- 

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

125 Cubes of windspeed to be converted. 

126 Required: `wind_speed_at_10m`. 

127 

128 Returns 

129 ------- 

130 iris.cube.Cube | iris.cube.CubeList 

131 Converted windspeed. 

132 

133 Notes 

134 ----- 

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

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

137 

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

139 

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

141 

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

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

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

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

146 

147 .. list-table:: Beaufort Scale 

148 :widths: 5 20 10 10 10 

149 :header-rows: 1 

150 

151 * - Force [1] 

152 - Descriptor 

153 - Windspeed [m/s] 

154 - Windspeed [kn] 

155 - Windspeed [mph] 

156 * - 0 

157 - Calm 

158 - < 0.4 

159 - < 1 

160 - < 1 

161 * - 1 

162 - Light Air 

163 - 0.4 - 1.5 

164 - 1 - 3 

165 - 1 - 3 

166 * - 2 

167 - Light Breeze 

168 - 1.6 - 3.3 

169 - 4 - 6 

170 - 4 - 7 

171 * - 3 

172 - Gentle Breeze 

173 - 3.4 - 5.4 

174 - 7 - 10 

175 - 8 - 12 

176 * - 4 

177 - Moderate Breeze 

178 - 5.5 - 7.9 

179 - 11 - 16 

180 - 13 - 18 

181 * - 5 

182 - Fresh Breeze 

183 - 8.0 - 10.7 

184 - 17 - 21 

185 - 19 - 24 

186 * - 6 

187 - Strong Breeze 

188 - 10.8 - 13.8 

189 - 22 - 27 

190 - 25 - 31 

191 * - 7 

192 - Near Gale 

193 - 13.9 - 17.1 

194 - 28 - 33 

195 - 32 - 38 

196 * - 8 

197 - Gale 

198 - 17.2 - 20.7 

199 - 34 - 40 

200 - 39 - 46 

201 * - 9 

202 - Strong Gale 

203 - 20.8 - 24.4 

204 - 41 - 47 

205 - 47 - 54 

206 * - 10 

207 - Storm 

208 - 24.5 - 28.4 

209 - 48 - 55 

210 - 55 - 63 

211 * - 11 

212 - Violent Storm 

213 - 28.5 - 33.5 

214 - 56 - 63 

215 - 64 - 73 

216 * - 12 (+) 

217 - Hurricane 

218 - > 33.6 

219 - > 64 

220 - > 74 

221 

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

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

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

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

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

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

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

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

230 

231 References 

232 ---------- 

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

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

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

236 of Meteorology. McGraw Hill, 1068 pp. 

237 

238 Examples 

239 -------- 

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

241 """ 

242 # Create and empty cubelist. 

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

244 # Loop over cubelist. 

245 for cube in iter_maybe(cubes): 

246 # Copy cube so we do not overwrite data. 

247 wind_cube = cube.copy() 

248 # Divide data by 0.836. 

249 wind_cube /= 0.836 

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

251 wind_cube.data **= 2.0 / 3.0 

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

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

254 # Convert units. 

255 wind_cube.units = "1" 

256 # Rename cube. 

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

258 winds.append(wind_cube) 

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

260 # as input. 

261 if len(winds) == 1: 

262 return winds[0] 

263 else: 

264 return winds