Contents

Skew-T’s

Contents

Skew-T’s

Requesting Upper Air Data from Siphon and Plotting a Skew-T with MetPy

Import the libraries we need

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import metpy.calc as mpcalc
from metpy.plots import Hodograph, SkewT
from metpy.units import units, pandas_dataframe_to_unit_arrays
from datetime import datetime, timedelta
import pandas as pd
from siphon.simplewebservice.wyoming import WyomingUpperAir

Next we use methods from the datetime library to determine the latest time, and then use it to select the most recent date and time that we’ll use for our selected Skew-T.

now = datetime.utcnow()

curr_year = now.year
curr_month = now.month
curr_day = now.day
curr_hour = now.hour

print(f"Current time is: {now}")
print(f"Current year, month, date, hour: {curr_year}, {curr_month}, {curr_day}, {curr_hour}")

if (curr_hour > 13) :
    raob_hour = 12
    hr_delta = curr_hour - 12
elif (curr_hour > 1):
    raob_hour = 0
    hr_delta = curr_hour - 0
else:
    raob_hour = 12
    hr_delta = curr_hour + 12

raob_time = now - timedelta(hours=hr_delta)

raob_year = raob_time.year
raob_month = raob_time.month
raob_day = raob_time.day
raob_hour = raob_time.hour

print(f"Time of RAOB is: {raob_year}, {raob_month}, {raob_day}, {raob_hour}")
Current time is: 2024-04-30 00:59:22.308818
Current year, month, date, hour: 2024, 4, 30, 0
Time of RAOB is: 2024, 4, 29, 12

Construct a datetime object that we will use in our query to the data server. Note what it looks like.

query_date = datetime(raob_year,raob_month,raob_day,raob_hour)
query_date
datetime.datetime(2024, 4, 29, 12, 0)

If desired, we can choose a past date and time.

#current = True
current = False
if (current == False):
    query_date = datetime(2012, 10, 29, 12)
    
raob_timeStr = query_date.strftime("%Y-%m-%d %H UTC")
raob_timeTitle = query_date.strftime("%H00 UTC %-d %b %Y")
print(raob_timeStr)
print(raob_timeTitle)
2012-10-29 12 UTC
1200 UTC 29 Oct 2012

Select our station and query the data server.

station = 'ALB'

df = WyomingUpperAir.request_data (query_date,station)

Possible data retrieval errors:

  • If you get a No data available for YYYY-MM-DD HHZ for station XXX , you may have specified an incorrect date or location, or there just might not have been a successful RAOB at that date and time.
  • If you get a 503 Server Error, try requesting the sounding again. The Wyoming server does not handle simultaneous requests well. Usually the request will eventually work.

What does the returned data file look like? Well, it looks like a Pandas Dataframe!

df
pressure height temperature dewpoint direction speed u_wind v_wind station station_number time latitude longitude elevation pw
0 993.0 96 12.6 7.6 0.0 8.0 -0.000000 -8.000000e+00 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
1 990.0 122 12.0 7.0 2.0 10.0 -0.348995 -9.993908e+00 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
2 968.6 305 10.4 6.5 20.0 25.0 -8.550504 -2.349232e+01 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
3 955.0 423 9.4 6.1 26.0 26.0 -11.397650 -2.336865e+01 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
4 933.8 610 8.1 5.7 35.0 27.0 -15.486564 -2.211711e+01 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
138 10.4 30480 -57.3 NaN 295.0 22.0 19.938771 -9.297602e+00 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
139 10.0 30720 -58.1 NaN 280.0 23.0 22.650578 -3.993908e+00 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
140 9.7 30911 -58.9 NaN 277.0 26.0 25.806200 -3.168603e+00 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
141 9.0 31394 -58.2 NaN 270.0 34.0 34.000000 6.245699e-15 ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8
142 8.7 31595 -57.9 NaN NaN NaN NaN NaN ALB 72518 2012-10-29 12:00:00 42.7 -73.83 96.0 16.8

143 rows × 15 columns

Examine one of the columns

df['pressure']
0      993.0
1      990.0
2      968.6
3      955.0
4      933.8
       ...  
138     10.4
139     10.0
140      9.7
141      9.0
142      8.7
Name: pressure, Length: 143, dtype: float64

The University of Wyoming sounding reader includes a units attribute, which produces a Python dictionary of units specific to Wyoming sounding data. What does it look like?

