def test_no_el(): """Test equilibrium layer calculation when there is no EL in the data.""" levels = np.array([959., 867.9, 779.2, 647.5, 472.5, 321.9, 251.]) * units.mbar temperatures = np.array([22.2, 17.4, 14.6, 1.4, -17.6, -39.4, -52.5]) * units.celsius dewpoints = np.array([19., 14.3, -11.2, -16.7, -21., -43.3, -56.7]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert assert_nan(el_pressure, levels.units) assert assert_nan(el_temperature, temperatures.units)
def test_el(): """Test equilibrium layer calculation.""" levels = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.mbar temperatures = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.celsius dewpoints = np.array([19., -11.2, -10.8, -10.4, -10., -53.2]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_almost_equal(el_pressure, 520.8700 * units.mbar, 3) assert_almost_equal(el_temperature, -11.7027 * units.degC, 3)
def test_el_ml(): """Test equilibrium layer calculation for a mixed parcel.""" levels = np.array([959., 779.2, 751.3, 724.3, 700., 400., 269.]) * units.mbar temperatures = np.array([22.2, 14.6, 12., 9.4, 7., -25., -35.]) * units.celsius dewpoints = np.array([19., -11.2, -10.8, -10.4, -10., -35., -53.2]) * units.celsius __, t_mixed, td_mixed = mixed_parcel(levels, temperatures, dewpoints) mixed_parcel_prof = parcel_profile(levels, t_mixed, td_mixed) el_pressure, el_temperature = el(levels, temperatures, dewpoints, mixed_parcel_prof) assert_almost_equal(el_pressure, 355.834 * units.mbar, 3) assert_almost_equal(el_temperature, -28.371 * units.degC, 3)
def test_no_el_multi_crossing(): """Test el calculation with no el and severel parcel path-profile crossings.""" levels = np.array([918., 911., 880., 873.9, 850., 848., 843.5, 818., 813.8, 785., 773., 763., 757.5, 730.5, 700., 679., 654.4, 645., 643.9]) * units.mbar temperatures = np.array([24.2, 22.8, 19.6, 19.1, 17., 16.8, 16.5, 15., 14.9, 14.4, 16.4, 16.2, 15.7, 13.4, 10.6, 8.4, 5.7, 4.6, 4.5]) * units.celsius dewpoints = np.array([19.5, 17.8, 16.7, 16.5, 15.8, 15.7, 15.3, 13.1, 12.9, 11.9, 6.4, 3.2, 2.6, -0.6, -4.4, -6.6, -9.3, -10.4, -10.5]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert assert_nan(el_pressure, levels.units) assert assert_nan(el_temperature, temperatures.units)
def test_el_small_surface_instability(): """Test that no EL is found when there is a small pocket of instability at the sfc.""" levels = np.array([959., 931.3, 925., 899.3, 892., 867.9, 850., 814., 807.9, 790., 779.2, 751.3, 724.3, 700., 655., 647.5, 599.4, 554.7, 550., 500.]) * units.mbar temperatures = np.array([22.2, 20.2, 19.8, 18.4, 18., 17.4, 17., 15.4, 15.4, 15.6, 14.6, 12., 9.4, 7., 2.2, 1.4, -4.2, -9.7, -10.3, -14.9]) * units.degC dewpoints = np.array([20., 18.5, 18.1, 17.9, 17.8, 15.3, 13.5, 6.4, 2.2, -10.4, -10.2, -9.8, -9.4, -9., -15.8, -15.7, -14.8, -14., -13.9, -17.9]) * units.degC el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert assert_nan(el_pressure, levels.units) assert assert_nan(el_temperature, temperatures.units)
def test_no_el_parcel_colder(): """Test no EL when parcel stays colder than environment. INL 20170925-12Z.""" levels = np.array([974., 946., 925., 877.2, 866., 850., 814.6, 785., 756.6, 739., 729.1, 700., 686., 671., 641., 613., 603., 586., 571., 559.3, 539., 533., 500., 491., 477.9, 413., 390., 378., 345., 336.]) * units.mbar temperatures = np.array([10., 8.4, 7.6, 5.9, 7.2, 7.6, 6.8, 7.1, 7.7, 7.8, 7.7, 5.6, 4.6, 3.4, 0.6, -0.9, -1.1, -3.1, -4.7, -4.7, -6.9, -7.5, -11.1, -10.9, -12.1, -20.5, -23.5, -24.7, -30.5, -31.7]) * units.celsius dewpoints = np.array([8.9, 8.4, 7.6, 5.9, 7.2, 7., 5., 3.6, 0.3, -4.2, -12.8, -12.4, -8.4, -8.6, -6.4, -7.9, -11.1, -14.1, -8.8, -28.1, -18.9, -14.5, -15.2, -15.1, -21.6, -41.5, -45.5, -29.6, -30.6, -32.1]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert assert_nan(el_pressure, levels.units) assert assert_nan(el_temperature, temperatures.units)
def test_no_el_parcel_colder(): """Tests no EL when parcel stays colder than environment. INL 20170925-12Z.""" levels = np.array([974., 946., 925., 877.2, 866., 850., 814.6, 785., 756.6, 739., 729.1, 700., 686., 671., 641., 613., 603., 586., 571., 559.3, 539., 533., 500., 491., 477.9, 413., 390., 378., 345., 336.]) * units.mbar temperatures = np.array([10., 8.4, 7.6, 5.9, 7.2, 7.6, 6.8, 7.1, 7.7, 7.8, 7.7, 5.6, 4.6, 3.4, 0.6, -0.9, -1.1, -3.1, -4.7, -4.7, -6.9, -7.5, -11.1, -10.9, -12.1, -20.5, -23.5, -24.7, -30.5, -31.7]) * units.celsius dewpoints = np.array([8.9, 8.4, 7.6, 5.9, 7.2, 7., 5., 3.6, 0.3, -4.2, -12.8, -12.4, -8.4, -8.6, -6.4, -7.9, -11.1, -14.1, -8.8, -28.1, -18.9, -14.5, -15.2, -15.1, -21.6, -41.5, -45.5, -29.6, -30.6, -32.1]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert assert_nan(el_pressure, levels.units) assert assert_nan(el_temperature, temperatures.units)
def test_el_lfc_equals_lcl(): """Test equilibrium layer calculation when the lfc equals the lcl.""" levels = np.array([912., 905.3, 874.4, 850., 815.1, 786.6, 759.1, 748., 732.3, 700., 654.8, 606.8, 562.4, 501.8, 500., 482., 400., 393.3, 317.1, 307., 300., 252.7, 250., 200., 199.3, 197., 190., 172., 156.6, 150., 122.9, 112., 106.2, 100.]) * units.mbar temperatures = np.array([29.4, 28.7, 25.2, 22.4, 19.4, 16.8, 14.3, 13.2, 12.6, 11.4, 7.1, 2.2, -2.7, -10.1, -10.3, -12.4, -23.3, -24.4, -38., -40.1, -41.1, -49.8, -50.3, -59.1, -59.1, -59.3, -59.7, -56.3, -56.9, -57.1, -59.1, -60.1, -58.6, -56.9]) * units.celsius dewpoints = np.array([18.4, 18.1, 16.6, 15.4, 13.2, 11.4, 9.6, 8.8, 0., -18.6, -22.9, -27.8, -32.7, -40.1, -40.3, -42.4, -53.3, -54.4, -68., -70.1, -70., -70., -70., -70., -70., -70., -70., -70., -70., -70., -70., -70., -70., -70.]) * units.celsius el_pressure, el_temperature = el(levels, temperatures, dewpoints) assert_almost_equal(el_pressure, 175.8684 * units.mbar, 3) assert_almost_equal(el_temperature, -57.0307 * units.degC, 3)
def process_skewt(self): # Calculation index_p100 = get_pressure_level_index(self.p_i, 100) lcl_p, lcl_t = mpcalc.lcl(self.p_i[0], self.t_i[0], self.td_i[0]) lfc_p, lfc_t = mpcalc.lfc(self.p_i, self.t_i, self.td_i) el_p, el_t = mpcalc.el(self.p_i, self.t_i, self.td_i) prof = mpcalc.parcel_profile(self.p_i, self.t_i[0], self.td_i[0]).to('degC') cape, cin = mpcalc.cape_cin(self.p_i, self.t_i, self.td_i, prof) mucape, mucin = mpcalc.most_unstable_cape_cin(self.p_i, self.t_i, self.td_i) pwat = mpcalc.precipitable_water(self.td_i, self.p_i) i8 = get_pressure_level_index(self.p_i, 850) i7 = get_pressure_level_index(self.p_i, 700) i5 = get_pressure_level_index(self.p_i, 500) theta850 = mpcalc.equivalent_potential_temperature(850 * units('hPa'), self.t_i[i8], self.td_i[i5]) theta500 = mpcalc.equivalent_potential_temperature(500 * units('hPa'), self.t_i[i5], self.td_i[i5]) thetadiff = theta850 - theta500 k = self.t_i[i8] - self.t_i[i5] + self.td_i[i8] - (self.t_i[i7] - self.td_i[i7]) a = ((self.t_i[i8] - self.t_i[i5]) - (self.t_i[i8] - self.td_i[i5]) - (self.t_i[i7] - self.td_i[i7]) - (self.t_i[i5] - self.td_i[i5])) sw = c_sweat(np.array(self.t_i[i8].magnitude), np.array(self.td_i[i8].magnitude), np.array(self.t_i[i5].magnitude), np.array(self.u_i[i8].magnitude), np.array(self.v_i[i8].magnitude), np.array(self.u_i[i5].magnitude), np.array(self.v_i[i5].magnitude)) si = showalter_index(self.t_i[i8], self.td_i[i8], self.t_i[i5]) li = lifted_index(self.t_i[0], self.td_i[0], self.p_i[0], self.t_i[i5]) srh_pos, srh_neg, srh_tot = mpcalc.storm_relative_helicity(self.u_i, self.v_i, self.alt, 1000 * units('m')) sbcape, sbcin = mpcalc.surface_based_cape_cin(self.p_i, self.t_i, self.td_i) shr6km = mpcalc.bulk_shear(self.p_i, self.u_i, self.v_i, heights=self.alt, depth=6000 * units('m')) wshr6km = mpcalc.wind_speed(*shr6km) sigtor = mpcalc.significant_tornado(sbcape, delta_height(self.p_i[0], lcl_p), srh_tot, wshr6km)[0] # Plotting self.ax.set_ylim(1050, 100) self.ax.set_xlim(-40, 50) self.plot(self.p_i, self.t_i, 'r', linewidth=1) self.plot(self.p_i[:self.dp_idx], self.td_i[:self.dp_idx], 'g', linewidth=1) self.plot_barbs(self.p_i[:index_p100], self.u_i[:index_p100] * 1.94, self.v_i[:index_p100] * 1.94) self.plot(lcl_p, lcl_t, 'ko', markerfacecolor='black') self.plot(self.p_i, prof, 'k', linewidth=2) if cin.magnitude < 0: chi = -1 * cin.magnitude self.shade_cin(self.p_i, self.t_i, prof) elif cin.magnitude > 0: chi = cin.magnitude self.shade_cin(self.p_i, self.t_i, prof) else: chi = 0. self.shade_cape(self.p_i, self.t_i, prof) self.plot_dry_adiabats(linewidth=0.5) self.plot_moist_adiabats(linewidth=0.5) self.plot_mixing_lines(linewidth=0.5) plt.title('Skew-T Plot \nStation: {} Time: {}'.format(self.st, self.time.strftime('%Y.%m.%d %H:%M')), fontsize=14, loc='left') # Add hodograph ax = self._fig.add_axes([0.95, 0.71, 0.17, 0.17]) h = Hodograph(ax, component_range=50) h.add_grid(increment=20) h.plot_colormapped(self.u_i[:index_p100], self.v_i[:index_p100], self.alt[:index_p100], linewidth=1.2) # Annotate parameters # Annotate names namelist = ['CAPE', 'CIN', 'MUCAPE', 'PWAT', 'K', 'A', 'SWEAT', 'LCL', 'LFC', 'EL', 'SI', 'LI', 'T850-500', 'θse850-500', 'SRH', 'STP'] xcor = -50 ycor = -90 spacing = -9 for nm in namelist: ax.text(xcor, ycor, '{}: '.format(nm), fontsize=10) ycor += spacing # Annotate values varlist = [cape, chi, mucape, pwat, k, a, sw, lcl_p, lfc_p, el_p, si, li, self.t_i[i8] - self.t_i[i5], thetadiff, srh_tot, sigtor] xcor = 10 ycor = -90 for v in varlist: if hasattr(v, 'magnitude'): v = v.magnitude ax.text(xcor, ycor, str(np.round_(v, 2)), fontsize=10) ycor += spacing # Annotate units unitlist = ['J/kg', 'J/kg', 'J/kg', 'mm', '°C', '°C', '', 'hPa', 'hPa', 'hPa', '°C', '°C', '°C', '°C'] xcor = 45 ycor = -90 for u in unitlist: ax.text(xcor, ycor, ' {}'.format(u), fontsize=10) ycor += spacing
def do_profile(cursor, fid, gdf, nt): """Process this profile.""" # The inbound profile may contain mandatory level data that is below # the surface. It seems the best we can do here is to ensure both # temperature and dewpoint are valid and call that the bottom. td_profile = gdf[pd.notnull(gdf["tmpc"]) & pd.notnull(gdf["dwpc"])] wind_profile = gdf[pd.notnull(gdf["u"])] # Presently we are all or nothing here. The length is arb if len(td_profile.index) < 5 or len(wind_profile.index) < 5: msg = ("quorum fail td: %s wind: %s, skipping") % ( len(td_profile.index), len(wind_profile.index), ) raise ValueError(msg) if gdf["pressure"].min() > 500: raise ValueError("Profile only up to %s mb" % (gdf["pressure"].min(), )) # Does a crude check that our metadata station elevation is within 50m # of the profile bottom, otherwise we ABORT station_elevation_m = get_station_elevation(td_profile, nt) # get surface wind u_sfc, v_sfc = get_surface_winds(wind_profile) u_1km, v_1km = get_aloft_winds(wind_profile, station_elevation_m + 1000.0) u_3km, v_3km = get_aloft_winds(wind_profile, station_elevation_m + 3000.0) u_6km, v_6km = get_aloft_winds(wind_profile, station_elevation_m + 6000.0) shear_sfc_1km_smps = np.sqrt((u_1km - u_sfc)**2 + (v_1km - v_sfc)**2) shear_sfc_3km_smps = np.sqrt((u_3km - u_sfc)**2 + (v_3km - v_sfc)**2) shear_sfc_6km_smps = np.sqrt((u_6km - u_sfc)**2 + (v_6km - v_sfc)**2) total_totals = compute_total_totals(td_profile) sweat_index = compute_sweat_index(td_profile, total_totals) try: bunkers_rm, bunkers_lm, mean0_6_wind = bunkers_storm_motion( wind_profile["pressure"].values * units.hPa, wind_profile["u"].values * units("m/s"), wind_profile["v"].values * units("m/s"), wind_profile["height"].values * units("m"), ) except ValueError: # Profile may not go up high enough bunkers_rm = [np.nan * units("m/s"), np.nan * units("m/s")] bunkers_lm = [np.nan * units("m/s"), np.nan * units("m/s")] mean0_6_wind = [np.nan * units("m/s"), np.nan * units("m/s")] bunkers_rm_smps = wind_speed(bunkers_rm[0], bunkers_rm[1]) bunkers_rm_drct = wind_direction(bunkers_rm[0], bunkers_rm[1]) bunkers_lm_smps = wind_speed(bunkers_lm[0], bunkers_lm[1]) bunkers_lm_drct = wind_direction(bunkers_lm[0], bunkers_lm[1]) mean0_6_wind_smps = wind_speed(mean0_6_wind[0], mean0_6_wind[1]) mean0_6_wind_drct = wind_direction(mean0_6_wind[0], mean0_6_wind[1]) try: ( srh_sfc_1km_pos, srh_sfc_1km_neg, srh_sfc_1km_total, ) = storm_relative_helicity( wind_profile["height"].values * units("m"), wind_profile["u"].values * units("m/s"), wind_profile["v"].values * units("m/s"), 1000.0 * units("m"), ) except ValueError: srh_sfc_1km_pos = np.nan * units("m") # blah srh_sfc_1km_neg = np.nan * units("m") # blah srh_sfc_1km_total = np.nan * units("m") # blah try: ( srh_sfc_3km_pos, srh_sfc_3km_neg, srh_sfc_3km_total, ) = storm_relative_helicity( wind_profile["height"].values * units("m"), wind_profile["u"].values * units("m/s"), wind_profile["v"].values * units("m/s"), 3000.0 * units("m"), ) except ValueError: srh_sfc_3km_pos = np.nan * units("m") # blah srh_sfc_3km_neg = np.nan * units("m") # blah srh_sfc_3km_total = np.nan * units("m") # blah pwater = precipitable_water( td_profile["pressure"].values * units.hPa, td_profile["dwpc"].values * units.degC, ) (sbcape, sbcin) = surface_based_cape_cin( td_profile["pressure"].values * units.hPa, td_profile["tmpc"].values * units.degC, td_profile["dwpc"].values * units.degC, ) (mucape, mucin) = most_unstable_cape_cin( td_profile["pressure"].values * units.hPa, td_profile["tmpc"].values * units.degC, td_profile["dwpc"].values * units.degC, ) (mlcape, mlcin) = mixed_layer_cape_cin( td_profile["pressure"].values * units.hPa, td_profile["tmpc"].values * units.degC, td_profile["dwpc"].values * units.degC, ) el_p, el_t = el( td_profile["pressure"].values * units.hPa, td_profile["tmpc"].values * units.degC, td_profile["dwpc"].values * units.degC, ) lfc_p, lfc_t = lfc( td_profile["pressure"].values * units.hPa, td_profile["tmpc"].values * units.degC, td_profile["dwpc"].values * units.degC, ) (lcl_p, lcl_t) = lcl( td_profile["pressure"].values[0] * units.hPa, td_profile["tmpc"].values[0] * units.degC, td_profile["dwpc"].values[0] * units.degC, ) vals = [ el_p.to(units("hPa")).m, lfc_p.to(units("hPa")).m, lcl_p.to(units("hPa")).m, ] [el_hght, lfc_hght, lcl_hght] = log_interp( np.array(vals, dtype="f"), td_profile["pressure"].values[::-1], td_profile["height"].values[::-1], ) el_agl = gt1(el_hght - station_elevation_m) lcl_agl = gt1(lcl_hght - station_elevation_m) lfc_agl = gt1(lfc_hght - station_elevation_m) args = ( nonull(sbcape.to(units("joules / kilogram")).m), nonull(sbcin.to(units("joules / kilogram")).m), nonull(mucape.to(units("joules / kilogram")).m), nonull(mucin.to(units("joules / kilogram")).m), nonull(mlcape.to(units("joules / kilogram")).m), nonull(mlcin.to(units("joules / kilogram")).m), nonull(pwater.to(units("mm")).m), nonull(el_agl), nonull(el_p.to(units("hPa")).m), nonull(el_t.to(units.degC).m), nonull(lfc_agl), nonull(lfc_p.to(units("hPa")).m), nonull(lfc_t.to(units.degC).m), nonull(lcl_agl), nonull(lcl_p.to(units("hPa")).m), nonull(lcl_t.to(units.degC).m), nonull(total_totals), nonull(sweat_index), nonull(bunkers_rm_smps.m), nonull(bunkers_rm_drct.m), nonull(bunkers_lm_smps.m), nonull(bunkers_lm_drct.m), nonull(mean0_6_wind_smps.m), nonull(mean0_6_wind_drct.m), nonull(srh_sfc_1km_pos.m), nonull(srh_sfc_1km_neg.m), nonull(srh_sfc_1km_total.m), nonull(srh_sfc_3km_pos.m), nonull(srh_sfc_3km_neg.m), nonull(srh_sfc_3km_total.m), nonull(shear_sfc_1km_smps), nonull(shear_sfc_3km_smps), nonull(shear_sfc_6km_smps), fid, ) cursor.execute( """ UPDATE raob_flights SET sbcape_jkg = %s, sbcin_jkg = %s, mucape_jkg = %s, mucin_jkg = %s, mlcape_jkg = %s, mlcin_jkg = %s, pwater_mm = %s, el_agl_m = %s, el_pressure_hpa = %s, el_tmpc = %s, lfc_agl_m = %s, lfc_pressure_hpa = %s, lfc_tmpc = %s, lcl_agl_m = %s, lcl_pressure_hpa = %s, lcl_tmpc = %s, total_totals = %s, sweat_index = %s, bunkers_rm_smps = %s, bunkers_rm_drct = %s, bunkers_lm_smps = %s, bunkers_lm_drct = %s, mean_sfc_6km_smps = %s, mean_sfc_6km_drct = %s, srh_sfc_1km_pos = %s, srh_sfc_1km_neg = %s, srh_sfc_1km_total = %s, srh_sfc_3km_pos = %s, srh_sfc_3km_neg = %s, srh_sfc_3km_total = %s, shear_sfc_1km_smps = %s, shear_sfc_3km_smps = %s, shear_sfc_6km_smps = %s, computed = 't' WHERE fid = %s """, args, )
def entropy_plots(pressure, temperature, mixing_ratio, altitude, h0_std=2000, ensemble_size=20, ent_rate=np.arange(0, 2, 0.05), entrain=False): """ plotting the summarized entropy diagram with annotations and thermodynamic parameters """ p = pressure * units('mbar') T = temperature * units('degC') q = mixing_ratio * units('kilogram/kilogram') qs = mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(T), p) Td = mpcalc.dewpoint(mpcalc.vapor_pressure(p, q)) # dewpoint Tp = mpcalc.parcel_profile(p, T[0], Td[0]).to('degC') # parcel profile # Altitude based on the hydrostatic eq. if len(altitude) == len(pressure): # (1) altitudes for whole levels altitude = altitude * units('meter') elif len(altitude ) == 1: # (2) known altitude where the soundings was launched z_surf = altitude.copy() * units('meter') # given altitude altitude = np.zeros((np.size(T))) * units('meter') for i in range(np.size(T)): altitude[i] = mpcalc.thickness_hydrostatic( p[:i + 1], T[:i + 1]) + z_surf # Hypsometric Eq. for height else: print( '***NOTE***: the altitude at the surface is assumed 0 meter, and altitudes are derived based on the hypsometric equation' ) altitude = np.zeros( (np.size(T))) * units('meter') # surface is 0 meter for i in range(np.size(T)): altitude[i] = mpcalc.thickness_hydrostatic( p[:i + 1], T[:i + 1]) # Hypsometric Eq. for height # specific entropy [joule/(kg*K)] # sd : specific entropy of dry air # sm1 : specific entropy of airborne mositure in state 1 (water vapor) # sm2 : specific entropy of airborne mositure in state 2 (saturated water vapor) sd = entropy(T.magnitude, q.magnitude * 1e-6, p.magnitude) sm1 = entropy(T.magnitude, q.magnitude, p.magnitude) sm2 = entropy(T.magnitude, qs.magnitude, p.magnitude) ############################### # Water vapor calculations p_PWtop = min(p) #p_PWtop = max(200*units.mbar, min(p) + 1*units.mbar) # integrating until 200mb cwv = mpcalc.precipitable_water(Td, p, top=p_PWtop) # column water vapor [mm] cwvs = mpcalc.precipitable_water( T, p, top=p_PWtop) # saturated column water vapor [mm] crh = (cwv / cwvs) * 100. # column relative humidity [%] #================================================ # plotting MSE vertical profiles fig = plt.figure(figsize=[12, 8]) ax = fig.add_axes([0.1, 0.1, 0.6, 0.8]) ax.plot(sd, p, '-k', linewidth=2) ax.plot(sm1, p, '-b', linewidth=2) ax.plot(sm2, p, '-r', linewidth=2) # mse based on different percentages of relative humidity qr = np.zeros((9, np.size(qs))) * units('kilogram/kilogram') sm1_r = qr # container for i in range(9): qr[i, :] = qs * 0.1 * (i + 1) sm1_r[i, :] = entropy(T.magnitude, qr[i, :].magnitude, p.magnitude) for i in range(9): ax.plot(sm1_r[i, :], p[:], '-', color='grey', linewidth=0.7) ax.text(sm1_r[i, 3].magnitude - 2, p[3].magnitude, str((i + 1) * 10)) # drawing LCL and LFC levels [lcl_pressure, lcl_temperature] = mpcalc.lcl(p[0], T[0], Td[0]) lcl_idx = np.argmin(np.abs(p.magnitude - lcl_pressure.magnitude)) [lfc_pressure, lfc_temperature] = mpcalc.lfc(p, T, Td) lfc_idx = np.argmin(np.abs(p.magnitude - lfc_pressure.magnitude)) # conserved mse of air parcel arising from 1000 hpa sm1_p = np.squeeze(np.ones((1, np.size(T))) * sm1[0]) # illustration of CAPE el_pressure, el_temperature = mpcalc.el(p, T, Td) # equilibrium level el_idx = np.argmin(np.abs(p.magnitude - el_pressure.magnitude)) ELps = [el_pressure.magnitude ] # Initialize an array of EL pressures for detrainment profile [CAPE, CIN] = mpcalc.cape_cin(p[:el_idx], T[:el_idx], Td[:el_idx], Tp[:el_idx]) plt.plot(sm1_p, p, color='green', linewidth=2) #ax.fill_betweenx(p[lcl_idx:el_idx+1],sm1_p[lcl_idx:el_idx+1],sm2[lcl_idx:el_idx+1],interpolate=True # ,color='green',alpha='0.3') ax.fill_betweenx(p, sd, sm1, color='deepskyblue', alpha='0.5') ax.set_xlabel('Specific entropies: sd, sm, sm_sat [J K$^{-1}$ kg$^{-1}$]', fontsize=14) ax.set_ylabel('Pressure [hPa]', fontsize=14) ax.set_xticks([0, 50, 100, 150, 200, 250, 300, 350]) ax.set_xlim([0, 440]) ax.set_ylim(1030, 120) if entrain is True: # Depict Entraining parcels # Parcel mass solves dM/dz = eps*M, solution is M = exp(eps*Z) # M=1 at ground without loss of generality # Distribution of surface parcel h offsets h0offsets = np.sort(np.random.normal( 0, h0_std, ensemble_size)) * units('joule/kilogram') # Distribution of entrainment rates entrainment_rates = ent_rate / (units('km')) for h0offset in h0offsets: h4ent = sm1.copy() h4ent[0] += h0offset for eps in entrainment_rates: M = np.exp(eps * (altitude - altitude[0])).to('dimensionless') # dM is the mass contribution at each level, with 1 at the origin level. M[0] = 0 dM = np.gradient(M) # parcel mass is a sum of all the dM's at each level # conserved linearly-mixed variables like h are weighted averages if eps.magnitude == 0.0: hent = np.ones(len(h4ent)) * h4ent[0] # no mixing else: hent = np.cumsum(dM * h4ent) / np.cumsum(dM) # Boolean for positive buoyancy, and its topmost altitude (index) where curve is clippes posboy = (hent > sm2) posboy[0] = True # so there is always a detrainment level # defining the first EL by posboy as the detrainment layer, swiching from positive buoyancy to # negative buoyancy (0 to 1) and skipping the surface ELindex_ent = 0 for idx in range(len(posboy) - 1): if posboy[idx + 1] == 0 and posboy[idx] == 1 and idx > 0: ELindex_ent = idx break # Plot the curve plt.plot(hent[0:ELindex_ent + 2], p[0:ELindex_ent + 2], linewidth=0.6, color='g') #plt.plot( hent[0:], p[0:], linewidth=0.6, color='g') # Keep a list for a histogram plot (detrainment profile) if p[ELindex_ent].magnitude < lfc_pressure.magnitude: # buoyant parcels only ELps.append(p[ELindex_ent].magnitude) # Plot a crude histogram of parcel detrainment levels NBINS = 20 pbins = np.linspace(1000, 150, num=NBINS) # pbins for detrainment levels hist = np.zeros((len(pbins) - 1)) for x in ELps: for i in range(len(pbins) - 1): if (x < pbins[i]) & (x >= pbins[i + 1]): hist[i] += 1 break det_per = hist / sum(hist) * 100 # percentages of detrainment ensumbles at levels ax2 = fig.add_axes([0.705, 0.1, 0.1, 0.8], facecolor=None) ax2.barh(pbins[1:], det_per, color='lightgrey', edgecolor='k', height=15 * (20 / NBINS)) ax2.set_xlim([0, 100]) ax2.set_xticks([0, 20, 40, 60, 80, 100]) ax2.set_ylim([1030, 120]) ax2.set_xlabel('Detrainment [%]') ax2.grid() ax2.set_zorder(2) ax.plot([400, 400], [1100, 0]) ax.annotate('Detrainment', xy=(362, 320), color='dimgrey') ax.annotate('ensemble: ' + str(ensemble_size * len(entrainment_rates)), xy=(364, 340), color='dimgrey') ax.annotate('Detrainment', xy=(362, 380), color='dimgrey') ax.annotate(' scale: 0 - 2 km', xy=(365, 400), color='dimgrey') # Overplots on the mess: undilute parcel and CAPE, etc. ax.plot((1, 1) * sm1[0], (1, 0) * (p[0]), color='g', linewidth=2) # Replot the sounding on top of all that mess ax.plot(sm2, p, color='r', linewidth=1.5) ax.plot(sm1, p, color='b', linewidth=1.5) # label LCL and LCF ax.plot((sm2[lcl_idx] + (-2000, 2000) * units('joule/kilogram')), lcl_pressure + (0, 0) * units('mbar'), color='orange', linewidth=3) ax.plot((sm2[lfc_idx] + (-2000, 2000) * units('joule/kilogram')), lfc_pressure + (0, 0) * units('mbar'), color='magenta', linewidth=3) # Plot a crude histogram of parcel detrainment levels # Text parts ax.text(30, pressure[3], 'RH (%)', fontsize=11, color='k') ax.text(20, 200, 'CAPE = ' + str(np.around(CAPE.magnitude, decimals=2)) + ' [J/kg]', fontsize=12, color='green') ax.text(20, 250, 'CIN = ' + str(np.around(CIN.magnitude, decimals=2)) + ' [J/kg]', fontsize=12, color='green') ax.text(20, 300, 'LCL = ' + str(np.around(lcl_pressure.magnitude, decimals=2)) + ' [hpa]', fontsize=12, color='darkorange') ax.text(20, 350, 'LFC = ' + str(np.around(lfc_pressure.magnitude, decimals=2)) + ' [hpa]', fontsize=12, color='magenta') ax.text(20, 400, 'CWV = ' + str(np.around(cwv.magnitude, decimals=2)) + ' [mm]', fontsize=12, color='deepskyblue') ax.text(20, 450, 'CRH = ' + str(np.around(crh.magnitude, decimals=2)) + ' [%]', fontsize=12, color='blue') ax.legend(['DEnt', 'MEnt', 'SMEnt'], fontsize=12, loc=1) ax.set_zorder(3) return (ax)
def cape_cin(pressure, temperature, dewpt, parcel_profile, dz, temp): r"""Calculate CAPE and CIN. This script is originally from Metpy module but it was not avaialble in Python 3.6.3 Anaconda version. JLGF. Calculate the convective available potential energy (CAPE) and convective inhibition (CIN) of a given upper air profile and parcel path. CIN is integrated between the surface and LFC, CAPE is integrated between the LFC and EL (or top of sounding). Intersection points of the measured temperature profile and parcel profile are linearly interpolated. Especifically this script has been adapted from :cite:`montearl` and :cite:`molinari2010` which use a very particular function for CAPE. CAPE is not trivially computed from dropsonde measurements and several cautions are extended: 1. Vertical profiles usually do not reach the equilibrium level (EL) but instead are cut-off at 8-9 km. 2. Typical CAPE formula estimates the area of the difference between parcel and environmental profiles, however, this method uses a more robust approach :cite:`bogner2000`. 3. Several corrections would need to be in place for this script to be comparable to other studies (see above). It is then a simple approximation and by no means a complete and thorough algorithm. Parameters ---------- pressure : `pint.Quantity` The atmospheric pressure level(s) of interest. The first entry should be the starting point pressure. temperature : `pint.Quantity` The atmospheric temperature corresponding to pressure. dewpt : `pint.Quantity` The atmospheric dew point corresponding to pressure. parcel_profile : `pint.Quantity` The temperature profile of the parcel Returns ------- `pint.Quantity` Convective available potential energy (CAPE). `pint.Quantity` Convective inhibition (CIN). Notes ----- Formula adopted from :cite:`montearl` .. math:: \text{CAPE} = \int_{LFC}^{EL} g\frac{(T_{v} - T_{ve})}{\overline{T_{ve}}} dz .. math:: \text{CIN} = \int_{SFC}^{LFC} g\frac{(T_{v} - T_{ve})}{\overline{T_{ve}}} dz * :math:`CAPE` Convective available potential energy * :math:`CIN` Convective inhibition * :math:`LFC` Pressure of the level of free convection * :math:`EL` Pressure of the equilibrium level * :math:`SFC` Level of the surface or beginning of parcel path * :math:`g` Gravitational acceleration * :math:`T_{v}` Parcel potential temperature. * :math:`T_{ve}` Environmental potential temperature. * :math:`\overline{T_{ve}}` Mean environmental potential temperature. * :math:`dz` Height array differential. See Also :meth:`toolbox._find_append_zero_crossings`, :meth:`toolbox.parcel_profile` """ # Calculate LFC limit of integration lfc_pressure = mpcalc.lfc(pressure, temperature, dewpt)[0] g = 9.806 * units.m / units.s**2 # If there is no LFC, no need to proceed. if np.isnan(lfc_pressure): return 0 * units('J/kg'), 0 * units('J/kg') else: lfc_pressure = lfc_pressure.magnitude # Calculate the EL limit of integration el_pressure = mpcalc.el(pressure, temperature, dewpt)[0] # No EL and we use the top reading of the sounding. if np.isnan(el_pressure): el_pressure = pressure[-1].magnitude else: el_pressure = el_pressure.magnitude # Difference between the parcel path and measured temperature profiles y = (parcel_profile - temperature).to(units.degK) dzx, yz = _find_append_zero_crossings(np.copy(dz), y) # Estimate zero crossings x, y = _find_append_zero_crossings(np.copy(pressure), y) x = np.flip(x, 0) lfc_height = np.nanmean(dzx[np.isclose(x, lfc_pressure, atol=1.5)]) el_height = np.nanmean(dzx[np.isclose(x, el_pressure, atol=1.5)]) Tv_env = np.nanmean(temperature[np.where((dz > lfc_height) & (dz < el_height))]) * units.degK # CAPE # Only use data between the LFC and EL for calculation p_mask = _less_or_close(x, lfc_pressure) & _greater_or_close( x, el_pressure) z_mask = _less_or_close(dzx, lfc_height) & _greater_or_close( dzx, el_height) x_clipped = dzx[p_mask] y_clipped = yz[p_mask] cape = ((g / Tv_env) * (np.trapz(y_clipped, x_clipped, np.diff(dz)) * units.degK * units.m)).to(units('J/kg')) # CIN # Only use data between the surface and LFC for calculation p_mask = _greater_or_close(x, lfc_pressure) x_clipped = x[p_mask] y_clipped = y[p_mask] cin = ((g / Tv_env) * (np.trapz(y_clipped, x_clipped, np.diff(dz)) * units.degK * units.m)).to(units('J/kg')) return cape, cin
markersize=8, fillstyle='none', label='LCL') # Calculate LCF height and plot as purple dot LCF_pressure, LCF_temperature = mpcalc.lfc( p, T, Td, prof_0) skew.plot(LCF_pressure, LCF_temperature, 'rx', markersize=8, fillstyle='none', label='LCF') # Calculate EL height and plot as blue dot EL_pressure, EL_temperature = mpcalc.el(p, T, Td, prof_0) skew.plot(EL_pressure, EL_temperature, 'bo', markersize=8, fillstyle='none', label='EL') # Calculate most_unstable_parcel mup_pressure, mup_temperature, mup_dewTemperature, mup_index = mpcalc.most_unstable_parcel( p, T, Td, heights=prof_0, bottom=1050 * units.hPa, depth=650 * units.hPa)
def msed_plots(pressure, temperature, mixing_ratio, h0_std=2000, ensemble_size=20, ent_rate=np.arange(0, 2, 0.05), entrain=False): """ plotting the summarized static energy diagram with annotations and thermodynamic parameters """ p = pressure * units('mbar') T = temperature * units('degC') q = mixing_ratio * units('kilogram/kilogram') qs = mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(T), p) Td = mpcalc.dewpoint(mpcalc.vapor_pressure(p, q)) # dewpoint Tp = mpcalc.parcel_profile(p, T[0], Td[0]).to('degC') # parcel profile # Altitude based on the hydrostatic eq. altitude = np.zeros((np.size(T))) * units('meter') # surface is 0 meter for i in range(np.size(T)): altitude[i] = mpcalc.thickness_hydrostatic( p[:i + 1], T[:i + 1]) # Hypsometric Eq. for height # Static energy calculations mse = mpcalc.moist_static_energy(altitude, T, q) mse_s = mpcalc.moist_static_energy(altitude, T, qs) dse = mpcalc.dry_static_energy(altitude, T) # Water vapor calculations p_PWtop = max(200 * units.mbar, min(p) + 1 * units.mbar) # integrating until 200mb cwv = mpcalc.precipitable_water(Td, p, top=p_PWtop) # column water vapor [mm] cwvs = mpcalc.precipitable_water( T, p, top=p_PWtop) # saturated column water vapor [mm] crh = (cwv / cwvs) * 100. # column relative humidity [%] #================================================ # plotting MSE vertical profiles fig = plt.figure(figsize=[12, 8]) ax = fig.add_axes([0.1, 0.1, 0.6, 0.8]) ax.plot(dse, p, '-k', linewidth=2) ax.plot(mse, p, '-b', linewidth=2) ax.plot(mse_s, p, '-r', linewidth=2) # mse based on different percentages of relative humidity qr = np.zeros((9, np.size(qs))) * units('kilogram/kilogram') mse_r = qr * units('joule/kilogram') # container for i in range(9): qr[i, :] = qs * 0.1 * (i + 1) mse_r[i, :] = mpcalc.moist_static_energy(altitude, T, qr[i, :]) for i in range(9): ax.plot(mse_r[i, :], p[:], '-', color='grey', linewidth=0.7) ax.text(mse_r[i, 3].magnitude / 1000 - 1, p[3].magnitude, str((i + 1) * 10)) # drawing LCL and LFC levels [lcl_pressure, lcl_temperature] = mpcalc.lcl(p[0], T[0], Td[0]) lcl_idx = np.argmin(np.abs(p.magnitude - lcl_pressure.magnitude)) [lfc_pressure, lfc_temperature] = mpcalc.lfc(p, T, Td) lfc_idx = np.argmin(np.abs(p.magnitude - lfc_pressure.magnitude)) # conserved mse of air parcel arising from 1000 hpa mse_p = np.squeeze(np.ones((1, np.size(T))) * mse[0].magnitude) # illustration of CAPE el_pressure, el_temperature = mpcalc.el(p, T, Td) # equilibrium level el_idx = np.argmin(np.abs(p.magnitude - el_pressure.magnitude)) ELps = [el_pressure.magnitude ] # Initialize an array of EL pressures for detrainment profile [CAPE, CIN] = mpcalc.cape_cin(p[:el_idx], T[:el_idx], Td[:el_idx], Tp[:el_idx]) plt.plot(mse_p, p, color='green', linewidth=2) ax.fill_betweenx(p[lcl_idx:el_idx + 1], mse_p[lcl_idx:el_idx + 1], mse_s[lcl_idx:el_idx + 1], interpolate=True, color='green', alpha='0.3') ax.fill_betweenx(p, dse, mse, color='deepskyblue', alpha='0.5') ax.set_xlabel('Specific static energies: s, h, hs [kJ kg$^{-1}$]', fontsize=14) ax.set_ylabel('Pressure [hpa]', fontsize=14) ax.set_xticks([280, 300, 320, 340, 360, 380]) ax.set_xlim([280, 390]) ax.set_ylim(1030, 120) if entrain is True: # Depict Entraining parcels # Parcel mass solves dM/dz = eps*M, solution is M = exp(eps*Z) # M=1 at ground without loss of generality # Distribution of surface parcel h offsets H0STDEV = h0_std # J/kg h0offsets = np.sort(np.random.normal( 0, H0STDEV, ensemble_size)) * units('joule/kilogram') # Distribution of entrainment rates entrainment_rates = ent_rate / (units('km')) for h0offset in h0offsets: h4ent = mse.copy() h4ent[0] += h0offset for eps in entrainment_rates: M = np.exp(eps * (altitude - altitude[0])).to('dimensionless') # dM is the mass contribution at each level, with 1 at the origin level. M[0] = 0 dM = np.gradient(M) # parcel mass is a sum of all the dM's at each level # conserved linearly-mixed variables like h are weighted averages hent = np.cumsum(dM * h4ent) / np.cumsum(dM) # Boolean for positive buoyancy, and its topmost altitude (index) where curve is clippes posboy = (hent > mse_s) posboy[0] = True # so there is always a detrainment level ELindex_ent = np.max(np.where(posboy)) # Plot the curve plt.plot(hent[0:ELindex_ent + 2], p[0:ELindex_ent + 2], linewidth=0.25, color='g') # Keep a list for a histogram plot (detrainment profile) if p[ELindex_ent].magnitude < lfc_pressure.magnitude: # buoyant parcels only ELps.append(p[ELindex_ent].magnitude) # Plot a crude histogram of parcel detrainment levels NBINS = 20 pbins = np.linspace(1000, 150, num=NBINS) # pbins for detrainment levels hist = np.zeros((len(pbins) - 1)) for x in ELps: for i in range(len(pbins) - 1): if (x < pbins[i]) & (x >= pbins[i + 1]): hist[i] += 1 break det_per = hist / sum(hist) * 100 # percentages of detrainment ensumbles at levels ax2 = fig.add_axes([0.705, 0.1, 0.1, 0.8], facecolor=None) ax2.barh(pbins[1:], det_per, color='lightgrey', edgecolor='k', height=15 * (20 / NBINS)) ax2.set_xlim([0, max(det_per)]) ax2.set_ylim([1030, 120]) ax2.set_xlabel('Detrainment [%]') ax2.grid() ax2.set_zorder(2) ax.plot([400, 400], [1100, 0]) ax.annotate('Detrainment', xy=(362, 320), color='dimgrey') ax.annotate('ensemble: ' + str(ensemble_size * len(entrainment_rates)), xy=(364, 340), color='dimgrey') ax.annotate('Detrainment', xy=(362, 380), color='dimgrey') ax.annotate(' scale: 0 - 2 km', xy=(365, 400), color='dimgrey') # Overplots on the mess: undilute parcel and CAPE, etc. ax.plot((1, 1) * mse[0], (1, 0) * (p[0]), color='g', linewidth=2) # Replot the sounding on top of all that mess ax.plot(mse_s, p, color='r', linewidth=1.5) ax.plot(mse, p, color='b', linewidth=1.5) # label LCL and LCF ax.plot((mse_s[lcl_idx] + (-2000, 2000) * units('joule/kilogram')), lcl_pressure + (0, 0) * units('mbar'), color='orange', linewidth=3) ax.plot((mse_s[lfc_idx] + (-2000, 2000) * units('joule/kilogram')), lfc_pressure + (0, 0) * units('mbar'), color='magenta', linewidth=3) ### Internal waves (100m adiabatic displacements, assumed adiabatic: conserves s, sv, h). #dZ = 100 *mpunits.units.meter dp = 1000 * units.pascal # depict displacements at sounding levels nearest these target levels targetlevels = [900, 800, 700, 600, 500, 400, 300, 200] * units.hPa for ilev in targetlevels: idx = np.argmin(np.abs(p - ilev)) # dp: hydrostatic rho = (p[idx]) / Rd / (T[idx]) dZ = -dp / rho / g # dT: Dry lapse rate dT/dz_dry is -g/Cp dT = (-g / Cp_d * dZ).to('kelvin') Tdisp = T[idx].to('kelvin') + dT # dhsat dqs = mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(Tdisp), p[idx] + dp) - qs[idx] dhs = g * dZ + Cp_d * dT + Lv * dqs # Whiskers on the data plots ax.plot((mse_s[idx] + dhs * (-1, 1)), p[idx] + dp * (-1, 1), linewidth=3, color='r') ax.plot((dse[idx] * (1, 1)), p[idx] + dp * (-1, 1), linewidth=3, color='k') ax.plot((mse[idx] * (1, 1)), p[idx] + dp * (-1, 1), linewidth=3, color='b') # annotation to explain it if ilev == 400 * ilev.units: ax.plot(360 * mse_s.units + dhs * (-1, 1) / 1000, 440 * units('mbar') + dp * (-1, 1), linewidth=3, color='r') ax.annotate('+/- 10mb', xy=(362, 440), fontsize=8) ax.annotate(' adiabatic displacement', xy=(362, 460), fontsize=8) # Plot a crude histogram of parcel detrainment levels # Text parts ax.text(290, pressure[3], 'RH (%)', fontsize=11, color='k') ax.text(285, 200, 'CAPE = ' + str(np.around(CAPE.magnitude, decimals=2)) + ' [J/kg]', fontsize=12, color='green') ax.text(285, 250, 'CIN = ' + str(np.around(CIN.magnitude, decimals=2)) + ' [J/kg]', fontsize=12, color='green') ax.text(285, 300, 'LCL = ' + str(np.around(lcl_pressure.magnitude, decimals=2)) + ' [hpa]', fontsize=12, color='darkorange') ax.text(285, 350, 'LFC = ' + str(np.around(lfc_pressure.magnitude, decimals=2)) + ' [hpa]', fontsize=12, color='magenta') ax.text(285, 400, 'CWV = ' + str(np.around(cwv.magnitude, decimals=2)) + ' [mm]', fontsize=12, color='deepskyblue') ax.text(285, 450, 'CRH = ' + str(np.around(crh.magnitude, decimals=2)) + ' [%]', fontsize=12, color='blue') ax.legend(['DSE', 'MSE', 'SMSE'], fontsize=12, loc=1) ax.set_zorder(3) return (ax)
def EnergyMassPlot(pressure, temperature, dewpoint, height, uwind, vwind, sphum=None, rh=None, label='', size=(12,10), return_fig=False): p=pressure Z=height T=temperature Td=dewpoint if isinstance(sphum,np.ndarray) and isinstance(rh,np.ndarray): q=sphum qs=q/rh else: q = mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(Td),p) qs= mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(T),p) s = g*Z + Cp_d*T sv= g*Z + Cp_d*mpcalc.virtual_temperature(T,q) h = s + Lv*q hs= s + Lv*qs parcel_Tprofile = mpcalc.parcel_profile(p, T[0], Td[0]).to('degC') CAPE,CIN = mpcalc.cape_cin(p,T,Td,parcel_Tprofile) ELp,ELT = mpcalc.el(p,T,Td) ELindex = np.argmin(np.abs(p - ELp)) lcl_pressure, lcl_temperature = mpcalc.lcl(p[0], T[0], Td[0]) p_PWtop = max(200*units.mbar, min(p) +1*units.mbar) PW = mpcalc.precipitable_water(Td,p, top=p_PWtop) PWs = mpcalc.precipitable_water(T,p, top=p_PWtop) CRH = (PW/PWs).magnitude *100. fig,ax=setup_fig(size=size,label=label) ax.plot(s /1000, p, color='r', linewidth=1.5) ### /1000 for kJ/kg ax.plot(sv /1000, p, color='r', linestyle='-.') ax.plot(h /1000, p, color='b', linewidth=1.5) ax.plot(hs /1000, p, color='r', linewidth=1.5) ### RH rulings between s and h lines: annotate near 800 hPa level annot_level = 800 #hPa idx = np.argmin(np.abs(p - annot_level *units.hPa)) right_annot_loc = 380 for iRH in np.arange(10,100,10): ax.plot( (s+ Lv*qs*iRH/100.)/1000, p, linewidth=0.5, linestyle=':', color='k') ax.annotate(str(iRH), xy=( (s[idx]+Lv*qs[idx]*iRH/100.)/1000, annot_level), horizontalalignment='center',fontsize=6) ax.annotate('RH (%)', xy=(right_annot_loc, annot_level), fontsize=10) if not np.isnan(CAPE.magnitude) and CAPE.magnitude >10: parcelh = h [0] # for a layer mean: np.mean(h[idx1:idx2]) parcelsv = sv[0] parcelp0 = p[0] # Undilute parcel ax.plot( (1,1)*parcelh/1000., (1,0)*parcelp0, linewidth=0.5, color='g') maxbindex = np.argmax(parcel_Tprofile - T) ax.annotate('CAPE='+str(int(CAPE.magnitude)), xy=(parcelh/1000., p[maxbindex]), color='g') # Plot LCL at saturation point, above the lifted sv of the surface parcel lcl_pressure, lcl_temperature = mpcalc.lcl(p[0], T[0], Td[0]) ax.annotate('LCL', xy=(sv[0]/1000., lcl_pressure), fontsize=10, color='g', horizontalalignment='right') # Purple fill for negative buoyancy below LCL: ax.fill_betweenx(p, sv/1000., parcelsv/1000., where=p>lcl_pressure, facecolor='purple', alpha=0.4) # Positive moist convective buoyancy in green # Above LCL: ax.fill_betweenx(p, hs/1000., parcelh/1000., where= parcelh>hs, facecolor='g', alpha=0.4) # Depict Entraining parcels # Parcel mass solves dM/dz = eps*M, solution is M = exp(eps*Z) # M=1 at ground without loss of generality entrainment_distance = 10000., 5000., 2000. ax.annotate('entrain: 10,5,2 km', xy=(parcelh/1000, 140), color='g', fontsize=8, horizontalalignment='right') ax.annotate('parcel h', xy=(parcelh/1000, 120), color='g', fontsize=10, horizontalalignment='right') for ED in entrainment_distance: eps = 1.0 / (ED*units.meter) M = np.exp(eps * (Z-Z[0]).to('m')).to('dimensionless') # dM is the mass contribution at each level, with 1 at the origin level. M[0] = 0 dM = np.gradient(M) # parcel mass is a sum of all the dM's at each level # conserved linearly-mixed variables like h are weighted averages hent = np.cumsum(dM*h) / np.cumsum(dM) ax.plot( hent[0:ELindex+3]/1000., p[0:ELindex+3], linewidth=0.5, color='g') ### Internal waves (100m adiabatic displacements, assumed adiabatic: conserves s, sv, h). dZ = 100 *units.meter # depict displacements at sounding levels nearest these target levels targetlevels = [900,800,700,600,500,400,300,200]*units.hPa for ilev in targetlevels: idx = np.argmin(np.abs(p - ilev)) # dT: Dry lapse rate dT/dz_dry is -g/Cp dT = (-g/Cp_d *dZ).to('kelvin') Tdisp = T[idx].to('kelvin') + dT # dp: hydrostatic rho = (p[idx]/Rd/T[idx]) dp = -rho*g*dZ # dhsat #qs = mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(T) ,p) dqs = mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(Tdisp) ,p[idx]+dp) -qs[idx] dhs = g*dZ + Cp_d*dT + Lv*dqs # Whiskers on the data plots ax.plot( (hs[idx]+dhs*(-1,1))/1000, p[idx]+dp*(-1,1), linewidth=3, color='r') ax.plot( (s [idx] *( 1,1))/1000, p[idx]+dp*(-1,1), linewidth=3, color='r') ax.plot( (h [idx] *( 1,1))/1000, p[idx]+dp*(-1,1), linewidth=3, color='b') # annotation to explain it if ilev == 600*ilev.units: ax.plot(right_annot_loc*hs.units +dhs*(-1,1)/1000, p[idx]+dp*(-1,1), linewidth=3, color='r') ax.annotate('+/- 100m', xy=(right_annot_loc,600), fontsize=8) ax.annotate(' internal', xy=(right_annot_loc,630), fontsize=8) ax.annotate(' waves', xy=(right_annot_loc,660), fontsize=8) ### Blue fill proportional to precipitable water, and blue annotation ax.fill_betweenx(p, s/1000., h/1000., where=h > s, facecolor='b', alpha=0.4) # Have to specify the top of the PW integral. # I want whole atmosphere of course, but 200 hPa captures it all really. #import metpy.calc as mpcalc p_PWtop = max(200*units.mbar, min(p) +1*units.mbar) PW = mpcalc.precipitable_water(Td,p, top=p_PWtop) PWs = mpcalc.precipitable_water(T,p, top=p_PWtop) CRH = (PW/PWs).magnitude *100. # PW annotation arrow tip at 700 mb idx = np.argmin(np.abs(p - 700*p.units)) centerblue = (s[idx]+h[idx])/2.0 /1000. ax.annotate('CWV='+str(round(PW.to('mm').magnitude, 1))+'mm', xy=(centerblue, 700), xytext=(285, 200), color='blue', fontsize=15, arrowprops=dict(width = 1, edgecolor='blue', shrink=0.02), ) ax.annotate('(' + str(round(CRH,1)) +'% of sat)', xy=(285, 230), color='blue', fontsize=12) ### Surface water values at 1C intervals, for eyeballing surface fluxes sT = np.trunc(T[0].to('degC')) sTint = int(sT.magnitude) for idT in [-2,0,2,4]: ssTint = sTint + idT # UNITLESS degC integers, for labels # Kelvin values for computations ssTC = ssTint * units.degC ssTK = ssTC.to('kelvin') ss = g*Z[0] + Cp_d*ssTK hs = ss + Lv*mpcalc.mixing_ratio(mpcalc.saturation_vapor_pressure(ssTK) ,p[0]) ax.annotate(str(ssTint), xy=(ss/1000., p[0]+0*p.units), verticalalignment='top', horizontalalignment='center', color='red', fontsize=7) ax.annotate(str(ssTint), xy=(hs/1000., p[0]+0*p.units), verticalalignment='top', horizontalalignment='center', color='red', fontsize=9) ax.annotate('\u00b0C water', xy=(right_annot_loc, p[0]), verticalalignment='top', fontsize=10, color='r') if return_fig: return ax,fig
def SkewT_plot(p, T, Td, heights, u=0, v=0, wind_barb=0, p_lims=[1000, 100], T_lims=[-50, 35], metpy_logo=0, plt_lfc=0, plt_lcl=0, plt_el=0, title=None): #plotting fig = plt.figure(figsize=(10, 10)) skew = plots.SkewT(fig) skew.plot(p, T, 'red') #Virtual Temperature skew.plot(p, Td, 'green') #Dewpoint skew.ax.set_ylim(p_lims) skew.ax.set_xlim(T_lims) if wind_barb == 1: # resampling wind barbs interval = np.logspace(2, 3) * units.hPa idx = mpcalc.resample_nn_1d(p, interval) skew.plot_barbs(p[idx], u[idx], v[idx]) #Showing Adiabasts and Mixing Ratio Lines skew.plot_dry_adiabats() skew.plot_moist_adiabats() skew.plot_mixing_lines() parcel_path = mpcalc.parcel_profile(p, T[0], Td[0]) skew.plot(p, parcel_path, color='k') # CAPE and CIN skew.shade_cape(p, T, parcel_path) skew.shade_cin(p, T, parcel_path) # Isotherms and Isobars # skew.ax.axhline(500 * units.hPa, color='k') # skew.ax.axvline(0 * units.degC, color='c') # LCL, LFC, EL lcl_p, lcl_T = mpcalc.lcl(p[0], T[0], Td[0]) lfc_p, lfc_T = mpcalc.lfc(p, T, Td) el_p, el_T = mpcalc.el(p, T, Td) if plt_lfc == 1: skew.ax.axhline(lfc_p, color='k') if plt_lcl == 1: skew.ax.axhline(lcl_p, color='k') # skew.ax.text(lcl_p, ) if plt_el == 1: skew.ax.axhline(el_p, color='k') if metpy_logo == 1: plots.add_metpy_logo(fig, x=55, y=50) decimate = 3 for p, T, heights in zip(df['pressure'][::decimate], df['temperature'][::decimate], df['height'][::decimate]): if p >= 700: skew.ax.text(T + 1, p, round(heights, 0)) plt.title(title) plt.show() return
#plots the barbs my_interval = np.arange(100, 1000, 20) * units('mbar') ix = resample_nn_1d(p, my_interval) skew.plot_barbs(p[ix], u[ix], v[ix]) skew.ax.set_ylim(1075, 100) skew.ax.set_ylabel('Pressure (hPa)') lcl_pressure, lcl_temperature = mpcalc.lcl(p[0], T[0], Td[0]) #LCL pwat = mpcalc.precipitable_water(Td, p, 500 * units.hectopascal).to('in') #PWAT cape, cin = mpcalc.most_unstable_cape_cin(p[:], T[:], Td[:]) #MUCAPE cape_sfc, cin_sfc = mpcalc.surface_based_cape_cin(p, T, Td) #SBCAPE prof = mpcalc.parcel_profile(p, T[0], Td[0]).to('degC') #parcel profile equiv_pot_temp = mpcalc.equivalent_potential_temperature(p, T, Td) #equivalent potential temperature el_pressure, el_temperature = mpcalc.el(p, T, Td) #elevated level lfc_pressure, lfc_temperature = mpcalc.lfc(p, T, Td) #LFC #calculates shear u_threekm_bulk_shear, v_threekm_bulk_shear = mpcalc.bulk_shear(p, u, v, hgt, bottom = min(hgt), depth = 3000 * units.meter) threekm_bulk_shear = mpcalc.get_wind_speed(u_threekm_bulk_shear, v_threekm_bulk_shear) u_onekm_bulk_shear, v_onekm_bulk_shear = mpcalc.bulk_shear(p, u, v, hgt, bottom = min(hgt), depth = 1000 * units.meter) onekm_bulk_shear = mpcalc.get_wind_speed(u_onekm_bulk_shear, v_onekm_bulk_shear) #shows the level of the LCL, LFC, and EL. skew.ax.text(T[0].magnitude, p[0].magnitude + 5, str(int(np.round(T[0].to('degF').magnitude))), fontsize = 'medium', horizontalalignment = 'left', verticalalignment = 'top', color = 'red') skew.ax.text(Td[0].magnitude, p[0].magnitude + 5, str(int(np.round(Td[0].to('degF').magnitude))), fontsize = 'medium', horizontalalignment = 'right', verticalalignment = 'top', color = 'green') skew.ax.text(lcl_temperature.magnitude + 5, lcl_pressure.magnitude, "---- LCL", fontsize = 'medium', verticalalignment = 'center') skew.ax.text(Td[0].magnitude - 10, p[0].magnitude, 'SFC: {}hPa ----'.format(p[0].magnitude), fontsize = 'medium', horizontalalignment = 'right', verticalalignment = 'center', color = 'black') if str(lfc_temperature.magnitude) != 'nan': #checks to see if LFC/EL exists. If not, skip.