Why Are My Wind Directions Off by 180 Degrees?
On this page
Problem statement
You compute wind direction from a model's u and v, compare it with station reports, and every direction is wrong by about half a turn: the model says the wind blows from the south-west where the stations say north-east. Or the error is stranger, 90° off in one place and right in another. Nothing is wrong with the data. Three conventions are being mixed: the mathematical angle, measured anticlockwise from east; the direction the wind blows towards; and the meteorological direction, the bearing the wind blows from, clockwise from north. np.arctan2 returns the first, and each of the usual one-line formulas gives one of the three.
This page computes direction three ways from the GFS 10-metre wind at 00 UTC on 27 September 2024 at seven airports in Florida and Georgia, as Hurricane Helene approached, and compares each with what the airports reported.
Quick answer
Use atan2(-u, -v): reversing the vector gives where it comes from, and swapping the arguments measures from north, clockwise:
formulas = {
"atan2(v, u) — maths angle": np.degrees(np.arctan2(v, u)) % 360,
"atan2(u, v) — blowing towards": np.degrees(np.arctan2(u, v)) % 360,
"atan2(-u, -v) — blowing from": np.degrees(np.arctan2(-u, -v)) % 360,
}
for name, d in formulas.items():
err = (d - OBS_DIR + 180) % 360 - 180
print(f"{name:<34} median |error| against the airports: {np.median(np.abs(err)):5.0f}°")
atan2(v, u) — maths angle median |error| against the airports: 101°
atan2(u, v) — blowing towards median |error| against the airports: 171°
atan2(-u, -v) — blowing from median |error| against the airports: 9°
Test on known vectors, recognise the pattern, check signs
1. Test the formula on vectors you know
Before trusting any formula, feed it winds whose answer you know. A westerly blows towards the east, so u > 0, v = 0, and its direction is 270°:
tests = {"westerly": (5.0, 0.0, 270), "southerly": (0.0, 5.0, 180), "northerly": (0.0, -5.0, 0), "easterly": (-5.0, 0.0, 90)}
for name, (tu, tv, want) in tests.items():
got = {k.split("—")[0].strip(): round(float(np.degrees(f(tu, tv)) % 360)) for k, f in
{"atan2(v, u)": lambda a, b: np.arctan2(b, a), "atan2(u, v)": lambda a, b: np.arctan2(a, b),
"atan2(-u, -v)": lambda a, b: np.arctan2(-a, -b)}.items()}
print(f"{name:>9} (want {want:3d}°): {got}")
westerly (want 270°): {'atan2(v, u)': 0, 'atan2(u, v)': 90, 'atan2(-u, -v)': 270}
southerly (want 180°): {'atan2(v, u)': 90, 'atan2(u, v)': 0, 'atan2(-u, -v)': 180}
northerly (want 0°): {'atan2(v, u)': 270, 'atan2(u, v)': 180, 'atan2(-u, -v)': 0}
easterly (want 90°): {'atan2(v, u)': 180, 'atan2(u, v)': 270, 'atan2(-u, -v)': 90}
2. Recognise the error pattern
Each wrong formula leaves a signature in the errors. "Towards" is off by a constant 180°; the maths angle is off by an amount that depends on the direction itself, because it is reflected and rotated:
for name in list(formulas)[:2]:
err = (formulas[name] - OBS_DIR + 180) % 360 - 180
print(f"{name:<34} errors: {np.round(err).astype(int).tolist()}")
atan2(v, u) — maths angle errors: [23, -60, -168, 103, -101, 34, -126]
atan2(u, v) — blowing towards errors: [-173, -170, 178, -173, 171, -164, -164]
3. Check the sign of u and v themselves
If the formula is right and directions are still reversed, the components may be: some datasets store the direction the wind blows towards, or ocean-style currents, with the same names. Compare against stations once:
print(f"GFS u10 attributes: {dict((k, v) for k, v in W.u10.attrs.items() if k in ('long_name', 'units', 'GRIB_shortName'))}")
GFS u10 attributes: {'GRIB_shortName': '10u', 'long_name': '10 metre U wind component', 'units': 'm s**-1'}
A function that cannot be called the wrong way
The data used on this page
The GFS 0.25° analysis for 00 UTC on 27 September 2024 from the NOAA GFS bucket on AWS, interpolated to seven airports, and the routine hourly reports from those airports nearest 00 UTC in NOAA's Integrated Surface Database.
from pathlib import Path
import numpy as np
import pandas as pd
import requests
import xarray as xr
Path("data").mkdir(exist_ok=True)
GFS = "https://noaa-gfs-bdp-pds.s3.amazonaws.com/gfs.20240927/00/atmos/gfs.t00z.pgrb2.0p25.f000"
G = Path("data/gfs_helene.grib2")
if not G.exists():
lines = requests.get(GFS + ".idx", timeout=60).text.splitlines()
starts = [int(l.split(":")[1]) for l in lines]
with open(G, "wb") as f:
for i, l in enumerate(lines):
if ":UGRD:10 m above ground:" in l or ":VGRD:10 m above ground:" in l:
f.write(requests.get(GFS, headers={"Range": f"bytes={starts[i]}-{starts[i + 1] - 1}"}, timeout=120).content)
W = xr.open_dataset(G, engine="cfgrib", decode_timedelta=False, backend_kwargs={
"indexpath": "", "filter_by_keys": {"typeOfLevel": "heightAboveGround", "level": 10}}).load()
IDS = {"KJAX": "72206013889", "KTPA": "72211012842", "KTLH": "72214093805", "KVLD": "72216693845",
"KAAF": "72220012832", "KGNV": "74756012816", "KMAI": "74776003818"}
rows = []
for icao, sid in IDS.items():
f = Path(f"data/{sid}.csv")
if not f.exists():
f.write_bytes(requests.get(f"https://noaa-global-hourly-pds.s3.amazonaws.com/2024/{sid}.csv", timeout=300).content)
d = pd.read_csv(f, usecols=["DATE", "REPORT_TYPE", "LATITUDE", "LONGITUDE", "WND"], dtype=str)
d = d[d.REPORT_TYPE.str.strip() == "FM-15"]
d["gap"] = (pd.to_datetime(d.DATE) - pd.Timestamp("2024-09-27 00:00")).abs()
r = d.loc[d.gap.idxmin()]
rows.append((icao, float(r.LATITUDE), float(r.LONGITUDE), float(r.WND.split(",")[0])))
ST = pd.DataFrame(rows, columns=["icao", "lat", "lon", "obs_dir"]).set_index("icao")
at = W.interp(latitude=xr.DataArray(ST.lat, dims="s"), longitude=xr.DataArray(ST.lon % 360, dims="s"))
u, v, OBS_DIR = at.u10.values, at.v10.values, ST.obs_dir.values
print(f"{len(ST)} airports; observed directions {OBS_DIR.astype(int).tolist()}")
7 airports; observed directions [120, 160, 40, 80, 10, 110, 10]
Example 1: a function that cannot be called the wrong way
Wrap the convention once, name it after what it returns, and test it:
def direction_from(u, v):
"""Meteorological wind direction: the bearing the wind blows FROM, clockwise from north, in degrees."""
d = np.degrees(np.arctan2(-u, -v)) % 360
return np.where(np.hypot(u, v) > 0, d, np.nan)
assert direction_from(5.0, 0.0) == 270 and direction_from(0.0, 5.0) == 180
print("direction_from passes; GFS at the airports:", np.round(direction_from(u, v)).astype(int).tolist())
direction_from passes; GFS at the airports: [127, 170, 38, 87, 1, 126, 26]
Mathematics and meteorology measure angles differently
np.arctan2(y, x) is the angle of the point (x, y) measured anticlockwise from the positive x axis, the mathematician's convention. Meteorology measures clockwise from north, which swaps the roles of the axes, and names the wind after its origin, which reverses the vector. So arctan2(u, v) gives the compass bearing the wind blows towards, and negating both components gives the bearing it comes from. Oceanographers name currents after where they flow to, so the "towards" formula is right for them; the same code applied to wind is exactly 180° out.
The maths-angle mistake is harder to spot, because for winds from the north-west or the south-east it gives exactly the right answer, by coincidence, and for winds from the north-east or south-west exactly the opposite. Testing with four known winds, one from each quadrant, catches all of these in seconds.
Northerlies, calm and radians
- Northerly winds:
% 360returns 0° for a northerly; aviation reports 360° and keeps 0° for calm. - Calm: with both components zero,
arctan2(-0.0, -0.0)returns 180°; mask calm before reporting. - Grid-relative winds: on HRRR's Lambert grid, rotate
uandvfirst, or directions are off by up to about 23°. - Degrees vs radians:
np.arctan2returns radians;np.degreesbefore% 360. - Averaging: average
uandv, then convert; averaging directions directly fails across north.
Internal links
- How to calculate wind speed and direction from u and v — the full conversion
- How to read HRRR wind data in Python — grid-relative winds
FAQ
Why is my wind direction 180 degrees off?
You computed the direction the wind blows towards, arctan2(u, v); meteorological direction is where it blows from, arctan2(-u, -v).
What angle does numpy arctan2 return?
The angle anticlockwise from the positive x axis, in radians, from −π to π; it is not a compass bearing.
Is wind direction where the wind is going or coming from?
Coming from: a westerly wind blows from the west, direction 270°.
How do I test a wind direction formula?
Feed it a westerly (u > 0, v = 0) and a southerly (u = 0, v > 0); the answers must be 270° and 180°.