Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Plot an 500-hPa map with calculating vorticity advection using MetPy calculations.

Beyond just plotting 500-hPa level data, this uses calculations from metpy.calc to find the vorticity and vorticity advection. Currently, this needs an extra helper function to calculate the distance between lat/lon grid points.

Imports

from datetime import datetime

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
import metpy.calc as mpcalc
from metpy.units import units
import numpy as np
from siphon.catalog import TDSCatalog
from xarray.backends import NetCDF4DataStore
import xarray as xr

Function to Compute Earth-Relative Winds

This function takes a data array with relevant information about the projection of the wind component data, along with the grid-relative components of the wind. It outputs the earth-relative components of the wind.

def earth_relative_wind_components(ugrd, vgrd):
    """Calculate the north-relative components of the
    wind from the grid-relative components using Cartopy
    transform_vectors.

    Parameters
    ----------
        ugrd : Xarray DataArray (M, N)
            grid relative u-component of the wind
        vgrd : Xarray DataArray (M, N)
            grid relative v-component of the wind

    Returns
    -------
        unr, vnr : tuple of array-like Quantity
            The north-relative wind components in the X (East-West)
            and Y (North-South) directions, respectively.
    """
    if 'metpy_crs' not in ugrd.coords:
        raise ValueError('No CRS in coordinate, be sure to use'
                         'the MetPy accessor parse_cf()')

    data_crs = ugrd.metpy.cartopy_crs

    x = ugrd.x.values
    y = ugrd.y.values

    xx, yy = np.meshgrid(x, y)

    ut, vt = ccrs.PlateCarree().transform_vectors(data_crs, xx, yy,
                                                  ugrd.values, vgrd.values)

    # Make a copy of u and v component DataArrays
    uer = ugrd.copy()
    ver = vgrd.copy()

    # Update values with transformed winds
    uer.values = ut
    ver.values = vt

    return uer, ver

Data Aquisition

dt = datetime(2016, 4, 16, 18)

# Assemble our URL to the THREDDS Data Server catalog,
# and access our desired dataset within via NCSS
base_url = 'https://www.ncei.noaa.gov/thredds/catalog/model-namanl-old/'
cat = TDSCatalog(f'{base_url}{dt:%Y%m}/{dt:%Y%m%d}/catalog.xml')
ncss = cat.datasets[f'namanl_218_{dt:%Y%m%d}_{dt:%H}00_000.grb'].subset()

# Query for Latest GFS Run
query = ncss.query()

query.time(dt)
query.accept('netcdf')
query.variables('Geopotential_height_isobaric',
                'u-component_of_wind_isobaric',
                'v-component_of_wind_isobaric')

# Obtain our queried data
data = ncss.get_data(query)
ds_data = xr.open_dataset(NetCDF4DataStore(data)).metpy.parse_cf()
ds = ds_data.metpy.assign_latitude_longitude()

times = ds.Geopotential_height_isobaric.metpy.time
vtime = times.values.squeeze().astype('datetime64[ms]').astype('O')

lev_500 = 500 * units.hPa

hght_500 = ds.Geopotential_height_isobaric.metpy.sel(
    vertical=lev_500).squeeze()
hght_500 = mpcalc.smooth_gaussian(hght_500, 4)

uwnd_500 = ds['u-component_of_wind_isobaric'].metpy.sel(
    vertical=lev_500).squeeze()
vwnd_500 = ds['v-component_of_wind_isobaric'].metpy.sel(
    vertical=lev_500).squeeze()

# Compute north-relative wind components for calculation purposes
uwnd_500er, vwnd_500er = earth_relative_wind_components(uwnd_500, vwnd_500)
---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
Cell In[3], line 7
      5 base_url = 'https://www.ncei.noaa.gov/thredds/catalog/model-namanl-old/'
      6 cat = TDSCatalog(f'{base_url}{dt:%Y%m}/{dt:%Y%m%d}/catalog.xml')
----> 7 ncss = cat.datasets[f'namanl_218_{dt:%Y%m%d}_{dt:%H}00_000.grb'].subset()
      9 # Query for Latest GFS Run
     10 query = ncss.query()

File ~/micromamba/envs/metpy-cookbook/lib/python3.14/site-packages/siphon/catalog.py:707, in Dataset.subset(self, service)
    703 elif service not in self.ncss_service_names:
    704     raise ValueError(service + ' is not a valid service for subset. Options are: '
    705                      + ', '.join(self.ncss_service_names))
--> 707 return self.access_with_service(service)

File ~/micromamba/envs/metpy-cookbook/lib/python3.14/site-packages/siphon/catalog.py:763, in Dataset.access_with_service(self, service, use_xarray)
    760     raise ValueError(service + ' is not an access method supported by Siphon')
    762 try:
--> 763     return provider(self.access_urls[service])
    764 except KeyError:
    765     raise ValueError(service + ' is not available for this dataset') from None

