Sounding Plotter
This can be run as a script from the command line or as a notebook.
Download and plot the most recent sounding data for a specified site.
Provides a simple command line interface to specify a site. Using the current UTC time, the script calculates what the most recent sounding should be and requests it from the Wyoming archive using Siphon.
Imports
import posixpath
import numpy as np
import matplotlib.pyplot as plt
import metpy.calc as mpcalc
from metpy.plots import add_metpy_logo, add_timestamp, SkewT
from metpy.units import units
from siphon.simplewebservice.wyoming import WyomingUpperAir
This class encapsulates the code needed to upload an image to Google Drive
class DriveUploader:
def __init__(self, credsfile='mycreds.txt'):
from pydrive.drive import GoogleDrive
self.gdrive = GoogleDrive(self._get_auth(credsfile))
def _get_auth(self, credsfile):
from pydrive.auth import GoogleAuth
gauth = GoogleAuth()
# Try to load saved client credentials
gauth.LoadCredentialsFile(credsfile)
if gauth.credentials is None:
# Authenticate if they're not there
gauth.LocalWebserverAuth()
elif gauth.access_token_expired:
# Refresh them if expired
gauth.Refresh()
else:
# Initialize the saved creds
gauth.Authorize()
# Save the current credentials to a file
gauth.SaveCredentialsFile(credsfile)
return gauth
def _get_first_file_id(self, title, parent, **kwargs):
query = "title='{}' and '{}' in parents".format(title, parent)
for k, v in kwargs.items():
query += " and {}='{}'".format(k, v)
res = next(self.gdrive.ListFile({'q': query}))
if res:
return res[0]['id']
return None
def get_folder(self, path):
parent = 'root'
for part in path.split('/'):
if not part:
continue
parent = self._get_first_file_id(part, parent,
mimeType='application/vnd.google-apps.folder')
return parent
def create_or_get_file(self, path):
pathname, filename = posixpath.split(path)
folder = self.get_folder(pathname)
create_file_args = {'parents': [{'kind': 'drive#fileLink', 'id': folder}]}
file_id = self._get_first_file_id(filename, folder)
if file_id is not None:
create_file_args['id'] = file_id
return self.gdrive.CreateFile(create_file_args)
def upload_to(self, local_path, remote_path):
f = self.create_or_get_file(remote_path)
f.SetContentFile(local_path)
f['title'] = posixpath.basename(remote_path)
f.Upload()
This function takes care of actually generating a skewT from the DataFrame
def plot_skewt(df):
# We will pull the data out of the example dataset into individual variables
# and assign units.
pres = df['pressure'].values * units.hPa
temp = df['temperature'].values * units.degC
dewpoint = df['dewpoint'].values * units.degC
wind_speed = df['speed'].values * units.knots
wind_dir = df['direction'].values * units.degrees
u, v = mpcalc.wind_components(wind_speed, wind_dir)
# Create a new figure. The dimensions here give a good aspect ratio.
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, rotation=45)
# Plot temperature, dewpoint and wind barbs
skew.plot(pres, temp, 'red')
skew.plot(pres, dewpoint, 'green')
# Plot wind barbs
my_interval = np.arange(100, 1000, 50) * units('hPa') #set spacing interval
ix = mpcalc.resample_nn_1d(pres, my_interval) #find nearest indices for chosen interval
skew.plot_barbs(pres[ix], u[ix], v[ix], xloc=1) #plot values closest to chosen interval
# Improve labels and set axis limits
skew.ax.set_xlabel('Temperature (\N{DEGREE CELSIUS})')
skew.ax.set_ylabel('Pressure (hPa)')
skew.ax.set_ylim(1000, 100)
skew.ax.set_xlim(-40, 59)
# Calculate LCL height and plot as black dot.
lcl_pressure, lcl_temperature = mpcalc.lcl(pres[0], temp[0], dewpoint[0]) #index 0 is chosen to lift parcel from the surface
skew.plot(lcl_pressure, lcl_temperature, 'ko', markerfacecolor='black')
# Calculate full parcel profile and add to plot as black line
prof = mpcalc.parcel_profile(pres, temp[0], dewpoint[0]).to('degC')
skew.plot(pres, prof, 'black', linewidth=2)
# Shade areas of CAPE and CIN
skew.shade_cin(pres, temp, prof, dewpoint)
skew.shade_cape(pres, temp, prof)
# Add emphasis to 0 degree isotherm with color change
skew.ax.axvline(0, color='c', linestyle='--', linewidth=2)
# Add the relevant special lines throughout the figure
skew.plot_dry_adiabats(t0=np.arange(233, 533, 15) * units.K, alpha=0.25, color='orangered')
skew.plot_moist_adiabats(t0=np.arange(233, 400, 10) * units.K, alpha=0.25, color='tab:green')
skew.plot_mixing_lines(pressure=np.arange(1000, 99, -25) * units.hPa, linestyle='dotted', color='tab:blue')
# Add the MetPy logo!
fig = plt.gcf()
add_metpy_logo(fig, 115, 100, size='small');
return skew
def make_name(site, time):
return '{site}_{dt:%Y%m%d_%H%M}.png'.format(site=site, dt=time)
This is where the command line script will actually enter, and handles parsing command line arguments and driving everything else.
if __name__ == '__main__':
import argparse
from datetime import datetime, timedelta
import tempfile
# Set up argument parsing for the script. Provides one argument for the site, and another
# that controls whether the plot should be shown or saved as an image.
parser = argparse.ArgumentParser(description='Download sounding data and plot.')
parser.add_argument('-s', '--site', help='Site to obtain data for', type=str,
default='DDC')
parser.add_argument('--show', help='Whether to show the plot rather than save to disk',
action='store_true')
parser.add_argument('-d', '--date', help='Date and time to request data for in YYYYMMDDHH.'
' Defaults to most recent 00/12 hour.', type=str)
parser.add_argument('-g', '--gdrive', help='Google Drive upload path', type=str)
parser.add_argument('-f', '--filename', help='Image filename', type=str)
args = parser.parse_args()
if args.date:
request_time = datetime.strptime(args.date, '%Y%m%d%H')
else:
# Figure out the most recent sounding, 00 or 12. Subtracting two hours
# helps ensure that we choose a time with data available.
now = datetime.utcnow() - timedelta(hours=2)
request_time = now.replace(hour=(now.hour // 12) * 12, minute=0, second=0)
# Request the data and plot
df = WyomingUpperAir.request_data(request_time, args.site)
skewt = plot_skewt(df)
# Add the timestamp for the data to the plot
add_timestamp(skewt.ax, request_time, y=1.02, x=0, ha='left', fontsize='large')
skewt.ax.set_title(args.site)
if args.show:
plt.show()
else:
fname = args.filename if args.filename else make_name(args.site, request_time)
if args.gdrive:
uploader = DriveUploader()
with tempfile.NamedTemporaryFile(suffix='.png') as f:
skewt.ax.figure.savefig(f.name)
uploader.upload_to(f.name, posixpath.join(args.gdrive, fname))
else:
skewt.ax.figure.savefig(make_name(args.site, request_time))
usage: ipykernel_launcher.py [-h] [-s SITE] [--show] [-d DATE] [-g GDRIVE]
[-f FILENAME]
ipykernel_launcher.py: error: unrecognized arguments: --HistoryManager.hist_file=:memory:
An exception has occurred, use %tb to see the full traceback.
SystemExit: 2
/home/runner/miniconda3/envs/cookbook-dev/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3585: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)