df.units
{'pressure': 'hPa',
 'height': 'meter',
 'temperature': 'degC',
 'dewpoint': 'degC',
 'direction': 'degrees',
 'speed': 'knot',
 'u_wind': 'knot',
 'v_wind': 'knot',
 'station': None,
 'station_number': None,
 'time': None,
 'latitude': 'degrees',
 'longitude': 'degrees',
 'elevation': 'meter',
 'pw': 'millimeter'}

Attach units to the various columns using MetPy’s pandas_dataframe_to_unit_arrays function, as follows:

df = pandas_dataframe_to_unit_arrays(df)

Now that units are attached, how does the pressure column look?

df['pressure']
Magnitude
[993.0 990.0 968.6 955.0 933.8 925.0 900.0 876.0 867.1 850.0 833.0 818.0
813.0 806.0 804.2 794.0 783.0 774.5 754.0 746.0 740.0 718.4 716.0 706.0
700.0 690.0 680.0 666.0 663.0 649.0 641.1 640.0 630.0 596.0 593.9 584.0
550.0 549.5 517.0 508.0 500.0 493.0 490.0 486.0 484.0 477.0 472.0 469.3
466.0 458.0 451.0 446.0 437.0 432.9 428.0 427.0 419.0 409.0 404.0 400.0
398.0 388.0 383.1 380.0 331.0 318.0 310.6 300.0 250.0 217.0 216.8 205.0
200.0 198.0 183.0 172.0 166.0 162.6 160.0 154.8 153.0 151.0 150.0 145.0
143.0 140.2 134.0 120.8 118.0 105.0 100.0 96.0 87.1 81.6 80.9 76.2 73.3
73.2 72.5 70.0 68.6 66.2 63.0 60.0 58.7 54.2 51.6 50.2 50.0 49.0 47.1
46.8 44.5 42.4 42.3 40.5 38.0 36.8 36.6 34.9 33.2 31.7 31.7 30.0 27.4
26.2 24.0 20.8 20.4 20.0 19.5 18.6 17.7 16.8 16.7 14.6 12.4 10.7 10.4
10.0 9.7 9.0 8.7]
Unitshectopascal

As with any Pandas Dataframe, we can select columns, so we do so now for all the relevant variables for our Skew-T.

P = df['pressure']
Z = df['height']
T = df['temperature']
Td = df['dewpoint']
u = df['u_wind']
v = df['v_wind']

Now that u and v are units aware, we can use one of MetPy’s diagnostic routines to calculate the wind speed from the vector’s components.

wind_speed = mpcalc.wind_speed(u,v)
wind_speed
Magnitude
[8.0 10.0 25.0 26.0 27.0 27.999999999999996 29.0 33.0 34.0 36.0 39.0
42.00000000000001 43.0 45.0 45.0 45.0 44.0 44.0 43.0 42.99999999999999
42.0 38.0 38.0 39.0 39.0 36.0 33.0 29.000000000000004 29.000000000000004
30.000000000000004 31.0 31.0 33.0 39.0 39.0 39.0 40.0 40.0
46.00000000000001 47.0 49.0 50.0 50.0 50.0 51.0 51.0 52.0 52.0
52.99999999999999 55.0 57.0 58.0 60.99999999999999 61.99999999999999
61.99999999999999 61.99999999999999 60.0 58.0 57.00000000000001 56.0 56.0
55.0 55.00000000000001 55.00000000000001 52.0 51.0 50.0 52.0
55.00000000000001 61.0 61.0 52.0 48.0 47.0 38.0 30.999999999999996 27.0
25.0 26.0 28.999999999999996 29.999999999999996 34.0 36.0 43.0 45.0 49.0
47.0 42.0 41.0 33.0 30.0 31.999999999999996 38.0 42.0 42.0 35.0 31.0 31.0
28.0 15.000000000000002 11.0 3.0 11.0 16.0 16.0 15.0 14.0
11.999999999999998 12.000000000000002 11.000000000000002 10.0
9.999999999999998 11.0 1.0 1.0 6.0 13.0 16.0 17.0 11.999999999999998 10.0
11.0 11.0 9.0 9.0 10.0 11.0 13.999999999999998 13.999999999999998 14.0
13.999999999999998 10.0 5.0 9.0 9.0 13.0 17.0 21.000000000000004 22.0
23.0 26.0 34.0 nan]
Unitsknot

Thermodynamic Calculations

Often times we will want to calculate some thermodynamic parameters of a sounding. The MetPy calc module has many such calculations already implemented!

  • Lifting Condensation Level (LCL) - The level at which an air parcel’s relative humidity becomes 100% when lifted along a dry adiabatic path.

  • Parcel Path - Path followed by a hypothetical parcel of air, beginning at the surface temperature/pressure and rising dry adiabatically until reaching the LCL, then rising moist adiabatially.

