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
« 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.
15"""Operators to calculate various forms or properties of wind."""
17import iris
18import iris.cube
19import numpy as np
21from CSET._common import iter_maybe
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.
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
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 )
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
65 u_cube = _find_by_name(in_list, u_names)
66 v_cube = _find_by_name(in_list, v_names)
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 )
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")
87 winds = iris.cube.CubeList([speed_cube, direction_cube])
88 if len(winds) == 1:
89 return winds[0]
90 else:
91 return winds
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.
99 Arguments
100 ---------
101 cubes: iris.cube.Cube | iris.cube.CubeList
102 Cubes of windspeed to be converted.
103 Required: `wind_speed_at_10m`.
105 Returns
106 -------
107 iris.cube.Cube | iris.cube.CubeList
108 Converted windspeed.
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]_):
115 .. math:: F = (\frac{v}{0.836})^{2/3}
117 for F the Beaufort Force, and v the windspeed at 10 m in m/s.
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]_).
124 .. list-table:: Beaufort Scale
125 :widths: 5 20 10 10 10
126 :header-rows: 1
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
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.
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.
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