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

44 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 13:38 +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 

17import iris 

18import iris.cube 

19import numpy as np 

20 

21from CSET._common import iter_maybe 

22 

23 

24def calculate_vector_wind( 

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

26 *, 

27 u_names: tuple[str, ...] = ("x_wind", "eastward_wind", "u", "u_wind"), 

28 v_names: tuple[str, ...] = ("y_wind", "northward_wind", "v", "v_wind"), 

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

30 """ 

31 Calculate wind speed and meteorological wind direction from U/V components. 

32 

33 Notes 

34 ----- 

35 - Speed = sqrt(u^2 + v^2) 

36 - Direction is meteorological "from" direction in degrees, 0..360: 

37 0 = from North, 90 = from East, 180 = from South, 270 = from West 

38 computed as: (atan2(-u, -v) in degrees + 360) % 360 

39 

40 Returns 

41 ------- 

42 If input was a single Cube (not typical for U/V), returns a CubeList. 

43 If input was a CubeList, returns a CubeList containing: 

44 - wind_speed cube 

45 - wind_direction cube 

46 (and optionally any untouched cubes if you choose to keep them; here we return only derived cubes.) 

47 Example 

48 -------- 

49 >>> vector_winds = wind.calculate_vector_wind(winds) 

50 """ 

51 # Normalize input to CubeList 

52 in_list = ( 

53 cubes if isinstance(cubes, iris.cube.CubeList) else iris.cube.CubeList([cubes]) 

54 ) 

55 

56 def _find_by_name( 

57 cubelist: iris.cube.CubeList, names: tuple[str, ...] 

58 ) -> iris.cube.Cube | None: 

59 for nm in names: 

60 matches = cubelist.extract(iris.Constraint(name=nm)) 

61 if matches: 

62 return matches[0] 

63 return None 

64 

65 u_cube = _find_by_name(in_list, u_names) 

66 v_cube = _find_by_name(in_list, v_names) 

67 

68 if u_cube is None or v_cube is None: 

69 available = [c.name() for c in in_list] 

70 raise ValueError( 

71 "calculate_vector_wind needs both U and V component cubes. " 

72 f"Looked for U names {u_names} and V names {v_names}. " 

73 f"Available cube names: {available}" 

74 ) 

75 

76 u = u_cube.core_data() 

77 v = v_cube.core_data() 

78 direction = (np.degrees(np.arctan2(-u, -v)) + 360) % 360 

79 speed = np.sqrt(u**2 + v**2) 

80 speed_cube = u_cube.copy(data=speed) 

81 speed_cube.rename("wind_speed") 

82 speed_cube.units = "m s-1" 

83 direction_cube = u_cube.copy(data=direction) 

84 direction_cube.units = "degrees" 

85 direction_cube.rename("wind_direction") 

86 

87 winds = iris.cube.CubeList([speed_cube, direction_cube]) 

88 if len(winds) == 1: 

89 return winds[0] 

90 else: 

91 return winds 

92 

93 

94def convert_to_beaufort_scale( 

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

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

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

98 

99 Arguments 

100 --------- 

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

102 Cubes of windspeed to be converted. 

103 Required: `wind_speed_at_10m`. 

104 

105 Returns 

106 ------- 

107 iris.cube.Cube | iris.cube.CubeList 

108 Converted windspeed. 

109 

110 Notes 

111 ----- 

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

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

114 

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

116 

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

118 

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

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

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

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

123 

124 .. list-table:: Beaufort Scale 

125 :widths: 5 20 10 10 10 

126 :header-rows: 1 

127 

128 * - Force [1] 

129 - Descriptor 

130 - Windspeed [m/s] 

131 - Windspeed [kn] 

132 - Windspeed [mph] 

133 * - 0 

134 - Calm 

135 - < 0.4 

136 - < 1 

137 - < 1 

138 * - 1 

139 - Light Air 

140 - 0.4 - 1.5 

141 - 1 - 3 

142 - 1 - 3 

143 * - 2 

144 - Light Breeze 

145 - 1.6 - 3.3 

146 - 4 - 6 

147 - 4 - 7 

148 * - 3 

149 - Gentle Breeze 

150 - 3.4 - 5.4 

151 - 7 - 10 

152 - 8 - 12 

153 * - 4 

154 - Moderate Breeze 

155 - 5.5 - 7.9 

156 - 11 - 16 

157 - 13 - 18 

158 * - 5 

159 - Fresh Breeze 

160 - 8.0 - 10.7 

161 - 17 - 21 

162 - 19 - 24 

163 * - 6 

164 - Strong Breeze 

165 - 10.8 - 13.8 

166 - 22 - 27 

167 - 25 - 31 

168 * - 7 

169 - Near Gale 

170 - 13.9 - 17.1 

171 - 28 - 33 

172 - 32 - 38 

173 * - 8 

174 - Gale 

175 - 17.2 - 20.7 

176 - 34 - 40 

177 - 39 - 46 

178 * - 9 

179 - Strong Gale 

180 - 20.8 - 24.4 

181 - 41 - 47 

182 - 47 - 54 

183 * - 10 

184 - Storm 

185 - 24.5 - 28.4 

186 - 48 - 55 

187 - 55 - 63 

188 * - 11 

189 - Violent Storm 

190 - 28.5 - 33.5 

191 - 56 - 63 

192 - 64 - 73 

193 * - 12 (+) 

194 - Hurricane 

195 - > 33.6 

196 - > 64 

197 - > 74 

198 

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

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

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

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

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

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

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

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

207 

208 References 

209 ---------- 

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

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

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

213 of Meteorology. McGraw Hill, 1068 pp. 

214 

215 Examples 

216 -------- 

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

218 """ 

219 # Create and empty cubelist. 

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

221 # Loop over cubelist. 

222 for cube in iter_maybe(cubes): 

223 # Copy cube so we do not overwrite data. 

224 wind_cube = cube.copy() 

225 # Divide data by 0.836. 

226 wind_cube /= 0.836 

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

228 wind_cube.data **= 2.0 / 3.0 

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

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

231 # Convert units. 

232 wind_cube.units = "1" 

233 # Rename cube. 

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

235 winds.append(wind_cube) 

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

237 # as input. 

238 if len(winds) == 1: 

239 return winds[0] 

240 else: 

241 return winds