Calculate the LCL. Note that the units will appear at the end when we print them out!

lclp, lclt = mpcalc.lcl(P[0], T[0], Td[0])
print (f'LCL Pressure = ,{lclp}, LCL Temperature = ,{lclt}')
LCL Pressure = ,920.7394732770757 hectopascal, LCL Temperature = ,6.497694364794597 degree_Celsius

Calculate the parcel profile. Here, we pass in the array of all pressure values, and specify the first (index 0, remember Python’s zero-based indexing) index of the temperature and dewpoint arrays as the starting point of the parcel path.

parcel_prof = mpcalc.parcel_profile(P, T[0], Td[0])
/tmp/ipykernel_1786263/3676909138.py:1: UserWarning: Duplicate pressure(s) [31.7] hPa provided. Output profile includes duplicate temperatures as a result.
  parcel_prof = mpcalc.parcel_profile(P, T[0], Td[0])

What does the parcel profile look like? Well, it’s just an array of all the temperatures of the parcel at each of the pressure levels as it ascends.

parcel_prof
Magnitude
[285.75 285.5030782457374 283.7260147751601 282.5820425453529
280.7753568958992 280.0168022430824 278.61618543739144 277.37788348934725
276.90581328134687 275.9782124328902 275.0279160241501 274.1648575205796
273.8718416928455 273.45700592006415 273.34945036323086 272.7329942810008
272.05454958420216 271.52026852209843 270.1940423960125
269.66141176132635 269.2561561415735 267.75442885282604 267.583285465949
266.86058618396595 266.41936839754885 265.670943007434 264.90567344474294
263.8048184817428 263.56429934984016 262.41949832525245
261.75674657963566 261.66347940670954 260.8042978252957
257.72244579177345 257.5235091091759 256.57148642495343
253.11433727310046 253.06122185662616 249.45701365883085
248.40382765227804 247.4466272947146 246.5924737262469 246.22158663524826
245.7225144400129 245.47101030595226 244.58029714478408 243.9340033692485
243.5814723658283 243.1472064494606 242.07876467549488 241.1254004769443
240.43369999721526 239.16574746350264 238.57823115316117
237.8678432880061 237.72175631562834 236.53937681720947
235.02671099621608 234.25567508240604 233.63168909112818
233.31729079659232 231.72095400080372 230.92374503777955
230.41422085980042 221.80231016677374 219.33027125104985
217.88539888577145 215.7662041397491 204.90076525801487
196.80086066722757 196.7491055884933 193.63230420022177
192.27240487119323 191.72157327817584 187.45709735772672
184.16733946029078 182.3089235232852 181.23437730188175
180.40176108737282 178.7070329164221 178.11091109548315
177.44264070955194 177.1061293624403 175.3990755095199 174.70446238301173
173.7202397319947 171.4898118203642 166.48329538275684 165.37152095228817
159.9473991168242 157.73319985025483 155.90417910984007
151.63011013010723 148.83043101365527 148.46452750857955
145.9472678542819 144.3382368185137 144.28194814096133 143.88638145757227
142.45097140890326 141.63108418948144 140.1973112603891 138.2266685377671
136.31314922182366 135.4626935136108 132.41064512066026
130.56387052114502 129.54178446437365 129.39411624035344
128.6493784304654 127.2039198706387 126.97190078130541 125.1568206552239
123.440079323706 123.35682849677039 121.83368438494378 119.63582641344145
118.54400701076734 118.35957405664507 116.76206663809988
115.10796780659943 113.5974481406761 113.5974481406761 111.82248617609415
108.96333045315536 107.57799151832863 104.91571700875281
100.71264475184138 100.15543570723631 99.59036699364985 98.87256347045566
97.5466718971011 96.17412869139926 94.7507880801904 94.58930354925499
91.02626068321919 86.87616551077693 83.29215371522552 82.61813651693153
81.6974917854495 80.98959301739299 79.27479832626867 78.51063722548488]
Unitskelvin

Set the limits for the x- and y-axes so they can be changed and easily re-used when desired.

pTop = 100
pBot = 1050
tMin = -60
tMax = 40

Since we are plotting a skew-T, define the log-p axis.

logTop = np.log10(pTop)
logBot = np.log10(pBot)
interval = np.logspace(logTop,logBot) * units('hPa')

This interval will tend to oversample points near the top of the atmosphere relative to the bottom. One effect of this will be that wind barbs will be hard to read, especially at higher levels. Use a resampling method from NumPy to deal with this.

