How to Map the Jet Stream in Python

On this page

Problem statement

The jet stream is the band of fastest wind near the top of the troposphere, about 10 kilometres up, and its position steers weather systems below it. Maps of it are drawn on a pressure level, usually 250 hPa, from the wind components and the geopotential height there: the height contours show the troughs and ridges, and isotachs of wind speed show the jet and the faster "streaks" within it. Getting there from a model file means picking the right level, keeping the heights in metres, and deciding what speed counts as "jet".

This page maps the GFS analysis at 250 hPa over North America for 00 UTC on 27 September 2024, finds the strongest jet streak, traces the jet axis, and locates the closed low over the eastern United States that sat north of Hurricane Helene's path.

Quick answer

Download UGRD, VGRD and HGT at 250 mb by byte range, compute speed, and contour height over the speed:

w = J.sel(latitude=slice(70, 15), longitude=slice(220, 310))
speed = np.hypot(w.u, w.v)
iy, ix = np.unravel_index(int(np.argmax(speed.values)), speed.shape)
print(f"strongest 250 hPa wind: {float(speed.max()):.1f} m/s ({float(speed.max()) * 1.94384:.0f} kt) at "
      f"{float(w.latitude[iy]):.2f}°N {360 - float(w.longitude[ix]):.2f}°W")
cell_km2 = (111.195 * 0.25) ** 2 * np.cos(np.radians(w.latitude))
print(f"area with winds above 50 m/s: {float(cell_km2.where(speed > 50).sum()):,.0f} km²")
print(f"250 hPa heights: {float(w.gh.min()):,.0f} to {float(w.gh.max()):,.0f} m")
strongest 250 hPa wind: 76.6 m/s (149 kt) at 51.75°N 125.25°W
area with winds above 50 m/s: 1,560,701 km²
250 hPa heights: 9,992 to 11,056 m
Map of North America at 250 hPa from the GFS analysis at 00 UTC on 27 September 2024: wind speed above 30 metres per second shaded, height contours in decametres, the jet axis dashed and the strongest wind starred off British Columbia at 76.6 metres per second; a second jet streak crosses New England and Atlantic Canada, a closed low sits near where Missouri, Kentucky and Tennessee meet, and Helene is marked on the Florida Gulf coast. Map of North America at 250 hPa from the GFS analysis at 00 UTC on 27 September 2024: wind speed above 30 metres per second shaded, height contours in decametres, the jet axis dashed and the strongest wind starred off British Columbia at 76.6 metres per second; a second jet streak crosses New England and Atlantic Canada, a closed low sits near where Missouri, Kentucky and Tennessee meet, and Helene is marked on the Florida Gulf coast.
The jet stream at 250 hPa, with Helene beneath.

One pressure level, the jet axis, the closed low

1. Fetch one pressure level

GFS files hold winds and heights on many pressure levels; the index lines carry the level, so :250 mb: picks exactly the jet level:

lines = requests.get(GFS + ".idx", timeout=60).text.splitlines()
print("\n".join(l for l in lines if ":250 mb:" in l and any(v in l for v in ("UGRD", "VGRD", "HGT"))))
print(f"levels with UGRD in the file: {sum(':UGRD:' in l and ' mb:' in l for l in lines)}")
269:203841899:d=2024092700:HGT:250 mb:anl:
276:209891015:d=2024092700:UGRD:250 mb:anl:
277:210489816:d=2024092700:VGRD:250 mb:anl:
levels with UGRD in the file: 41

2. Trace the jet axis

The jet axis is the line of maximum wind. A simple version takes, for each longitude, the latitude of the fastest wind between 30° and 65°N, where it exceeds 30 m/s:

band = speed.sel(latitude=slice(65, 30))
lat_max = band.idxmax("latitude"); v_max = band.max("latitude")
axis = pd.DataFrame({"lon": band.longitude.values - 360, "lat": lat_max.values, "speed": v_max.values})
axis = axis[axis.speed > 30]
print(f"jet axis found at {len(axis)} of {band.sizes['longitude']} longitudes; latitude range {axis.lat.min()}–{axis.lat.max()}°N")
print(axis.iloc[::40].round(1).to_string(index=False))
jet axis found at 349 of 361 longitudes; latitude range 33.5–61.75°N
   lon  lat     speed
-140.0 47.8 68.500000
-130.0 50.5 67.800003
-120.0 52.2 67.800003
-110.0 51.2 39.400002
-100.0 56.5 45.599998
 -90.0 58.8 33.000000
 -80.0 44.2 51.900002
 -70.0 44.8 72.300003
 -60.0 44.8 43.900002

Where the flow is weak or split, as over the central United States here, the fastest wind in each column jumps between latitudes, and the line zigzags. A higher threshold, or following the axis from one longitude to the next, gives a cleaner line.

Line chart along longitude from 140 to 50 degrees west of the fastest 250 hPa wind between 30 and 65 north, with its latitude on a second axis: above 65 metres per second from 140 to 115 west at about 50 north, dropping below 50 over the central United States where the latitude jumps between 35 and 62 north, then rising to about 74 metres per second around 70 west at 45 north. Line chart along longitude from 140 to 50 degrees west of the fastest 250 hPa wind between 30 and 65 north, with its latitude on a second axis: above 65 metres per second from 140 to 115 west at about 50 north, dropping below 50 over the central United States where the latitude jumps between 35 and 62 north, then rising to about 74 metres per second around 70 west at 45 north.
Two jet streaks and a split flow between them.

