Meteorology in Python
North American Regional Reanalysis (NARR) 250-mb Divergence Map
# Name: narr_250mb_div_map.py
#
# Purpose: Extracts numerical weather
# prediction model output from
# North American Regional Reanalysis
# (NARR). Calculates several key
# atmospheric properties from
# output. Plots contours,
# filled-contours, and barbs from output.
# Here I focus on creating a synoptic scale, 250-mb map.
#
# Caveats: Run code with Python 3.6
# or higher. Code requires making
# modification to one line of MetPy's
# cartopy_utils.py code to function
# with later releases of Cartopy
# (see footnote [1] below).
#
# Authors: Source code originally
# developed by University
# Corporation for Atmospheric Research.
# Code modified by Lawrence Burkett
# (2020) for academic use.
# Import the following modules:
import matplotlib
matplotlib.use('Agg')
from matplotlib import pyplot as plt
from datetime import datetime
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import metpy.calc as mpcalc
import numpy as np
from metpy.plots import StationPlot
from metpy.units import units
from netCDF4 import Dataset, num2date
from scipy.ndimage import gaussian_filter
from siphon.catalog import TDSCatalog
# Year, month, day, and hour of interest.
year = 2004
month = 5
day = 29
hour = 12
dt = datetime(year, month, day, hour)
# Pressure level of interest in mb.
p0 = 250
# Read NARR data from THREDDS
# server.
base_url = 'https://www.ncei.noaa.gov/thredds/catalog/model-narr-a-files/'
# Generate URL to year, month,
# day, and hour of interest.
cat = TDSCatalog(f'{base_url}{dt:%Y%m}/{dt:%Y%m%d}/catalog.xml')
# Have Siphon find appropriate dataset.
ds = cat.datasets.filter_time_nearest(dt)
# Interface with data through NetCDF
# Subset Service (NCSS).
ncss = ds.subset()
# Create NCSS query with desired
# specifications.
query = ncss.query()
query.lonlat_box(north = 60, south = 18, east = 300, west = 225)
query.all_times()
query.add_lonlat()
query.accept('netcdf')
query.variables('Geopotential_height_isobaric', 'u-component_of_wind_isobaric', 'v-component_of_wind_isobaric')
# Use the query to obtain NetCDF data.
data = ncss.get_data(query)
# Extract data and assign units
# Geopotential height in m AMSL.
h = gaussian_filter(data.variables['Geopotential_height_isobaric'][0], sigma = 1.0) * units.meter
# West-to-east (zonal) wind in m/s.
u = gaussian_filter(data.variables['u-component_of_wind_isobaric'][0], sigma = 1.0) * units('m/s')
# South-to-north (meridional) wind in m/s.
v = gaussian_filter(data.variables['v-component_of_wind_isobaric'][0], sigma = 1.0) * units('m/s')
# Extract coordinate data.
lat = data.variables['lat'][:]
lon = data.variables['lon'][:]
z = data.variables['isobaric1'][:]
# Extract time. Check whether
# time2, time1, or time by printing data:
time = data.variables['time2']
vtime = num2date(time[0], units = time.units)
# Calcualte dx and dy for calculations.
dx, dy = mpcalc.lat_lon_grid_deltas(lon, lat)
# Model pressure level k in (i, j, k)
# grid index coordinate system.
k = np.where(p == p0)[0][0]
# Geopotential height at k-th
# index in m.
h = h[k]
# West-to-east (zonal) wind
# at k-th index in m/s.
u = u[k]
# South-to-north (meridional)
# wind at k-th index in m/s.
v = v[k]
# Divergence in Hz / 1/s.
div = mpcalc.divergence(u, v, dx, dy, dim_order = 'yx') * 1e5
# Data projection.
dataproj = ccrs.PlateCarree()
# Plot projection.
plotproj = ccrs.LambertConformal(central_longitude = -100., central_latitude = 40., standard_parallels = [30, 60])
def create_map_background():
fig=plt.figure(figsize = (14, 12))
ax=plt.subplot(111, projection = plotproj)
ax.set_extent([-125, -73, 25, 50],ccrs.PlateCarree())
ax.coastlines("50m", linewidth = 0.75)
ax.add_feature(cfeature.STATES, linewidth = 0.5)
return fig, ax
# Cell content replaced by load magic replacement.
fig, ax = create_map_background()
# First layer is geopotential
# height contours in m AMSL.
cont_range = np.arange(9000, 12000, 120)
ly1 = ax.contour(lon, lat, h, cont_range, colors = 'k', linewidths = 1.0, linestyles = 'solid', transform = dataproj)
plt.clabel(ly1, fontsize = 10, inline = 1, inline_spacing = 10, fmt = '%i', rightside_up = True, use_clabeltext = True)
# Second layer are colorfill
# divergenes in Hz / 1/s.
div_range = np.arange(-16, 17, 1)
ly2 = ax.contourf(lon, lat, div, div_range, extend = 'both', cmap = 'bwr', transform = dataproj)
plt.colorbar(ly2, orientation = 'horizontal', pad = 0, aspect = 50, extendrect = True, ticks = div_range)
# Third layer are wind barbs
# in kt.
ax.barbs(lon, lat, u.to('kts'), v.to('kts'), regrid_shape = 15, transform = dataproj)
# Titles
plt.title('NARR 250-mb geo heights (m), div ($s^{-1}$), and wind barbs (kt)', loc = 'left')
plt.title(f'valid: {vtime} utc', loc = 'right')
# Save figure.
plt.savefig('narr-5-29-2004-12utc-250mb-div-map.png')
Figure 1. NARR 250-mb output map that indicate geopotential height (m), divergence * 105 (s-1), and wind barbs (kt) for 12 utc on May 29, 2004.
- The "ValueError: 20m is not a valid Natural Earth scale" problem with cartopy_utils.py: here.