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°
Polar chart with one ring for each of seven airports showing the reported wind direction as an open circle and the GFS direction from three formulas: the blowing-from formula falls next to each report, the blowing-towards formula sits on the opposite side of the compass, and the maths angle lands at unrelated bearings. Polar chart with one ring for each of seven airports showing the reported wind direction as an open circle and the GFS direction from three formulas: the blowing-from formula falls next to each report, the blowing-towards formula sits on the opposite side of the compass, and the maths angle lands at unrelated bearings.
Three formulas against seven airport reports.

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]
Four known winds put through three direction formulas. four test winds, three formulasatan2(v, u)atan2(u, v)atan2(−u, −v)westerly, want 270°✕0°✕90°✓270°southerly, want 180°✕90°✕0°✓180°northerly, want 0°✕270°✕180°✓0°easterly, want 90°✕180°✕270°✓90°Against seven airports: median errors of 101°, 171° and 9° spatialworkflow.io
Four test winds catch every wrong formula.

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

How the meteorological wind direction is built from u and v. from components to a compass bearingu, vtowards east, northreverse: −u,−vwhere it comes fromswap theargumentsatan2(−u, −v): fromnorthdegrees, mod360clockwise bearingmask calmno direction at 0m/sSwapping the arguments turns the maths angle into a bearing from north spatialworkflow.io
From components to a compass bearing.

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

Wind directions that come out wrong: problems and fixes. wrong-direction trapsSymptomFixoff by a constant 180°atan2(−u, −v), not atan2(u, v)right for NW and SE, wrong for NEnot the maths angle atan2(v, u)off by up to 23° on HRRRrotate grid-relative windscalm reported as 180°mask where speed is zeroGFS 10 m wind against seven airports, 00 UTC 27 September 2024 spatialworkflow.io
Wrong-direction traps.
  • Northerly winds: % 360 returns 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 u and v first, or directions are off by up to about 23°.
  • Degrees vs radians: np.arctan2 returns radians; np.degrees before % 360.
  • Averaging: average u and v, then convert; averaging directions directly fails across north.

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°.