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. 

  1. The "ValueError: 20m is not a valid Natural Earth scale" problem with cartopy_utils.py: here