3. Find the closed low

A closed low is a minimum of height enclosed by contours. Over the eastern United States the lowest 250 hPa height at this time was:

east = w.gh.sel(latitude=slice(45, 30), longitude=slice(265, 285))
jy, jx = np.unravel_index(int(np.argmin(east.values)), east.shape)
print(f"lowest height {float(east.min()):,.0f} m at {float(east.latitude[jy]):.2f}°N {360 - float(east.longitude[jx]):.2f}°W; "
      f"Helene's best-track centre at 00 UTC: 28.7°N 84.3°W")
lowest height 10,679 m at 36.75°N 90.25°W; Helene's best-track centre at 00 UTC: 28.7°N 84.3°W

Jet speeds in knots for aviation

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: UGRD, VGRD and HGT at 250 mb, opened with cfgrib.

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"
F = Path("data/gfs_250mb.grib2")
if not F.exists():
    lines = requests.get(GFS + ".idx", timeout=60).text.splitlines()
    starts = [int(l.split(":")[1]) for l in lines]
    with open(F, "wb") as f:
        for i, l in enumerate(lines):
            if any(f":{v}:250 mb:" in l for v in ("UGRD", "VGRD", "HGT")):
                f.write(requests.get(GFS, headers={"Range": f"bytes={starts[i]}-{starts[i + 1] - 1}"}, timeout=120).content)
J = xr.open_dataset(F, engine="cfgrib", decode_timedelta=False, backend_kwargs={
    "indexpath": "", "filter_by_keys": {"typeOfLevel": "isobaricInhPa"}}).load()
print(f"variables {list(J.data_vars)} at {float(J.isobaricInhPa):.0f} hPa; gh units: {J.gh.attrs.get('units')}")
variables ['gh', 'u', 'v'] at 250 hPa; gh units: gpm

Example 1: speed in knots for aviation

Aviation charts give jet speeds in knots and mark cores above 80 kt:

kt = speed * 1.94384
print(f"grid points above 80 kt: {int((kt > 80).sum()):,}; above 120 kt: {int((kt > 120).sum()):,}")
grid points above 80 kt: 6,479; above 120 kt: 1,094

Where the jet stream sits, and why

Steps to a 250 hPa jet stream map from a GFS analysis. from a GFS file to a jet map1UGRD, VGRD, HGT at 250 mb3 of 696 messages2speed = hypot(u, v)strongest: 76.6 m/s, 149 kt3shade above 30–50 m/s1.56 million km² above 50 m/s4contour heights in decametres999 to 1,106 dam5trace the axisfastest latitude per longitudeGFS 0.25°, 00 UTC 27 September 2024, North America spatialworkflow.io
From a GFS file to a jet map.

The jet stream sits where the temperature contrast between polar and tropical air is concentrated: the steeper the contrast below, the faster the wind above. On a constant-pressure map, the wind blows nearly parallel to the height contours, faster where they are packed together, which is why the two fields are drawn together. Speeds above about 50 m/s mark the jet; streaks within it, regions of locally faster wind, are where the strongest weather systems tend to develop.

Closed lows at this level are pools of cold air cut off from the main flow. They move slowly and influence the steering of storms beneath and around them. Here the lowest heights over the east lay near where Missouri, Kentucky and Tennessee meet, about 1,000 km north-west of Helene's centre.

Geopotential units and level choice

Mapping the jet stream: problems and fixes. jet map trapsTrapInsteadsurface winds for the jetthe 250 mb lines of the indexheights read as metres on mapsgpm ÷ 10 = decametresarea summed in grid cellsweight by cos(latitude)axis line zigzagshigher threshold, or follow theaxisGFS 250 hPa analysis spatialworkflow.io
Jet map traps.
  • Height units: GFS HGT is geopotential metres; maps often label it in decametres (divide by 10).
  • Level choice: 250 hPa suits mid-latitudes in most seasons; 200 hPa is closer to the jet in the tropics and in summer, 300 hPa in winter.
  • Thresholds: 50 m/s, about 100 kt, is a common jet threshold; aviation uses 80 kt for jet cores.
  • Longitude: GFS longitudes run 0–360; North America is about 190–310.
  • Projection: for hemispheric maps, use a polar stereographic or Lambert projection and rotate the winds.

FAQ

How do I plot the jet stream in Python?

Download u, v and geopotential height at 250 hPa, compute speed with np.hypot(u, v), shade speeds above about 50 m/s and contour the heights.

What pressure level is the jet stream?

Usually drawn at 250 hPa, about 10 to 11 km up; 200 or 300 hPa can be better depending on season and latitude.

How fast is the jet stream?

Typically 50 to 80 m/s in its core; here the strongest wind over North America was 76.6 m/s, about 149 knots.

What is a jet streak?

A region of locally faster wind within the jet stream, often where storms develop.