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

46 statements  

« prev     ^ index     » next       coverage.py v7.15.3, created at 2026-08-04 08:32 +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 

28logger = logging.getLogger(__name__) 

29 

30 

31def calculate_vector_wind( 

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

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

34) -> iris.cube.CubeList: 

35 """ 

36 

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

38 

39 Parameters 

40 ---------- 

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

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

43 it must contain one cube per model. 

44 

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

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

47 one-to-one with `u`. 

48 

49 Returns 

50 ------- 

51 iris.cube.CubeList 

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

53 - wind_speed cube 

54 - wind_direction cube 

55 

56 Notes 

57 ----- 

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

59 - Regrids U onto V grid if coordinate shapes differ. 

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

61 - Direction is meteorological "from" direction: 

62 (atan2(-u, -v) + 360) % 360. 

63 """ 

64 out = iris.cube.CubeList() 

65 

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

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

68 # This is triggered if either 

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

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

71 # errors. 

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

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

74 # for UM and LFRic comparisons. 

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

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

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

78 # given this dependency on regridding. 

79 

80 u_lat, u_lon = get_cube_yxcoordname(u_cube) 

81 v_lat, v_lon = get_cube_yxcoordname(v_cube) 

82 

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

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

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

86 ): 

87 logger.debug( 

88 "Regridding U cube onto V cube grid for vector wind calculation." 

89 ) 

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

91 

92 # Check units. 

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

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

95 

96 # Compute vector wind. 

97 u_data = u_cube.data 

98 v_data = v_cube.data 

99 

100 speed = np.hypot(u_data, v_data) 

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

102 

103 speed_cube = u_cube.copy(data=speed) 

104 speed_cube.rename("wind_speed") 

105 speed_cube.units = u_cube.units 

106 direction_cube = u_cube.copy(data=direction) 

107 direction_cube.standard_name = None 

108 direction_cube.long_name = None 

109 

110 direction_cube.standard_name = "wind_from_direction" 

111 direction_cube.units = "degrees" 

112 direction_cube.long_name = "wind direction" 

113 

114 out.extend([speed_cube, direction_cube]) 

115 

116 return out 

117 

118 

119def convert_to_beaufort_scale( 

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

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

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

123 

124 Arguments 

125 --------- 

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

127 Cubes of windspeed to be converted. 

128 Required: `wind_speed_at_10m`. 

129 

130 Returns 

131 ------- 

132 iris.cube.Cube | iris.cube.CubeList 

133 Converted windspeed. 

134 

135 Notes 

136 ----- 

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

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

139 

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

141 

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

143 

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

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

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

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

148 

149 .. list-table:: Beaufort Scale 

150 :widths: 5 20 10 10 10 

151 :header-rows: 1 

152 

153 * - Force [1] 

154 - Descriptor 

155 - Windspeed [m/s] 

156 - Windspeed [kn] 

157 - Windspeed [mph] 

158 * - 0 

159 - Calm 

160 - < 0.4 

161 - < 1 

162 - < 1 

163 * - 1 

164 - Light Air 

165 - 0.4 - 1.5 

166 - 1 - 3 

167 - 1 - 3 

168 * - 2 

169 - Light Breeze 

170 - 1.6 - 3.3 

171 - 4 - 6 

172 - 4 - 7 

173 * - 3 

174 - Gentle Breeze 

175 - 3.4 - 5.4 

176 - 7 - 10 

177 - 8 - 12 

178 * - 4 

179 - Moderate Breeze 

180 - 5.5 - 7.9 

181 - 11 - 16 

182 - 13 - 18 

183 * - 5 

184 - Fresh Breeze 

185 - 8.0 - 10.7 

186 - 17 - 21 

187 - 19 - 24 

188 * - 6 

189 - Strong Breeze 

190 - 10.8 - 13.8 

191 - 22 - 27 

192 - 25 - 31 

193 * - 7 

194 - Near Gale 

195 - 13.9 - 17.1 

196 - 28 - 33 

197 - 32 - 38 

198 * - 8 

199 - Gale 

200 - 17.2 - 20.7 

201 - 34 - 40 

202 - 39 - 46 

203 * - 9 

204 - Strong Gale 

205 - 20.8 - 24.4 

206 - 41 - 47 

207 - 47 - 54 

208 * - 10 

209 - Storm 

210 - 24.5 - 28.4 

211 - 48 - 55 

212 - 55 - 63 

213 * - 11 

214 - Violent Storm 

215 - 28.5 - 33.5 

216 - 56 - 63 

217 - 64 - 73 

218 * - 12 (+) 

219 - Hurricane 

220 - > 33.6 

221 - > 64 

222 - > 74 

223 

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

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

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

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

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

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

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

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

232 

233 References 

234 ---------- 

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

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

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

238 of Meteorology. McGraw Hill, 1068 pp. 

239 

240 Examples 

241 -------- 

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

243 """ 

244 # Create and empty cubelist. 

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

246 # Loop over cubelist. 

247 for cube in iter_maybe(cubes): 

248 # Copy cube so we do not overwrite data. 

249 wind_cube = cube.copy() 

250 # Divide data by 0.836. 

251 wind_cube /= 0.836 

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

253 wind_cube.data **= 2.0 / 3.0 

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

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

256 # Convert units. 

257 wind_cube.units = "1" 

258 # Rename cube. 

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

260 winds.append(wind_cube) 

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

262 # as input. 

263 if len(winds) == 1: 

264 return winds[0] 

265 else: 

266 return winds