File ~/micromamba/envs/metpy-cookbook/lib/python3.14/site-packages/siphon/http_util.py:383, in HTTPEndPoint.__init__(self, url)
    381 self._base = url
    382 self._session = session_manager.create_session()
--> 383 self._get_metadata()

File ~/micromamba/envs/metpy-cookbook/lib/python3.14/site-packages/siphon/ncss.py:57, in NCSS._get_metadata(self)
     55 def _get_metadata(self):
     56     # Need to use .content here to avoid decode problems
---> 57     meta_xml = self.get_path('dataset.xml').content
     58     root = ET.fromstring(meta_xml)
     59     self.metadata = NCSSDataset(root)

File ~/micromamba/envs/metpy-cookbook/lib/python3.14/site-packages/siphon/http_util.py:453, in HTTPEndPoint.get_path(self, path, query)
    430 def get_path(self, path, query=None):
    431     """Make a GET request, optionally including a query, to a relative path.
    432 
    433     The path of the request includes a path on top of the base URL
   (...)    451 
    452     """
--> 453     return self.get(self.url_path(path), query)

File ~/micromamba/envs/metpy-cookbook/lib/python3.14/site-packages/siphon/http_util.py:488, in HTTPEndPoint.get(self, path, params)
    486     else:
    487         text = resp.text
--> 488     raise requests.HTTPError(f'Error accessing {resp.request.url}\n'
    489                              f'Server Error ({resp.status_code: d}: {text})')
    490 return resp

HTTPError: Error accessing https://www.ncei.noaa.gov/thredds/ncss/grid/model-namanl-old/201604/20160416/namanl_218_20160416_1800_000.grb/dataset.xml
Server Error ( 404: FileNotFound: No such file or directory)

Begin Data Calculations

avor = mpcalc.vorticity(uwnd_500er, vwnd_500er)

avor = mpcalc.smooth_n_point(avor, 9, 10) * 1e5

vort_adv = mpcalc.advection(avor, uwnd_500er, vwnd_500er) * 1e4

Map Creation

# Set up Coordinate System for Plot and Transforms
datacrs = ds.Geopotential_height_isobaric.metpy.cartopy_crs
plotcrs = ccrs.LambertConformal(central_latitude=45., central_longitude=-100.,
                                standard_parallels=[30, 60])

fig = plt.figure(1, figsize=(12., 14.))
ax = plt.subplot(111, projection=plotcrs)

# Plot Titles
plt.title(r'500-hPa Heights (m), AVOR$*10^5$ ($s^{-1}$),'
          'AVOR Adv$*10^9$ ($s^{-2}$)', loc='left')
plt.title(f'VALID: {vtime}', loc='right')

# Plot Background
ax.set_extent([235., 290., 20., 58.], ccrs.PlateCarree())
ax.coastlines('50m', edgecolor='black', linewidth=0.75)
ax.add_feature(cfeature.STATES, linewidth=.5)

# Plot Height Contours
clev500 = np.arange(5100, 6061, 60)
cs = ax.contour(hght_500.longitude, hght_500.latitude,
                hght_500, clev500,
                colors='black', linewidths=1.0,
                linestyles='solid', transform=ccrs.PlateCarree())
plt.clabel(cs, fontsize=10, inline=1, inline_spacing=10, fmt='%i',
           rightside_up=True, use_clabeltext=True)

# Plot Absolute Vorticity Contours
clevvort500 = np.arange(-9, 50, 5)
cs2 = ax.contour(avor.longitude, avor.latitude,
                 avor, clevvort500,
                 colors='grey', linewidths=1.25, linestyles='dashed',
                 transform=ccrs.PlateCarree())
plt.clabel(cs2, fontsize=10, inline=1, inline_spacing=10, fmt='%i',
           rightside_up=True, use_clabeltext=True)

# Plot Colorfill of Vorticity Advection
clev_avoradv = np.arange(-30, 31, 5)
cf = ax.contourf(vort_adv.longitude, vort_adv.latitude, vort_adv,
                 clev_avoradv[clev_avoradv != 0], extend='both',
                 cmap='bwr', transform=ccrs.PlateCarree())
cb = plt.colorbar(cf, orientation='horizontal', pad=0, aspect=50,
                  extendrect='True', ticks=clev_avoradv)
cb.set_label(r'$1/s^2$', size='large')

# Plot Wind Barbs
# Transform Vectors and plot wind barbs.
wind_slice = (slice(None, None, 20), slice(None, None, 20))
xx, yy = np.meshgrid(uwnd_500.x.values[wind_slice[0]],
                     uwnd_500.y.values[wind_slice[0]])
ax.barbs(xx, yy, uwnd_500.values[wind_slice], vwnd_500.values[wind_slice],
         length=6, pivot='middle', transform=datacrs)