idx = mpcalc.resample_nn_1d(P, interval)

Now we are ready to create our skew-T. Plot it and save it as a PNG file.

Note:

Annoyingly, when specifying the color of the dry/moist adiabats, and mixing ratio lines, the parameter must be colors, not color. In other Matplotlib-related code lines in the cell below where we set the color, such as skew.ax.axvline , color must be singular.

# Create a new figure. The dimensions here give a good aspect ratio
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, rotation=30)

# Plot the data using normal plotting functions, in this case using
# log scaling in Y, as dictated by the typical meteorological plot
skew.plot(P, T, 'r', linewidth=3)
skew.plot(P, Td, 'g', linewidth=3)
skew.plot_barbs(P[idx], u[idx], v[idx])
skew.ax.set_ylim(pBot, pTop)
skew.ax.set_xlim(tMin, tMax)

# Plot LCL as black dot
skew.plot(lclp, lclt, 'ko', markerfacecolor='black')

# Plot the parcel profile as a black line
skew.plot(P, parcel_prof, 'k', linestyle='--', linewidth=2)

# Shade areas of CAPE and CIN
#skew.shade_cin(p, T, parcel_prof)
#skew.shade_cape(p, T, parcel_prof)

# Shade areas between a certain temperature (in this case, the DGZ)
skew.shade_area(P.m,-18,-12,color=['purple'])
# Plot a zero degree isotherm
skew.ax.axvline(0, color='c', linestyle='-', linewidth=2)

# Add the relevant special lines
skew.plot_dry_adiabats()
skew.plot_moist_adiabats(colors='#58a358', linestyle='-')
skew.plot_mixing_lines()

plt.title(f"RAOB for {station} valid at: {raob_timeTitle}")

# Create a hodograph
# Create an inset axes object that is 30% width and height of the
# figure and put it in the upper right hand corner.
ax_hod = inset_axes(skew.ax, '30%', '30%', loc=1)
h = Hodograph(ax_hod, component_range=100.)
h.add_grid(increment=30)
h.plot_colormapped(u, v, df['height'])
plt.savefig (f"station{raob_timeStr}_skewt.png")
../../_images/3e890487962922b2edbc6bc858229c5d5fa7fd87cf82ab440a0cdc9f4dd6973d.png

Here’s how the plot would appear if we hadn’t done the resampling. Note that the wind barbs are plotted at every level, and are harder to read:

# Create a new figure. The dimensions here give a good aspect ratio
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, rotation=30)

# Plot the data using normal plotting functions, in this case using
# log scaling in Y, as dictated by the typical meteorological plot
skew.plot(P, T, 'r', linewidth=3)
skew.plot(P, Td, 'g', linewidth=3)
skew.plot_barbs(P, u, v)
skew.ax.set_ylim(pBot, pTop)
skew.ax.set_xlim(tMin, tMax)

# Plot LCL as black dot
skew.plot(lclp, lclt, 'ko', markerfacecolor='black')

# Plot the parcel profile as a black line
skew.plot(P, parcel_prof, 'k', linestyle='--', linewidth=2)

# Shade areas of CAPE and CIN
skew.shade_cin(P, T, parcel_prof)
skew.shade_cape(P, T, parcel_prof)

# Plot a zero degree isotherm
skew.ax.axvline(0, color='c', linestyle='-', linewidth=2)

# Add the relevant special lines
skew.plot_dry_adiabats()
skew.plot_moist_adiabats(colors='#58a358', linestyle='-')
skew.plot_mixing_lines()

plt.title(f"RAOB for {station} valid at: {raob_timeTitle}")

# Create a hodograph
# Create an inset axes object that is 30% width and height of the
# figure and put it in the upper right hand corner.
ax_hod = inset_axes(skew.ax, '30%', '30%', loc=1)
h = Hodograph(ax_hod, component_range=100.)
h.add_grid(increment=30)
h.plot_colormapped(u, v, df['height'])  # Plot a line colored by a parameter
<matplotlib.collections.LineCollection at 0x146528d62350>
../../_images/e837afb59c36aa5c28265cde6f2b5569b6f6fff946c476b3a5a6065667e8ea3f.png

What’s next:

Try different RAOB locations and dates (for example, draw a skew-t relevant to your class project).
Here is a link to a map of NWS RAOB sites.
Depending on the situation, you may want to omit plotting the DGZ, LCL, parcel path, CAPE/CIN, freezing line, and/or move the location of the hodograph.